UNPKG

altair-graphql-core

Version:

Several of the core logic for altair graphql client

449 lines 22.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const evaluator_worker_engine_1 = require("./evaluator-worker-engine"); const events_1 = require("./events"); const types_1 = require("./types"); // --------------------------------------------------------------------------- // Mock worker // --------------------------------------------------------------------------- class MockWorker extends types_1.ScriptEvaluatorWorker { constructor() { super(...arguments); this._messageHandlers = []; this._errorHandlers = []; /** Every call to send() is recorded here */ this.sentMessages = []; /** Override the auto-reply value for a specific message type */ this.responseMap = {}; /** Message types that will NOT receive an automatic _response reply */ this.blockedTypes = new Set(); } onMessage(handler) { this._messageHandlers.push(handler); } send(type, payload) { this.sentMessages.push({ type, payload }); // Automatically reply with a _response message unless the type is blocked if (!type.endsWith('_response') && !type.endsWith('_error') && !this.blockedTypes.has(type)) { const id = payload.id; const response = Object.prototype.hasOwnProperty.call(this.responseMap, type) ? this.responseMap[type] : undefined; this._dispatch(`${type}_response`, { id, response }); } } onError(handler) { this._errorHandlers.push(handler); } /** Deliver a message to all registered onMessage handlers and flush async work */ async simulateMessage(type, payload) { this._dispatch(type, payload); await new Promise((r) => setTimeout(r, 0)); } /** Invoke all registered onError handlers */ simulateError(err) { this._errorHandlers.forEach((h) => h(err)); } getSentMessage(type) { return this.sentMessages.find((m) => m.type === type); } getAllSentMessages(type) { return this.sentMessages.filter((m) => m.type === type); } _dispatch(type, payload) { [...this._messageHandlers].forEach((h) => h({ type, payload })); } } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const createData = (overrides = {}) => ({ headers: [], variables: '{}', operationName: 'TestOp', query: 'query { test }', url: 'https://example.com/graphql', environment: {}, requestScriptLogs: [], ...overrides, }); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- (0, vitest_1.describe)('ScriptEvaluatorWorkerEngine', () => { let worker; let engine; (0, vitest_1.beforeEach)(() => { worker = new MockWorker(); engine = new evaluator_worker_engine_1.ScriptEvaluatorWorkerEngine(worker); }); (0, vitest_1.describe)('start()', () => { (0, vitest_1.it)('registers exactly one onMessage handler on the worker', () => { const spy = vitest_1.vi.spyOn(worker, 'onMessage'); engine.start(); (0, vitest_1.expect)(spy).toHaveBeenCalledOnce(); }); (0, vitest_1.it)('can be called multiple times without throwing', () => { (0, vitest_1.expect)(() => { engine.start(); engine.start(); }).not.toThrow(); }); }); (0, vitest_1.describe)('message routing', () => { (0, vitest_1.beforeEach)(() => engine.start()); (0, vitest_1.it)('does nothing for unknown message types', async () => { await worker.simulateMessage('unknown_event', { some: 'data' }); (0, vitest_1.expect)(worker.getSentMessage('executeComplete')).toBeUndefined(); (0, vitest_1.expect)(worker.getSentMessage('scriptError')).toBeUndefined(); }); (0, vitest_1.it)('triggers script execution for SCRIPT_INIT_EXECUTE messages', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, ['', createData()]); (0, vitest_1.expect)(worker.getSentMessage('executeComplete')).toBeDefined(); }); }); (0, vitest_1.describe)('successful script execution', () => { (0, vitest_1.beforeEach)(() => engine.start()); (0, vitest_1.it)('sends executeComplete after running an empty script', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, ['', createData()]); (0, vitest_1.expect)(worker.getSentMessage('executeComplete')).toBeDefined(); }); (0, vitest_1.it)('sends executeComplete with a non-empty id', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, ['', createData()]); const { id } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(typeof id).toBe('string'); (0, vitest_1.expect)(id.length).toBeGreaterThan(0); }); (0, vitest_1.it)('reflects mutations the script makes to altair.data in executeComplete', async () => { const data = createData({ query: 'original', operationName: 'GetUsers' }); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.data.query = "updated query";', data, ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(args[0].query).toBe('updated query'); // unmutated properties are preserved (0, vitest_1.expect)(args[0].operationName).toBe('GetUsers'); // original data is not mutated (0, vitest_1.expect)(data.query).toBe('original'); }); }); (0, vitest_1.describe)('script error handling', () => { (0, vitest_1.beforeEach)(() => engine.start()); (0, vitest_1.it)('sends scriptError when the script throws an Error', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'throw new Error("oops");', createData(), ]); (0, vitest_1.expect)(worker.getSentMessage('scriptError')).toBeDefined(); }); (0, vitest_1.it)('includes the thrown error instance in the scriptError args', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'throw new Error("test error message");', createData(), ]); const { args } = worker.getSentMessage('scriptError') .payload; (0, vitest_1.expect)(args[0]).toBeInstanceOf(Error); (0, vitest_1.expect)(args[0].message).toBe('test error message'); }); (0, vitest_1.it)('does not send executeComplete when the script throws', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'throw new Error("fail");', createData(), ]); (0, vitest_1.expect)(worker.getSentMessage('executeComplete')).toBeUndefined(); }); }); (0, vitest_1.describe)('active environment update', () => { (0, vitest_1.beforeEach)(() => engine.start()); (0, vitest_1.it)('passes all environment updated with activeEnvironment=true to updateActiveEnvironment', async () => { const data = createData({ environment: {} }); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ ` altair.helpers.setEnvironment("token", "abc123", true); altair.helpers.setEnvironment("a", 1); altair.helpers.setEnvironment("b", 2, true); `, data, ]); const { args } = worker.getSentMessage('updateActiveEnvironment').payload; (0, vitest_1.expect)(args[0]).toEqual({ token: 'abc123', b: 2 }); const executeCompleteArgs = worker.getSentMessage('executeComplete') .payload; // The environment in executeComplete should also reflect the updates (0, vitest_1.expect)(executeCompleteArgs.args[0].environment).toEqual({ token: 'abc123', a: 1, b: 2, }); }); (0, vitest_1.it)('does not send updateActiveEnvironment when setEnvironment is called without the flag', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.helpers.setEnvironment("key", "value");', createData(), ]); (0, vitest_1.expect)(worker.getSentMessage('updateActiveEnvironment')).toBeUndefined(); }); (0, vitest_1.it)('sends updateActiveEnvironment before executeComplete', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.helpers.setEnvironment("x", "y", true);', createData({ environment: {} }), ]); const order = worker.sentMessages .filter((m) => m.type === 'updateActiveEnvironment' || m.type === 'executeComplete') .map((m) => m.type); (0, vitest_1.expect)(order.indexOf('updateActiveEnvironment')).toBeLessThan(order.indexOf('executeComplete')); }); }); (0, vitest_1.describe)('worker event handler forwarding (makeCall)', () => { (0, vitest_1.beforeEach)(() => engine.start()); (0, vitest_1.it)('sends a setCookie message when the script calls helpers.setCookie', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.helpers.setCookie("sid", "val", { path: "/", secure: true });', createData(), ]); const { args } = worker.getSentMessage('setCookie').payload; (0, vitest_1.expect)(args[2]).toEqual({ path: '/', secure: true }); }); (0, vitest_1.it)('sends a request message when the script calls await helpers.request', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'await altair.helpers.request("GET", "https://api.example.com", null);', createData(), ]); const msg = worker.getSentMessage('request'); (0, vitest_1.expect)(msg).toBeDefined(); const { args } = msg.payload; (0, vitest_1.expect)(args[0]).toBe('GET'); (0, vitest_1.expect)(args[1]).toBe('https://api.example.com'); (0, vitest_1.expect)(args[2]).toBeNull(); }); (0, vitest_1.it)('sends a getStorageItem message when the script calls await altair.storage.get', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'await altair.storage.get("myKey");', createData(), ]); const msg = worker.getSentMessage('getStorageItem'); (0, vitest_1.expect)(msg).toBeDefined(); const { args } = msg.payload; (0, vitest_1.expect)(args[0]).toBe('myKey'); }); (0, vitest_1.it)('sends a setStorageItem message when the script calls await altair.storage.set', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'await altair.storage.set("myKey", { value: 42 });', createData(), ]); const msg = worker.getSentMessage('setStorageItem'); (0, vitest_1.expect)(msg).toBeDefined(); const { args } = msg.payload; (0, vitest_1.expect)(args[0]).toBe('myKey'); (0, vitest_1.expect)(args[1]).toEqual({ value: 42 }); }); (0, vitest_1.it)('sends an alert message when the script calls alert()', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'alert("hello world");', createData(), ]); const msg = worker.getSentMessage('alert'); (0, vitest_1.expect)(msg).toBeDefined(); const { args } = msg.payload; (0, vitest_1.expect)(args[0]).toBe('hello world'); }); (0, vitest_1.it)('includes a unique id in every makeCall payload', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'await altair.storage.get("k1"); await altair.storage.get("k2");', createData(), ]); const msgs = worker.getAllSentMessages('getStorageItem'); (0, vitest_1.expect)(msgs).toHaveLength(2); const ids = msgs.map((m) => m.payload.id); (0, vitest_1.expect)(ids[0]).not.toBe(ids[1]); }); }); (0, vitest_1.describe)('handler response propagation', () => { (0, vitest_1.beforeEach)(() => engine.start()); (0, vitest_1.it)('makes the request handler response available in the script', async () => { worker.responseMap['request'] = { status: 200, body: '{"ok":true}' }; await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ ` const res = await altair.helpers.request('GET', 'https://api.example.com', null); altair.data.variables = JSON.stringify(res); `, createData(), ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(JSON.parse(args[0].variables)).toEqual({ status: 200, body: '{"ok":true}', }); }); (0, vitest_1.it)('makes the getStorageItem handler response available in the script', async () => { worker.responseMap['getStorageItem'] = 'stored-value'; await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ ` const val = await altair.storage.get('someKey'); altair.data.variables = JSON.stringify(val); `, createData(), ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(JSON.parse(args[0].variables)).toBe('stored-value'); }); }); (0, vitest_1.describe)('script context features', () => { (0, vitest_1.beforeEach)(() => engine.start()); (0, vitest_1.it)('exposes environment variables via altair.helpers.getEnvironment', async () => { const data = createData({ environment: { apiKey: 'secret-123' } }); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.data.query = altair.helpers.getEnvironment("apiKey");', data, ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(args[0].query).toBe('secret-123'); }); (0, vitest_1.it)('exposes nested environment variables using dot notation', async () => { const data = createData({ environment: { user: { name: 'Alice' } } }); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.data.query = altair.helpers.getEnvironment("user.name");', data, ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(args[0].query).toBe('Alice'); }); (0, vitest_1.it)('exposes cookie values via altair.helpers.getCookie', async () => { const data = createData({ __cookieJar: { sessionId: 'abc-xyz' } }); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.data.query = altair.helpers.getCookie("sessionId");', data, ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(args[0].query).toBe('abc-xyz'); }); (0, vitest_1.it)('appends a log entry to requestScriptLogs when altair.log is called', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.log("hello from script");', createData(), ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(args[0].requestScriptLogs).toHaveLength(1); (0, vitest_1.expect)(args[0].requestScriptLogs?.[0]?.text).toBe('"hello from script"'); }); (0, vitest_1.it)('serializes complex log values as JSON', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.log({ count: 3, items: ["a", "b"] });', createData(), ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(JSON.parse(args[0].requestScriptLogs[0].text)).toEqual({ count: 3, items: ['a', 'b'], }); }); (0, vitest_1.it)('exposes altair.response when response data is present in the context', async () => { const data = createData({ requestType: types_1.RequestType.QUERY, response: { ok: true, body: '{"data":{"users":[]}}', headers: { 'content-type': 'application/json' }, status: 200, statusText: 'OK', url: 'https://example.com/graphql', requestStartTime: 0, requestEndTime: 100, responseTime: 100, }, }); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.data.query = altair.response ? "has-response" : "no-response";', data, ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(args[0].query).toBe('has-response'); }); (0, vitest_1.it)('exposes the correct status code via altair.response.statusCode', async () => { const data = createData({ requestType: types_1.RequestType.QUERY, response: { ok: true, body: '', headers: {}, status: 201, statusText: 'Created', url: 'https://example.com/graphql', requestStartTime: 0, requestEndTime: 50, responseTime: 50, }, }); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.data.query = String(altair.response.statusCode);', data, ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(args[0].query).toBe('201'); }); (0, vitest_1.it)('altair.response is undefined when no response data is provided', async () => { await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'altair.data.query = altair.response ? "has-response" : "no-response";', createData(), ]); const { args } = worker.getSentMessage('executeComplete') .payload; (0, vitest_1.expect)(args[0].query).toBe('no-response'); }); }); (0, vitest_1.describe)('worker error handling', () => { (0, vitest_1.beforeEach)(() => engine.start()); (0, vitest_1.it)('sends scriptError when the worker emits an error during execution', async () => { // Block request auto-reply so the script suspends at the await worker.blockedTypes.add('request'); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'await altair.helpers.request("GET", "https://api.example.com", null);', createData(), ]); // The error handler inside initExecute is registered synchronously // before the first await, so we can fire it immediately after the flush worker.simulateError(new Error('worker crashed')); await new Promise((r) => setTimeout(r, 0)); (0, vitest_1.expect)(worker.getSentMessage('scriptError')).toBeDefined(); }); (0, vitest_1.it)('includes the worker error in the scriptError args', async () => { worker.blockedTypes.add('request'); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'await altair.helpers.request("GET", "https://api.example.com", null);', createData(), ]); worker.simulateError(new Error('connection lost')); await new Promise((r) => setTimeout(r, 0)); const { args } = worker.getSentMessage('scriptError') .payload; (0, vitest_1.expect)(args[0].message).toBe('connection lost'); }); (0, vitest_1.it)('does not send executeComplete when a worker error occurs', async () => { worker.blockedTypes.add('request'); await worker.simulateMessage(events_1.SCRIPT_INIT_EXECUTE, [ 'await altair.helpers.request("GET", "https://api.example.com", null);', createData(), ]); worker.simulateError(new Error('crashed')); await new Promise((r) => setTimeout(r, 0)); (0, vitest_1.expect)(worker.getSentMessage('executeComplete')).toBeUndefined(); }); }); }); //# sourceMappingURL=evaluator-worker-engine.spec.js.map