Files
bruno/packages/bruno-js/src/utils.js
Pragadesh-45 6a85635c49 Fix: Inconsistent JSON parsing and formatting in res.body and Res-preview (#4103)
* Fix: Revert selective JSON parsing where string response is not parsed

- Revert "Merge pull request #3706 from Pragadesh-45/fix/response-format-updates"
  - e897dc1eb0
- Revert "Merge pull request #3676 from pooja-bruno/fix/string-json-response"
  - 1f2bee1f90

* Fix: Revert interpreting Assert RHS-value wrapped in quotes literally

- Revert "Merge pull request #3806 from Pragadesh-45/fix/handle-assert-results"
  - 63d3cb380d
- Revert "Merge pull request #3805 from Pragadesh-45/fix/handle-assert-results"
  - 6abd063749

* Fix: Inconsistent JSON formatting in preview when encoded value is a string

* Fix: Prettify JSON for Res-preview without parsing to avoid JS specific roundings

* Fix(testbench): req.body is always Buffer after the binary req body related changes

* Added `/api/echo/custom` where response can be configured using request itself

* Added tests for validating Assert and Response-preview

Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>

* Handle char-encoding in Response-preview and added more tests

* Updated API endpoint in tests to use httpfaker api

* QuickJS (Safe Mode) exec logic to handle template literals similar to Developer Mode

* Safe Mode bru.runRequest to return statusText similar to Developer Mode

---------

Co-authored-by: ramki-bruno <ramki@usebruno.com>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2025-03-13 00:49:57 +05:30

154 lines
4.3 KiB
JavaScript

const jsonQuery = require('json-query');
const { get } = require('@usebruno/query');
const JS_KEYWORDS = `
break case catch class const continue debugger default delete do
else export extends false finally for function if import in instanceof
new null return super switch this throw true try typeof var void while with
undefined let static yield arguments of
`
.split(/\s+/)
.filter((word) => word.length > 0);
/**
* Creates a function from a JavaScript expression
*
* When the function is called, the variables used in this expression are picked up from the context
*
* ```js
* res.data.pets.map(pet => pet.name.toUpperCase())
*
* function(__bruno__functionInnerContext) {
* const { res, pet } = __bruno__functionInnerContext;
* return res.data.pets.map(pet => pet.name.toUpperCase())
* }
* ```
*/
const compileJsExpression = (expr) => {
// get all dotted identifiers (foo, bar.baz, .baz)
const matches = expr.match(/([\w\.$]+)/g) ?? [];
// get valid js identifiers (foo, bar)
const vars = new Set(
matches
.filter((match) => /^[a-zA-Z$_]/.test(match)) // starts with valid js identifier (foo.bar)
.map((match) => match.split('.')[0]) // top level identifier (foo)
.filter((name) => !JS_KEYWORDS.includes(name)) // exclude js keywords
);
// globals such as Math
const globals = [...vars].filter((name) => name in globalThis);
const code = {
vars: [...vars].join(', '),
// pick global from context or globalThis
globals: globals.map((name) => ` ${name} = ${name} ?? globalThis.${name};`).join('')
};
// param name that is unlikely to show up as a var in an expression
const param = `__bruno__functionInnerContext`;
const body = `let { ${code.vars} } = ${param}; ${code.globals}; return ${expr}`;
return new Function(param, body);
};
const internalExpressionCache = new Map();
const evaluateJsExpression = (expression, context) => {
let fn = internalExpressionCache.get(expression);
if (fn == null) {
internalExpressionCache.set(expression, (fn = compileJsExpression(expression)));
}
return fn(context);
};
const evaluateJsTemplateLiteral = (templateLiteral, context) => {
if (!templateLiteral || !templateLiteral.length || typeof templateLiteral !== 'string') {
return templateLiteral;
}
templateLiteral = templateLiteral.trim();
if (templateLiteral === 'true') {
return true;
}
if (templateLiteral === 'false') {
return false;
}
if (templateLiteral === 'null') {
return null;
}
if (templateLiteral === 'undefined') {
return undefined;
}
if (templateLiteral.startsWith('"') && templateLiteral.endsWith('"')) {
return templateLiteral.slice(1, -1);
}
if (templateLiteral.startsWith("'") && templateLiteral.endsWith("'")) {
return templateLiteral.slice(1, -1);
}
if (!isNaN(templateLiteral)) {
const number = Number(templateLiteral);
// Check if the number is too high. Too high number might get altered, see #1000
if (number > Number.MAX_SAFE_INTEGER) {
return templateLiteral;
}
return number;
}
templateLiteral = '`' + templateLiteral + '`';
return evaluateJsExpression(templateLiteral, context);
};
const createResponseParser = (response = {}) => {
const res = (expr, ...fns) => {
return get(response.data, expr, ...fns);
};
res.status = response.status;
res.statusText = response.statusText;
res.headers = response.headers;
res.body = response.data;
res.responseTime = response.responseTime;
res.jq = (expr) => {
const output = jsonQuery(expr, { data: response.data });
return output ? output.value : null;
};
return res;
};
/**
* Objects that are created inside vm2 execution context result in an serialization error when sent to the renderer process
* Error sending from webFrameMain: Error: Failed to serialize arguments
* at s.send (node:electron/js2c/browser_init:169:631)
* at g.send (node:electron/js2c/browser_init:165:2156)
* How to reproduce
* Remove the cleanJson fix and execute the below post response script
* bru.setVar("a", {b:3});
* Todo: Find a better fix
*/
const cleanJson = (data) => {
try {
return JSON.parse(JSON.stringify(data));
} catch (e) {
return data;
}
};
module.exports = {
evaluateJsExpression,
evaluateJsTemplateLiteral,
createResponseParser,
internalExpressionCache,
cleanJson
};