vm-helpers.test.ts•22.2 kB
import ivm from 'isolated-vm';
import { describe, expect, it } from 'vitest';
import { injectVMHelpersIndividually } from './vm-helpers.js';
describe('VM Helpers', () => {
describe('injectVMHelpersIndividually', () => {
it('should inject btoa function that encodes base64', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test basic base64 encoding
const result = await context.eval(`
btoa('Hello World')
`);
expect(result).toBe('SGVsbG8gV29ybGQ=');
});
it('should inject atob function that decodes base64', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test basic base64 decoding
const result = await context.eval(`
atob('SGVsbG8gV29ybGQ=')
`);
expect(result).toBe('Hello World');
});
it('should handle URL-safe base64 in atob', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test URL-safe base64 (- and _ characters)
const result = await context.eval(`
atob('SGVsbG8tV29ybGRf')
`);
expect(result).toBeTruthy();
});
it('should inject escape function', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test escape function
const result = await context.eval(`
escape('Hello World!')
`);
expect(result).toBe('Hello%20World!');
});
it('should inject decodeURIComponent function', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test decodeURIComponent
const result = await context.eval(`
decodeURIComponent('Hello%20World%21')
`);
expect(result).toBe('Hello World!');
});
it('should inject Buffer.from for base64 decoding', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test Buffer.from with base64
const result = await context.eval(`
Buffer.from('SGVsbG8gV29ybGQ=', 'base64').toString('utf-8')
`);
expect(result).toBe('Hello World');
});
it('should handle UTF-8 decoding with Buffer', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test UTF-8 handling
const result = await context.eval(`
const base64 = 'eyJuYW1lIjoi8J+YgCJ9'; // {"name":"😀"}
const decoded = Buffer.from(base64, 'base64').toString('utf-8');
JSON.parse(decoded).name
`);
expect(result).toBe('😀');
});
it('should handle complex base64 decoding scenarios', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test complex scenario combining multiple helpers
const result = await context.eval(`
const base64 = 'eyJ1cmwiOiJodHRwcyUzQSUyRiUyRmV4YW1wbGUuY29tJTJGcGF0aCUzRnF1ZXJ5JTNEdmFsdWUifQ==';
const decoded = atob(base64);
const parsed = JSON.parse(decoded);
decodeURIComponent(parsed.url);
`);
expect(result).toBe('https://example.com/path?query=value');
});
it('should handle btoa edge cases', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test empty string
const empty = await context.eval(`btoa('')`);
expect(empty).toBe('');
// Test string with special characters within Latin1 range
const special = await context.eval(`btoa('Hello\\n\\r\\t!')`);
expect(special).toBe('SGVsbG8KDQkh');
// Test Latin1 characters (0-255)
const latin1 = await context.eval(`btoa('café')`);
expect(latin1).toBeTruthy();
});
it('should throw error for non-Latin1 characters in btoa', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test with emoji (outside Latin1 range)
await expect(context.eval(`btoa('Hello 😀')`)).rejects.toThrow('btoa failed');
});
it('should handle btoa/atob round-trip correctly', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test round-trip encoding/decoding
const result = await context.eval(`
const original = 'The quick brown fox jumps over the lazy dog!@#$%^&*()_+-=[]{}|;:,.<>?';
const encoded = btoa(original);
const decoded = atob(encoded);
decoded === original;
`);
expect(result).toBe(true);
});
it('should handle btoa padding correctly', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test different string lengths to ensure proper padding
const oneChar = await context.eval(`btoa('a')`);
expect(oneChar).toBe('YQ=='); // Should have 2 padding chars
const twoChars = await context.eval(`btoa('ab')`);
expect(twoChars).toBe('YWI='); // Should have 1 padding char
const threeChars = await context.eval(`btoa('abc')`);
expect(threeChars).toBe('YWJj'); // Should have no padding
});
it('should inject URL constructor that parses URLs', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test basic URL parsing
const result = await context.eval(`
const url = new URL('https://example.com:8080/path?query=value#hash');
JSON.stringify({
href: url.href,
protocol: url.protocol,
hostname: url.hostname,
port: url.port,
pathname: url.pathname,
search: url.search,
hash: url.hash
})
`);
const parsed = JSON.parse(result);
expect(parsed.protocol).toBe('https:');
expect(parsed.hostname).toBe('example.com');
expect(parsed.port).toBe('8080');
expect(parsed.pathname).toBe('/path');
expect(parsed.search).toBe('?query=value');
expect(parsed.hash).toBe('#hash');
});
it('should handle URL with base parameter', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test URL with base
const result = await context.eval(`
const url = new URL('/api/users', 'https://example.com');
url.href
`);
expect(result).toBe('https://example.com/api/users');
});
it('should inject crypto.randomUUID that generates UUIDs', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Test crypto.randomUUID
const uuid = await context.eval(`crypto.randomUUID()`);
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
expect(uuid).toMatch(uuidRegex);
});
it('should generate unique UUIDs', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
// Generate multiple UUIDs and ensure they're unique
const result = await context.eval(`
const uuid1 = crypto.randomUUID();
const uuid2 = crypto.randomUUID();
const uuid3 = crypto.randomUUID();
JSON.stringify({ uuid1, uuid2, uuid3, allUnique: uuid1 !== uuid2 && uuid2 !== uuid3 && uuid1 !== uuid3 })
`);
const parsed = JSON.parse(result);
expect(parsed.allUnique).toBe(true);
});
it('should inject String.prototype.matchAll', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const str = 'test1test2test3';
const matches = [...str.matchAll(/test(\\d)/g)];
JSON.stringify({
count: matches.length,
first: matches[0][1],
second: matches[1][1],
third: matches[2][1]
})
`);
const parsed = JSON.parse(result);
expect(parsed.count).toBe(3);
expect(parsed.first).toBe('1');
expect(parsed.second).toBe('2');
expect(parsed.third).toBe('3');
});
it('should throw error for non-global regex in matchAll', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
await expect(context.eval(`
'test'.matchAll(/test/)
`)).rejects.toThrow();
});
it('should inject String.prototype.replaceAll', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
'foo bar foo baz foo'.replaceAll('foo', 'qux')
`);
expect(result).toBe('qux bar qux baz qux');
});
it('should handle replaceAll with regex', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
'test1 test2 test3'.replaceAll(/test\\d/g, 'replaced')
`);
expect(result).toBe('replaced replaced replaced');
});
it('should inject String.prototype.trimStart and trimEnd', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const str = ' hello world ';
JSON.stringify({
trimStart: str.trimStart(),
trimEnd: str.trimEnd(),
trimLeft: str.trimLeft(),
trimRight: str.trimRight()
})
`);
const parsed = JSON.parse(result);
expect(parsed.trimStart).toBe('hello world ');
expect(parsed.trimEnd).toBe(' hello world');
expect(parsed.trimLeft).toBe('hello world ');
expect(parsed.trimRight).toBe(' hello world');
});
it('should inject String.prototype.padStart and padEnd', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const str = '5';
JSON.stringify({
padStart: str.padStart(3, '0'),
padEnd: str.padEnd(3, '0'),
padStartCustom: 'abc'.padStart(10, '123'),
padEndCustom: 'abc'.padEnd(10, '123')
})
`);
const parsed = JSON.parse(result);
expect(parsed.padStart).toBe('005');
expect(parsed.padEnd).toBe('500');
expect(parsed.padStartCustom).toBe('1231231abc');
expect(parsed.padEndCustom).toBe('abc1231231');
});
it('should inject String.prototype.at for negative indexing', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const str = 'hello';
JSON.stringify({
last: str.at(-1),
secondLast: str.at(-2),
first: str.at(0),
outOfBounds: str.at(10)
})
`);
const parsed = JSON.parse(result);
expect(parsed.last).toBe('o');
expect(parsed.secondLast).toBe('l');
expect(parsed.first).toBe('h');
expect(parsed.outOfBounds).toBeUndefined();
});
it('should inject Array.prototype.flat', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const nested = [1, [2, 3], [4, [5, 6]]];
JSON.stringify({
flat1: nested.flat(),
flat2: nested.flat(2),
flatInfinity: nested.flat(Infinity)
})
`);
const parsed = JSON.parse(result);
expect(parsed.flat1).toEqual([1, 2, 3, 4, [5, 6]]);
expect(parsed.flat2).toEqual([1, 2, 3, 4, 5, 6]);
expect(parsed.flatInfinity).toEqual([1, 2, 3, 4, 5, 6]);
});
it('should inject Array.prototype.flatMap', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const arr = [1, 2, 3];
JSON.stringify(arr.flatMap(x => [x, x * 2]))
`);
expect(JSON.parse(result)).toEqual([1, 2, 2, 4, 3, 6]);
});
it('should inject Array.prototype.at for negative indexing', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const arr = ['a', 'b', 'c', 'd'];
JSON.stringify({
last: arr.at(-1),
secondLast: arr.at(-2),
first: arr.at(0),
outOfBounds: arr.at(10)
})
`);
const parsed = JSON.parse(result);
expect(parsed.last).toBe('d');
expect(parsed.secondLast).toBe('c');
expect(parsed.first).toBe('a');
expect(parsed.outOfBounds).toBeUndefined();
});
it('should inject Array.prototype.findLast and findLastIndex', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const arr = [1, 2, 3, 4, 5, 4, 3, 2, 1];
JSON.stringify({
findLast: arr.findLast(x => x > 3),
findLastIndex: arr.findLastIndex(x => x > 3),
findLastNotFound: arr.findLast(x => x > 10),
findLastIndexNotFound: arr.findLastIndex(x => x > 10)
})
`);
const parsed = JSON.parse(result);
expect(parsed.findLast).toBe(4);
expect(parsed.findLastIndex).toBe(5);
expect(parsed.findLastNotFound).toBeUndefined();
expect(parsed.findLastIndexNotFound).toBe(-1);
});
it('should inject Object.fromEntries', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const entries = [['a', 1], ['b', 2], ['c', 3]];
JSON.stringify(Object.fromEntries(entries))
`);
expect(JSON.parse(result)).toEqual({ a: 1, b: 2, c: 3 });
});
it('should inject Object.hasOwn', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const obj = { a: 1, b: 2 };
JSON.stringify({
hasA: Object.hasOwn(obj, 'a'),
hasC: Object.hasOwn(obj, 'c'),
hasToString: Object.hasOwn(obj, 'toString')
})
`);
const parsed = JSON.parse(result);
expect(parsed.hasA).toBe(true);
expect(parsed.hasC).toBe(false);
expect(parsed.hasToString).toBe(false);
});
it('should handle complex real-world scenario with multiple polyfills', async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
await injectVMHelpersIndividually(context);
const result = await context.eval(`
const data = [
{ id: 1, tags: ['foo', 'bar'] },
{ id: 2, tags: ['baz', 'foo'] },
{ id: 3, tags: ['qux'] }
];
// Use flatMap to get all tags
const allTags = data.flatMap(item => item.tags);
// Use matchAll to find 'foo' occurrences
const fooMatches = [...allTags.join(',').matchAll(/foo/g)];
// Use findLast to get last item with 'foo'
const lastWithFoo = data.findLast(item => item.tags.includes('foo'));
// Use Object.fromEntries to create a map
const tagMap = Object.fromEntries(allTags.map((tag, i) => [tag, i]));
// Use at for negative indexing
const lastTag = allTags.at(-1);
JSON.stringify({
allTags,
fooCount: fooMatches.length,
lastWithFooId: lastWithFoo.id,
hasQux: Object.hasOwn(tagMap, 'qux'),
lastTag
})
`);
const parsed = JSON.parse(result);
expect(parsed.allTags).toEqual(['foo', 'bar', 'baz', 'foo', 'qux']);
expect(parsed.fooCount).toBe(2);
expect(parsed.lastWithFooId).toBe(2);
expect(parsed.hasQux).toBe(true);
expect(parsed.lastTag).toBe('qux');
});
});
});