mirror of
https://github.com/javascript-obfuscator/javascript-obfuscator
synced 2026-08-09 12:42:29 +00:00
464 lines
18 KiB
TypeScript
464 lines
18 KiB
TypeScript
import { assert } from 'chai';
|
|
import * as sinon from 'sinon';
|
|
|
|
import { ProApiClient } from '../../../src/pro-api/ProApiClient';
|
|
import { ApiError } from '../../../src/pro-api/ApiError';
|
|
import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade';
|
|
|
|
describe('ProApiClient', () => {
|
|
let fetchStub: sinon.SinonStub;
|
|
|
|
// Helper to create NDJSON streaming response
|
|
const createNdjsonResponse = (messages: object[]): string => {
|
|
return messages.map((msg) => JSON.stringify(msg)).join('\n');
|
|
};
|
|
|
|
// Mock fetch to redirect to our test server
|
|
const mockFetch = (responseBody: string, statusCode: number = 200): void => {
|
|
fetchStub = sinon.stub(global, 'fetch').callsFake(async () => {
|
|
return {
|
|
ok: statusCode >= 200 && statusCode < 300,
|
|
status: statusCode,
|
|
text: async () => responseBody
|
|
} as Response;
|
|
});
|
|
};
|
|
|
|
afterEach(() => {
|
|
if (fetchStub) {
|
|
fetchStub.restore();
|
|
}
|
|
});
|
|
|
|
describe('obfuscate', () => {
|
|
describe('validation', () => {
|
|
it('should throw ApiError when vmObfuscation is not enabled', async () => {
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
|
|
try {
|
|
await client.obfuscate('const a = 1;', { vmObfuscation: false });
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.instanceOf(error, ApiError);
|
|
assert.include((error as ApiError).message, 'vmObfuscation');
|
|
assert.equal((error as ApiError).statusCode, 400);
|
|
}
|
|
});
|
|
|
|
it('should throw ApiError when vmObfuscation is undefined', async () => {
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
|
|
try {
|
|
await client.obfuscate('const a = 1;', {});
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.instanceOf(error, ApiError);
|
|
assert.include((error as ApiError).message, 'vmObfuscation');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('streaming response - direct result', () => {
|
|
it('should handle direct result response', async () => {
|
|
const obfuscatedCode = 'var _0x1234 = function() { return 1; };';
|
|
const sourceMap = '{"version":3}';
|
|
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'progress', message: 'Starting obfuscation...' },
|
|
{ type: 'progress', message: 'Processing...' },
|
|
{ type: 'result', code: obfuscatedCode, sourceMap: sourceMap }
|
|
]);
|
|
|
|
mockFetch(responseBody);
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
const result = await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
|
|
assert.equal(result.getObfuscatedCode(), obfuscatedCode);
|
|
assert.equal(result.getSourceMap(), sourceMap);
|
|
});
|
|
|
|
it('should call progress callback for progress messages', async () => {
|
|
const progressMessages: string[] = [];
|
|
const obfuscatedCode = 'var _0x1234 = 1;';
|
|
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'progress', message: 'Step 1: Parsing' },
|
|
{ type: 'progress', message: 'Step 2: Transforming' },
|
|
{ type: 'progress', message: 'Step 3: Generating' },
|
|
{ type: 'result', code: obfuscatedCode, sourceMap: '' }
|
|
]);
|
|
|
|
mockFetch(responseBody);
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
await client.obfuscate('const a = 1;', { vmObfuscation: true }, (msg) => {
|
|
progressMessages.push(msg);
|
|
});
|
|
|
|
assert.deepEqual(progressMessages, [
|
|
'Step 1: Parsing',
|
|
'Step 2: Transforming',
|
|
'Step 3: Generating'
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('streaming response - chunked result', () => {
|
|
it('should handle chunked code response', async () => {
|
|
const chunk1 = 'var _0x1234 = ';
|
|
const chunk2 = 'function() { ';
|
|
const chunk3 = 'return 1; };';
|
|
const expectedCode = chunk1 + chunk2 + chunk3;
|
|
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'progress', message: 'Processing...' },
|
|
{ type: 'chunk', field: 'code', data: chunk1, index: 0, total: 3 },
|
|
{ type: 'chunk', field: 'code', data: chunk2, index: 1, total: 3 },
|
|
{ type: 'chunk', field: 'code', data: chunk3, index: 2, total: 3 },
|
|
{ type: 'chunk_end', sourceMap: '' }
|
|
]);
|
|
|
|
mockFetch(responseBody);
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
const result = await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
|
|
assert.equal(result.getObfuscatedCode(), expectedCode);
|
|
});
|
|
|
|
it('should handle chunked code and sourceMap response', async () => {
|
|
const codeChunk1 = 'var a = 1;';
|
|
const codeChunk2 = 'var b = 2;';
|
|
const mapChunk1 = '{"version":';
|
|
const mapChunk2 = '3}';
|
|
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'chunk', field: 'code', data: codeChunk1, index: 0, total: 2 },
|
|
{ type: 'chunk', field: 'code', data: codeChunk2, index: 1, total: 2 },
|
|
{ type: 'chunk', field: 'sourceMap', data: mapChunk1, index: 0, total: 2 },
|
|
{ type: 'chunk', field: 'sourceMap', data: mapChunk2, index: 1, total: 2 },
|
|
{ type: 'chunk_end' }
|
|
]);
|
|
|
|
mockFetch(responseBody);
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
const result = await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
|
|
assert.equal(result.getObfuscatedCode(), codeChunk1 + codeChunk2);
|
|
assert.equal(result.getSourceMap(), mapChunk1 + mapChunk2);
|
|
});
|
|
|
|
it('should handle out-of-order chunks correctly', async () => {
|
|
const chunk0 = 'first';
|
|
const chunk1 = 'second';
|
|
const chunk2 = 'third';
|
|
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'chunk', field: 'code', data: chunk2, index: 2, total: 3 },
|
|
{ type: 'chunk', field: 'code', data: chunk0, index: 0, total: 3 },
|
|
{ type: 'chunk', field: 'code', data: chunk1, index: 1, total: 3 },
|
|
{ type: 'chunk_end', sourceMap: '' }
|
|
]);
|
|
|
|
mockFetch(responseBody);
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
const result = await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
|
|
assert.equal(result.getObfuscatedCode(), chunk0 + chunk1 + chunk2);
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should throw ApiError when API returns error message', async () => {
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'progress', message: 'Starting...' },
|
|
{ type: 'error', message: 'Invalid API token' }
|
|
]);
|
|
|
|
mockFetch(responseBody, 401);
|
|
|
|
const client = new ProApiClient({ apiToken: 'invalid-token' });
|
|
|
|
try {
|
|
await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.instanceOf(error, ApiError);
|
|
assert.equal((error as ApiError).message, 'Invalid API token');
|
|
}
|
|
});
|
|
|
|
it('should throw ApiError when no result is received', async () => {
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'progress', message: 'Processing...' }
|
|
]);
|
|
|
|
mockFetch(responseBody);
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
|
|
try {
|
|
await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.instanceOf(error, ApiError);
|
|
assert.include((error as ApiError).message, 'No result received');
|
|
}
|
|
});
|
|
|
|
it('should skip invalid JSON lines in response', async () => {
|
|
const obfuscatedCode = 'var a = 1;';
|
|
const responseBody =
|
|
'{"type":"progress","message":"Step 1"}\n' +
|
|
'invalid json line\n' +
|
|
`{"type":"result","code":"${obfuscatedCode}","sourceMap":""}`;
|
|
|
|
mockFetch(responseBody);
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
const result = await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
|
|
assert.equal(result.getObfuscatedCode(), obfuscatedCode);
|
|
});
|
|
|
|
it('should handle timeout', async () => {
|
|
fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => {
|
|
// Simulate abort being called
|
|
const signal = options?.signal as AbortSignal;
|
|
if (signal) {
|
|
const error = new Error('The operation was aborted');
|
|
error.name = 'AbortError';
|
|
throw error;
|
|
}
|
|
return { ok: true, text: async () => '' } as Response;
|
|
});
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token', timeout: 1 });
|
|
|
|
try {
|
|
await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.instanceOf(error, ApiError);
|
|
assert.equal((error as ApiError).statusCode, 408);
|
|
assert.include((error as ApiError).message, 'timeout');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('result interface', () => {
|
|
it('should return result implementing IProObfuscationResult', async () => {
|
|
const obfuscatedCode = 'var _0x1234 = 1;';
|
|
const sourceMap = '{"version":3}';
|
|
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'result', code: obfuscatedCode, sourceMap: sourceMap }
|
|
]);
|
|
|
|
mockFetch(responseBody);
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
const result = await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
|
|
assert.equal(result.getObfuscatedCode(), obfuscatedCode);
|
|
assert.equal(result.getSourceMap(), sourceMap);
|
|
assert.isNull(result.getIdentifierNamesCache());
|
|
assert.equal(result.toString(), obfuscatedCode);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('JavaScriptObfuscatorFacade Pro API', () => {
|
|
let fetchStub: sinon.SinonStub;
|
|
|
|
const createNdjsonResponse = (messages: object[]): string => {
|
|
return messages.map((msg) => JSON.stringify(msg)).join('\n');
|
|
};
|
|
|
|
const mockFetch = (responseBody: string, statusCode: number = 200): void => {
|
|
fetchStub = sinon.stub(global, 'fetch').callsFake(async () => {
|
|
return {
|
|
ok: statusCode >= 200 && statusCode < 300,
|
|
status: statusCode,
|
|
text: async () => responseBody
|
|
} as Response;
|
|
});
|
|
};
|
|
|
|
afterEach(() => {
|
|
if (fetchStub) {
|
|
fetchStub.restore();
|
|
}
|
|
});
|
|
|
|
describe('obfuscatePro', () => {
|
|
it('should throw ApiError when vmObfuscation is not enabled', async () => {
|
|
try {
|
|
await JavaScriptObfuscator.obfuscatePro('const a = 1;', {}, { apiToken: 'test' });
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.instanceOf(error, ApiError);
|
|
assert.include((error as ApiError).message, 'vmObfuscation');
|
|
}
|
|
});
|
|
|
|
it('should return obfuscation result on success', async () => {
|
|
const obfuscatedCode = 'var _0x1234 = 1;';
|
|
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'result', code: obfuscatedCode, sourceMap: '' }
|
|
]);
|
|
|
|
mockFetch(responseBody);
|
|
|
|
const result = await JavaScriptObfuscator.obfuscatePro(
|
|
'const a = 1;',
|
|
{ vmObfuscation: true },
|
|
{ apiToken: 'test-token' }
|
|
);
|
|
|
|
assert.equal(result.getObfuscatedCode(), obfuscatedCode);
|
|
});
|
|
|
|
it('should forward progress callback', async () => {
|
|
const progressMessages: string[] = [];
|
|
const obfuscatedCode = 'var a = 1;';
|
|
|
|
const responseBody = createNdjsonResponse([
|
|
{ type: 'progress', message: 'Processing...' },
|
|
{ type: 'result', code: obfuscatedCode, sourceMap: '' }
|
|
]);
|
|
|
|
mockFetch(responseBody);
|
|
|
|
await JavaScriptObfuscator.obfuscatePro(
|
|
'const a = 1;',
|
|
{ vmObfuscation: true },
|
|
{ apiToken: 'test-token' },
|
|
(msg) => progressMessages.push(msg)
|
|
);
|
|
|
|
assert.deepEqual(progressMessages, ['Processing...']);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('ApiError', () => {
|
|
it('should have correct properties', () => {
|
|
const error = new ApiError('Test error', 500, '{"error": "details"}');
|
|
|
|
assert.equal(error.message, 'Test error');
|
|
assert.equal(error.statusCode, 500);
|
|
assert.equal(error.response, '{"error": "details"}');
|
|
assert.equal(error.name, 'ApiError');
|
|
assert.instanceOf(error, Error);
|
|
});
|
|
|
|
it('should work without response parameter', () => {
|
|
const error = new ApiError('Test error', 400);
|
|
|
|
assert.equal(error.message, 'Test error');
|
|
assert.equal(error.statusCode, 400);
|
|
assert.isUndefined(error.response);
|
|
});
|
|
});
|
|
|
|
describe('ProApiClient request format', () => {
|
|
let fetchStub: sinon.SinonStub;
|
|
|
|
afterEach(() => {
|
|
if (fetchStub) {
|
|
fetchStub.restore();
|
|
}
|
|
});
|
|
|
|
it('should send correct headers', async () => {
|
|
let capturedHeaders: HeadersInit | undefined;
|
|
|
|
fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => {
|
|
capturedHeaders = options?.headers;
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
text: async () => JSON.stringify({ type: 'result', code: 'var a;', sourceMap: '' })
|
|
} as Response;
|
|
});
|
|
|
|
const client = new ProApiClient({ apiToken: 'my-api-token' });
|
|
await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
|
|
const headers = capturedHeaders as Record<string, string>;
|
|
assert.equal(headers['Content-Type'], 'application/json');
|
|
assert.equal(headers['Accept'], 'application/x-ndjson');
|
|
assert.equal(headers['Authorization'], 'Bearer my-api-token');
|
|
});
|
|
|
|
it('should send correct request body', async () => {
|
|
let capturedBody: string | undefined;
|
|
|
|
fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => {
|
|
capturedBody = options?.body as string;
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
text: async () => JSON.stringify({ type: 'result', code: 'var a;', sourceMap: '' })
|
|
} as Response;
|
|
});
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
await client.obfuscate('const a = 1;', {
|
|
vmObfuscation: true,
|
|
compact: true,
|
|
stringArray: false
|
|
});
|
|
|
|
const body = JSON.parse(capturedBody!);
|
|
assert.equal(body.code, 'const a = 1;');
|
|
assert.deepEqual(body.options, {
|
|
vmObfuscation: true,
|
|
compact: true,
|
|
stringArray: false
|
|
});
|
|
});
|
|
|
|
it('should use POST method', async () => {
|
|
let capturedMethod: string | undefined;
|
|
|
|
fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => {
|
|
capturedMethod = options?.method;
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
text: async () => JSON.stringify({ type: 'result', code: 'var a;', sourceMap: '' })
|
|
} as Response;
|
|
});
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token' });
|
|
await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
|
|
assert.equal(capturedMethod, 'POST');
|
|
});
|
|
|
|
it('should use custom timeout', async () => {
|
|
let signalReceived = false;
|
|
|
|
fetchStub = sinon.stub(global, 'fetch').callsFake(async (url, options) => {
|
|
signalReceived = options?.signal !== undefined;
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
text: async () => JSON.stringify({ type: 'result', code: 'var a;', sourceMap: '' })
|
|
} as Response;
|
|
});
|
|
|
|
const client = new ProApiClient({ apiToken: 'test-token', timeout: 60000 });
|
|
await client.obfuscate('const a = 1;', { vmObfuscation: true });
|
|
|
|
assert.isTrue(signalReceived);
|
|
});
|
|
});
|