Files

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);
});
});