const { describe, it, expect } = require('@jest/globals'); const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser, cleanJson } = require('../src/utils'); describe('utils', () => { describe('expression evaluation', () => { const context = { res: { data: { pets: ['bruno', 'max'] }, context: 'testContext', __bruno__functionInnerContext: 0 } }; beforeEach(() => cache.clear()); afterEach(() => cache.clear()); it('should evaluate expression', () => { let result; result = evaluateJsExpression('res.data.pets', context); expect(result).toEqual(['bruno', 'max']); result = evaluateJsExpression('res.data.pets[0].toUpperCase()', context); expect(result).toEqual('BRUNO'); }); it('should cache expression', () => { expect(cache.size).toBe(0); evaluateJsExpression('res.data.pets', context); expect(cache.size).toBe(1); }); it('should use cached expression', () => { const expr = 'res.data.pets'; evaluateJsExpression(expr, context); const fn = cache.get(expr); expect(fn).toBeDefined(); evaluateJsExpression(expr, context); // cache should not be overwritten expect(cache.get(expr)).toBe(fn); }); it('should identify top level variables', () => { const expr = 'res.data.pets[0].toUpperCase()'; evaluateJsExpression(expr, context); expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;'); }); it('should not duplicate variables', () => { const expr = 'res.data.pets[0] + res.data.pets[1]'; evaluateJsExpression(expr, context); expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;'); }); it('should exclude js keywords like true false from vars', () => { const expr = 'res.data.pets.length > 0 ? true : false'; evaluateJsExpression(expr, context); expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;'); }); it('should exclude numbers from vars', () => { const expr = 'res.data.pets.length + 10'; evaluateJsExpression(expr, context); expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;'); }); it('should pick variables from complex expressions', () => { const expr = 'res.data.pets.map(pet => pet.length)'; const result = evaluateJsExpression(expr, context); expect(result).toEqual([5, 3]); expect(cache.get(expr).toString()).toContain('let { res, pet } = __bruno__functionInnerContext;'); }); it('should be ok picking extra vars from strings', () => { const expr = '\'hello\' + \' \' + res.data.pets[0]'; const result = evaluateJsExpression(expr, context); expect(result).toBe('hello bruno'); // extra var hello is harmless expect(cache.get(expr).toString()).toContain('let { hello, res } = __bruno__functionInnerContext;'); }); it('should evaluate expressions referencing globals', () => { const startTime = new Date('2022-02-01').getTime(); const currentTime = new Date('2022-02-02').getTime(); jest.useFakeTimers({ now: currentTime }); const expr = 'Math.max(Date.now(), startTime)'; const result = evaluateJsExpression(expr, { startTime }); expect(result).toBe(currentTime); expect(cache.get(expr).toString()).toContain('Math = Math ?? globalThis.Math;'); expect(cache.get(expr).toString()).toContain('Date = Date ?? globalThis.Date;'); }); it('should use global overridden in context', () => { const startTime = new Date('2022-02-01').getTime(); const currentTime = new Date('2022-02-02').getTime(); jest.useFakeTimers({ now: currentTime }); const context = { Date: { now: () => new Date('2022-01-31').getTime() }, startTime }; const expr = 'Math.max(Date.now(), startTime)'; const result = evaluateJsExpression(expr, context); expect(result).toBe(startTime); }); it('should allow "context" as a var name', () => { const expr = 'res["context"].toUpperCase()'; evaluateJsExpression(expr, context); expect(cache.get(expr).toString()).toContain('let { res, context } = __bruno__functionInnerContext;'); }); it('should throw an error when we use "__bruno__functionInnerContext" as a var name', () => { const expr = 'res["__bruno__functionInnerContext"].toUpperCase()'; expect(() => evaluateJsExpression(expr, context)).toThrow(SyntaxError); expect(() => evaluateJsExpression(expr, context)).toThrow( 'Identifier \'__bruno__functionInnerContext\' has already been declared' ); }); }); describe('response parser', () => { const res = createResponseParser({ status: 200, data: { order: { items: [ { id: 1, amount: 10 }, { id: 2, amount: 20 } ] } } }); it('should default to bruno query', () => { const value = res('..items[?].amount[0]', (i) => i.amount > 10); expect(value).toBe(20); }); it('should allow json-query', () => { const value = res.jq('order.items[amount > 10].amount'); expect(value).toBe(20); }); }); describe('cleanJson', () => { it('primitives should be kept as is', () => { const input = { number: 1, string: 'hello world', booleanFalse: false, booleanTrue: true, float: 2.1, floatDeep: 2.2222222 }; expect(cleanJson(input)).toEqual(input); }); it('functions are lost', () => { const func = function (x, y) { return x + y; }; const input = { func, number: 1 }; expect(cleanJson(input)).toEqual({ number: 1 }); }); it('dates are serialized', () => { const date = new Date(); const str = date.toISOString(); const input = { date }; expect(cleanJson(input)).toEqual({ date: str }); }); it('typed arrays should be kept as is', () => { const input = { Int8Array: Int8Array.from(Buffer.from('hello world').toString()), Uint8Array: Uint8Array.from(Buffer.from('hello world').toString()), Uint8ClampedArray: Uint8ClampedArray.from(Buffer.from('hello world').toString()), Int16Array: Int16Array.from(Buffer.from('hello world').toString()), Uint16Array: Uint16Array.from(Buffer.from('hello world').toString()), Int32Array: Int32Array.from(Buffer.from('hello world').toString()), Uint32Array: Uint32Array.from(Buffer.from('hello world').toString()), Float32Array: Float32Array.from(Buffer.from('hello world').toString()), Float64Array: Float64Array.from(Buffer.from('hello world').toString()), BigInt64Array: BigInt64Array.from(Buffer.from('123').toString()), BigUint64Array: BigUint64Array.from(Buffer.from('234').toString()) }; expect(cleanJson(input)).toEqual(input); }); }); });