open-loop-killer
Version:
Inject timeout protection into JavaScript loops to prevent infinite loops. Supports while, for, do-while, for-in, and for-of loops with customizable timeout and error messages.
779 lines (651 loc) • 28.8 kB
JavaScript
;
const { expect } = require('chai');
const { injector } = require('../index.js');
const vm = require('vm');
// Helper function to verify code is compileable
function expectCompileable(code, context = {}) {
expect(() => {
const script = new vm.Script(code);
const vmContext = vm.createContext(context);
script.runInContext(vmContext);
}).to.not.throw();
}
// Helper function to verify code compilation only (not execution)
function expectCompileableOnly(code) {
expect(() => {
new vm.Script(code);
}).to.not.throw();
}
describe('Open Loop Killer - Injector', function() {
describe('While Loop Injection', function() {
it('should inject protection code into a simple while loop', function() {
const code = `while(true) { }`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expect(result).to.include('Open Loop Detected!');
expectCompileableOnly(result);
});
it('should inject protection code into while loop with condition', function() {
const code = `
let i = 0;
while(i < 100) {
i++;
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('1000');
expectCompileable(result);
});
it('should inject protection code into while loop with single statement body', function() {
const code = `while(true) console.log('test');`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expectCompileableOnly(result);
});
});
describe('For Loop Injection', function() {
it('should inject protection code into a simple for loop', function() {
const code = `for(let i = 0; i < 10; i++) { }`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expectCompileable(result);
});
it('should inject protection code into for loop with body', function() {
const code = `
for(let i = 0; i < 10; i++) {
console.log(i);
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('Open Loop Detected!');
expectCompileable(result, { console: { log: () => {} } });
});
it('should inject protection code into infinite for loop', function() {
const code = `for(;;) { }`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expectCompileableOnly(result);
});
it('should inject protection code into for loop with single statement', function() {
const code = `for(let i = 0; i < 10; i++) console.log(i);`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expectCompileable(result, { console: { log: () => {} } });
});
});
describe('Do-While Loop Injection', function() {
it('should inject protection code into do-while loop', function() {
const code = `
let i = 0;
do {
i++;
} while(i < 10);
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expectCompileable(result);
});
it('should inject protection code into infinite do-while loop', function() {
const code = `do { } while(true);`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('1000');
expectCompileableOnly(result);
});
});
describe('Nested Loops', function() {
it('should inject protection code into nested while loops', function() {
const code = `
while(true) {
while(true) {
break;
}
break;
}
`;
const result = injector(code);
// Should have multiple injection points
const dateNowCount = (result.match(/Date\.now\(\)/g) || []).length;
expect(dateNowCount).to.be.at.least(4); // 2 before loops + 2 inside loops
expectCompileable(result);
});
it('should inject protection code into nested for loops', function() {
const code = `
for(let i = 0; i < 10; i++) {
for(let j = 0; j < 10; j++) {
console.log(i, j);
}
}
`;
const result = injector(code);
const dateNowCount = (result.match(/Date\.now\(\)/g) || []).length;
expect(dateNowCount).to.be.at.least(4);
expectCompileable(result, { console: { log: () => {} } });
});
it('should inject protection code into mixed nested loops', function() {
const code = `
for(let i = 0; i < 10; i++) {
while(i < 5) {
i++;
}
}
`;
const result = injector(code);
const dateNowCount = (result.match(/Date\.now\(\)/g) || []).length;
expect(dateNowCount).to.be.at.least(4);
expectCompileable(result);
});
});
describe('Code Without Loops', function() {
it('should return code unchanged if no loops present', function() {
const code = `
let x = 5;
let y = 10;
console.log(x + y);
`;
const result = injector(code);
expect(result).to.not.include('Date.now()');
expect(result).to.include('let x = 5');
expectCompileable(result, { console: { log: () => {} } });
});
it('should handle function declarations without loops', function() {
const code = `
function test() {
return 42;
}
`;
const result = injector(code);
expect(result).to.not.include('Date.now()');
expect(result).to.include('function test');
expectCompileable(result);
});
});
describe('Loops Inside Functions', function() {
it('should inject protection into loops inside functions', function() {
const code = `
function test() {
while(true) {
break;
}
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expectCompileable(result);
});
it('should inject protection into multiple loops in different functions', function() {
const code = `
function test1() {
for(let i = 0; i < 10; i++) { }
}
function test2() {
while(true) { break; }
}
`;
const result = injector(code);
const dateNowCount = (result.match(/Date\.now\(\)/g) || []).length;
expect(dateNowCount).to.be.at.least(4);
expectCompileable(result);
});
});
describe('Complex Code Structures', function() {
it('should handle loops with complex bodies', function() {
const code = `
for(let i = 0; i < 10; i++) {
if(i % 2 === 0) {
console.log('even');
} else {
console.log('odd');
}
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('even');
expect(result).to.include('odd');
expectCompileable(result, { console: { log: () => {} } });
});
it('should handle loops with try-catch blocks', function() {
const code = `
while(true) {
try {
doSomething();
} catch(e) {
break;
}
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('try');
expect(result).to.include('catch');
expectCompileable(result, { doSomething: () => { throw new Error('test'); } });
});
});
describe('Error Handling', function() {
it('should throw error for invalid JavaScript syntax', function() {
const code = `while(true) { this is invalid }`;
expect(() => injector(code)).to.throw();
});
it('should throw error for incomplete code', function() {
const code = `while(true) {`;
expect(() => injector(code)).to.throw();
});
it('should handle empty code', function() {
const code = ``;
const result = injector(code);
expect(result).to.equal('');
});
it('should handle code with only whitespace', function() {
const code = ` \n\n `;
const result = injector(code);
expect(result.trim()).to.equal('');
});
});
describe('Variable Name Collision Prevention', function() {
it('should use unique variable names for each loop', function() {
const code = `
while(true) { break; }
for(;;) { break; }
`;
const result = injector(code);
// Extract all variable names that start with underscore
const varMatches = result.match(/let _\w+/g) || [];
// Should have at least 2 unique variable declarations
expect(varMatches.length).to.be.at.least(2);
expectCompileable(result);
});
it('should not conflict with existing variables', function() {
const code = `
let _abc = 5;
while(true) {
console.log(_abc);
break;
}
`;
const result = injector(code);
expect(result).to.include('let _abc = 5');
expect(result).to.include('Date.now()');
expectCompileable(result, { console: { log: () => {} } });
});
});
describe('Return Value', function() {
it('should return a string', function() {
const code = `while(true) { break; }`;
const result = injector(code);
expect(result).to.be.a('string');
expectCompileable(result);
});
it('should return valid JavaScript code', function() {
const code = `
for(let i = 0; i < 10; i++) {
console.log(i);
}
`;
const result = injector(code);
// Should be parseable JavaScript
expect(() => {
require('esprima').parseScript(result);
}).to.not.throw();
expectCompileable(result, { console: { log: () => {} } });
});
});
describe('For-In Loop Injection', function() {
it('should inject protection code into for-in loop', function() {
const code = `
const obj = {a: 1, b: 2, c: 3};
for(let key in obj) {
console.log(key);
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expect(result).to.include('Open Loop Detected!');
expectCompileable(result, { console: { log: () => {} } });
});
it('should inject protection into infinite for-in loop', function() {
const code = `
const obj = {};
for(let key in obj) {
obj.newKey = 1;
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('1000');
expectCompileable(result);
});
it('should inject protection into for-in loop with single statement', function() {
const code = `for(let key in {a:1}) console.log(key);`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expectCompileable(result, { console: { log: () => {} } });
});
it('should handle nested for-in loops', function() {
const code = `
const obj1 = {a: 1, b: 2};
const obj2 = {x: 1, y: 2};
for(let key1 in obj1) {
for(let key2 in obj2) {
console.log(key1, key2);
}
}
`;
const result = injector(code);
const dateNowCount = (result.match(/Date\.now\(\)/g) || []).length;
expect(dateNowCount).to.be.at.least(4);
expectCompileable(result, { console: { log: () => {} } });
});
});
describe('For-Of Loop Injection', function() {
it('should inject protection code into for-of loop', function() {
const code = `
const arr = [1, 2, 3];
for(let val of arr) {
console.log(val);
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expect(result).to.include('Open Loop Detected!');
expectCompileable(result, { console: { log: () => {} } });
});
it('should inject protection into for-of loop with generator', function() {
const code = `
function* gen() { yield 1; yield 2; }
for(let val of gen()) {
console.log(val);
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('1000');
expectCompileable(result, { console: { log: () => {} } });
});
it('should inject protection into for-of loop with single statement', function() {
const code = `for(let val of [1,2,3]) console.log(val);`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('throw new Error');
expectCompileable(result, { console: { log: () => {} } });
});
it('should handle nested for-of loops', function() {
const code = `
const arr1 = [1, 2];
const arr2 = [3, 4];
for(let val1 of arr1) {
for(let val2 of arr2) {
console.log(val1, val2);
}
}
`;
const result = injector(code);
const dateNowCount = (result.match(/Date\.now\(\)/g) || []).length;
expect(dateNowCount).to.be.at.least(4);
expectCompileable(result, { console: { log: () => {} } });
});
it('should inject protection into for-of with string', function() {
const code = `
for(let char of "hello") {
console.log(char);
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expectCompileable(result, { console: { log: () => {} } });
});
});
describe('Mixed Loop Types', function() {
it('should handle for-in and for-of loops together', function() {
const code = `
const obj = {a: 1, b: 2};
const arr = [1, 2, 3];
for(let key in obj) {
console.log(key);
}
for(let val of arr) {
console.log(val);
}
`;
const result = injector(code);
const dateNowCount = (result.match(/Date\.now\(\)/g) || []).length;
expect(dateNowCount).to.be.at.least(4);
expectCompileable(result, { console: { log: () => {} } });
});
it('should handle all loop types in one code block', function() {
const code = `
while(true) { break; }
for(let i = 0; i < 10; i++) { }
do { break; } while(true);
for(let key in {a:1}) { }
for(let val of [1]) { }
`;
const result = injector(code);
// Should have 5 loops protected
const dateNowCount = (result.match(/Date\.now\(\)/g) || []).length;
expect(dateNowCount).to.be.at.least(10); // 5 loops * 2 (before + inside)
expectCompileable(result);
});
});
describe('Edge Cases', function() {
it('should handle loop with labeled statement', function() {
const code = `
outer: while(true) {
break outer;
}
`;
const result = injector(code);
// Should not break on labeled statements (even if injection doesn't occur)
expect(result).to.be.a('string');
expect(result).to.include('outer:');
expect(result).to.include('while');
expectCompileable(result);
});
it('should handle for-in with computed properties', function() {
const code = `
const obj = {a: 1, b: 2};
for(let key in obj) {
console.log(obj[key]);
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expectCompileable(result, { console: { log: () => {} } });
});
it('should handle for-of with destructuring', function() {
const code = `
const arr = [[1, 2], [3, 4]];
for(let [a, b] of arr) {
console.log(a, b);
}
`;
const result = injector(code);
expect(result).to.include('Date.now()');
expectCompileable(result, { console: { log: () => {} } });
});
});
describe('Configuration Options', function() {
describe('Custom Timeout', function() {
it('should accept custom timeout value', function() {
const code = `while(true) { }`;
const result = injector(code, { timeout: 2000 });
expect(result).to.include('Date.now()');
expect(result).to.include('2000');
expect(result).to.not.include('1000');
expectCompileableOnly(result);
});
it('should use default timeout when not specified', function() {
const code = `while(true) { }`;
const result = injector(code);
expect(result).to.include('1000');
expectCompileableOnly(result);
});
it('should accept very short timeout', function() {
const code = `for(;;) { }`;
const result = injector(code, { timeout: 100 });
expect(result).to.include('100');
expectCompileableOnly(result);
});
it('should accept very long timeout', function() {
const code = `while(true) { break; }`;
const result = injector(code, { timeout: 30000 });
expect(result).to.include('30000');
expectCompileable(result);
});
it('should throw error for invalid timeout (negative)', function() {
const code = `while(true) { }`;
expect(() => injector(code, { timeout: -1 })).to.throw('options.timeout must be a positive number');
});
it('should throw error for invalid timeout (zero)', function() {
const code = `while(true) { }`;
expect(() => injector(code, { timeout: 0 })).to.throw('options.timeout must be a positive number');
});
it('should throw error for invalid timeout (non-number)', function() {
const code = `while(true) { }`;
expect(() => injector(code, { timeout: '1000' })).to.throw('options.timeout must be a positive number');
});
});
describe('Custom Error Message', function() {
it('should accept custom error message', function() {
const code = `while(true) { }`;
const result = injector(code, { errorMessage: 'Custom timeout!' });
expect(result).to.include('Custom timeout!');
expect(result).to.not.include('Open Loop Detected!');
expectCompileableOnly(result);
});
it('should use default error message when not specified', function() {
const code = `while(true) { }`;
const result = injector(code);
expect(result).to.include('Open Loop Detected!');
expectCompileableOnly(result);
});
it('should handle error message with special characters', function() {
const code = `for(;;) { }`;
const result = injector(code, { errorMessage: 'Timeout: execution exceeded!' });
expect(result).to.include('Timeout: execution exceeded!');
expectCompileableOnly(result);
});
it('should throw error for invalid error message (non-string)', function() {
const code = `while(true) { }`;
expect(() => injector(code, { errorMessage: 123 })).to.throw('options.errorMessage must be a string');
});
});
describe('Combined Options', function() {
it('should accept both custom timeout and error message', function() {
const code = `while(true) { }`;
const result = injector(code, {
timeout: 3000,
errorMessage: 'Loop took too long!'
});
expect(result).to.include('3000');
expect(result).to.include('Loop took too long!');
expectCompileableOnly(result);
});
it('should work with both timeout and error message', function() {
const code = `for(let i = 0; i < 10; i++) { }`;
const result = injector(code, {
timeout: 2500,
errorMessage: 'Custom message'
});
expect(result).to.include('2500');
expect(result).to.include('Custom message');
expectCompileable(result);
});
});
describe('Backward Compatibility', function() {
it('should work without options parameter', function() {
const code = `while(true) { break; }`;
const result = injector(code);
expect(result).to.include('Date.now()');
expect(result).to.include('1000');
expect(result).to.include('Open Loop Detected!');
expectCompileable(result);
});
it('should work with empty options object', function() {
const code = `while(true) { break; }`;
const result = injector(code, {});
expect(result).to.include('1000');
expect(result).to.include('Open Loop Detected!');
expectCompileable(result);
});
it('should work with partial options', function() {
const code = `for(;;) { break; }`;
const result = injector(code, { timeout: 5000 });
expect(result).to.include('5000');
expect(result).to.include('Open Loop Detected!');
expectCompileable(result);
});
});
describe('Options Validation', function() {
it('should ignore unknown options', function() {
const code = `while(true) { break; }`;
const result = injector(code, { unknownOption: 'test' });
expect(result).to.be.a('string');
expectCompileable(result);
});
});
describe('Multiple Loops with Custom Options', function() {
it('should apply custom options to all loops', function() {
const code = `
while(true) { break; }
for(;;) { break; }
do { break; } while(true);
`;
const result = injector(code, {
timeout: 4000,
errorMessage: 'All loops timeout!'
});
// Should have 3 loops with custom settings
const timeoutMatches = (result.match(/4000/g) || []).length;
const messageMatches = (result.match(/All loops timeout!/g) || []).length;
expect(timeoutMatches).to.equal(3);
expect(messageMatches).to.equal(3);
expectCompileable(result);
});
it('should apply custom options to for-in and for-of loops', function() {
const code = `
for(let key in {a:1}) { }
for(let val of [1]) { }
`;
const result = injector(code, {
timeout: 3500,
errorMessage: 'Custom for-in/of timeout!'
});
expect(result).to.include('3500');
expect(result).to.include('Custom for-in/of timeout!');
expectCompileable(result);
});
it('should apply custom options to all loop types', function() {
const code = `
while(true) { break; }
for(;;) { break; }
do { break; } while(true);
for(let k in {}) { }
for(let v of []) { }
`;
const result = injector(code, {
timeout: 6000,
errorMessage: 'All types timeout!'
});
// Should have 5 loops with custom settings
const timeoutMatches = (result.match(/6000/g) || []).length;
const messageMatches = (result.match(/All types timeout!/g) || []).length;
expect(timeoutMatches).to.equal(5);
expect(messageMatches).to.equal(5);
expectCompileable(result);
});
});
});
});