mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
345 lines
12 KiB
JavaScript
345 lines
12 KiB
JavaScript
const { describe, it, expect } = require('@jest/globals');
|
|
const {
|
|
evaluateJsExpression,
|
|
evaluateJsTemplateLiteral,
|
|
internalExpressionCache: cache,
|
|
createResponseParser,
|
|
cleanJson,
|
|
cleanCircularJson
|
|
} = 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);
|
|
});
|
|
|
|
it('replaces circular references with [Circular Reference]', () => {
|
|
const obj = { a: 1 };
|
|
obj.self = obj;
|
|
expect(cleanJson(obj)).toEqual({ a: 1, self: '[Circular Reference]' });
|
|
});
|
|
|
|
it('serializes Error instances with all own properties', () => {
|
|
const err = new Error('oops');
|
|
const out = cleanJson(err);
|
|
expect(out).toMatchObject({ message: 'oops', name: 'Error' });
|
|
expect(typeof out.stack).toBe('string');
|
|
});
|
|
|
|
it('serializes Error with extra own properties (code, cause)', () => {
|
|
const err = new Error('failed');
|
|
err.code = 'ERR_FAILED';
|
|
err.cause = new Error('root cause');
|
|
const out = cleanJson(err);
|
|
expect(out.message).toBe('failed');
|
|
expect(out.code).toBe('ERR_FAILED');
|
|
expect(out.cause).toMatchObject({ message: 'root cause', name: 'Error' });
|
|
expect(typeof out.cause.stack).toBe('string');
|
|
});
|
|
|
|
it('serializes Error subclasses', () => {
|
|
const err = new TypeError('type oops');
|
|
const out = cleanJson(err);
|
|
expect(out).toMatchObject({ message: 'type oops', name: 'TypeError' });
|
|
expect(typeof out.stack).toBe('string');
|
|
});
|
|
|
|
it('serializes duck-typed error-like objects (message + stack strings)', () => {
|
|
const fake = { message: 'fake', stack: 'at line 1' };
|
|
const out = cleanJson(fake);
|
|
expect(out).toEqual(fake);
|
|
});
|
|
|
|
it('does not treat plain objects with non-string message/stack as errors', () => {
|
|
const notError = { message: 123, stack: 'at line 1' };
|
|
const out = cleanJson(notError);
|
|
expect(out).toEqual(notError);
|
|
const notError2 = { message: 'ok', stack: 456 };
|
|
const out2 = cleanJson(notError2);
|
|
expect(out2).toEqual(notError2);
|
|
});
|
|
|
|
it('serializes nested Error inside object', () => {
|
|
const input = { err: new Error('nested'), id: 1 };
|
|
const out = cleanJson(input);
|
|
expect(out.id).toBe(1);
|
|
expect(out.err).toMatchObject({ message: 'nested', name: 'Error' });
|
|
expect(typeof out.err.stack).toBe('string');
|
|
});
|
|
|
|
it('handles circular ref and Error in same structure', () => {
|
|
const err = new Error('cycle');
|
|
const obj = { err, ref: null };
|
|
obj.ref = obj;
|
|
const out = cleanJson(obj);
|
|
expect(out.err).toMatchObject({ message: 'cycle' });
|
|
expect(out.ref).toBe('[Circular Reference]');
|
|
});
|
|
});
|
|
|
|
describe('cleanCircularJson', () => {
|
|
it('returns primitives and plain objects as-is', () => {
|
|
expect(cleanCircularJson(1)).toBe(1);
|
|
expect(cleanCircularJson('x')).toBe('x');
|
|
expect(cleanCircularJson({ a: 1 })).toEqual({ a: 1 });
|
|
});
|
|
|
|
it('replaces circular references with [Circular Reference]', () => {
|
|
const obj = { a: 1 };
|
|
obj.self = obj;
|
|
expect(cleanCircularJson(obj)).toEqual({ a: 1, self: '[Circular Reference]' });
|
|
});
|
|
|
|
it('handles deeply nested circular ref', () => {
|
|
const obj = { level: 1, child: null };
|
|
obj.child = { level: 2, back: obj };
|
|
const out = cleanCircularJson(obj);
|
|
expect(out.level).toBe(1);
|
|
expect(out.child.level).toBe(2);
|
|
expect(out.child.back).toBe('[Circular Reference]');
|
|
});
|
|
});
|
|
|
|
describe('evaluateJsTemplateLiteral', () => {
|
|
it('returns non-string or empty input as-is', () => {
|
|
expect(evaluateJsTemplateLiteral(null)).toBe(null);
|
|
expect(evaluateJsTemplateLiteral('')).toBe('');
|
|
expect(evaluateJsTemplateLiteral(42)).toBe(42);
|
|
});
|
|
|
|
it('parses boolean and null literals', () => {
|
|
expect(evaluateJsTemplateLiteral('true')).toBe(true);
|
|
expect(evaluateJsTemplateLiteral('false')).toBe(false);
|
|
expect(evaluateJsTemplateLiteral('null')).toBe(null);
|
|
expect(evaluateJsTemplateLiteral('undefined')).toBe(undefined);
|
|
});
|
|
|
|
it('parses quoted strings', () => {
|
|
expect(evaluateJsTemplateLiteral('"hello"')).toBe('hello');
|
|
expect(evaluateJsTemplateLiteral('\'world\'')).toBe('world');
|
|
});
|
|
|
|
it('parses numbers', () => {
|
|
expect(evaluateJsTemplateLiteral('42')).toBe(42);
|
|
expect(evaluateJsTemplateLiteral('3.14')).toBe(3.14);
|
|
});
|
|
|
|
it('evaluates template literal with context', () => {
|
|
const context = { res: { data: { name: 'Bruno' } } };
|
|
expect(evaluateJsTemplateLiteral('${res.data.name}', context)).toBe('Bruno');
|
|
});
|
|
|
|
it('keeps large numbers as string (safe integer limit)', () => {
|
|
const big = '9007199254740993';
|
|
expect(evaluateJsTemplateLiteral(big)).toBe(big);
|
|
});
|
|
});
|
|
});
|