mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 22:45:25 +00:00
feat: add custom jsonBody Chai assertion + simplify Postman translation (#7299)
* feat: enhance jsonBody translation handling in Postman to Bruno converter * feat: implement jsonBody assertion for Postman compatibility and enhance translation handling - Added custom Chai assertion for jsonBody to validate JSON structures, including deep equality and nested properties. - Updated Postman to Bruno translation logic to utilize the new jsonBody assertion, improving the handling of response validations. - Enhanced test coverage for jsonBody translations, including positive and negative cases for nested properties and deep equality checks. * feat: enhance jsonBody assertion translations for Postman compatibility - Added translations for `pm.response.not.to.have.jsonBody` and `pm.response.to.have.not.jsonBody` to the Postman to Bruno converter. - Updated tests to cover new translation cases, ensuring proper handling of negation scenarios for JSON body assertions. - Enhanced existing jsonBody assertion logic to support new translation patterns, improving overall compatibility with Postman syntax. * feat: add advanced path parsing for jsonBody assertions - Introduced a new `parsePath` function to handle various property path formats, including dot notation, numeric brackets, and quoted keys. - Updated the `getNestedValue` function to utilize the new path parsing logic, enhancing the robustness of jsonBody assertions. - Expanded test cases to cover a wide range of scenarios, including edge cases for bracket notation and keys with special characters. * docs: add examples for parsePath function in jsonBody assertions - Enhanced documentation for the `parsePath` function by including examples of various property path formats. - Updated comments in both `assert-runtime.js` and `test.js` to clarify the handling of dot notation, numeric brackets, and quoted keys. * fix: improve path handling in assertions for quoted keys - Updated condition checks in `assert-runtime.js` and `test.js` to ensure proper handling of quoted keys in path parsing. - Enhanced robustness of the path parsing logic to prevent potential out-of-bounds errors.
This commit is contained in:
@@ -65,6 +65,118 @@ chai.use(function (chai, utils) {
|
||||
});
|
||||
});
|
||||
|
||||
// Custom assertion for jsonBody (Postman parity)
|
||||
chai.use(function (chai, utils) {
|
||||
// Parse a property path into an array of keys.
|
||||
// Handles: dot notation (a.b), numeric brackets (a[0]), quoted brackets (a["b.c"], a['key']),
|
||||
// and combinations like data[0]["a.b"].name
|
||||
//
|
||||
// Examples:
|
||||
// "a.b.c" -> ["a", "b", "c"]
|
||||
// "items[0].name" -> ["items", "0", "name"]
|
||||
// 'data["a.b"]' -> ["data", "a.b"]
|
||||
// "matrix[0][1]" -> ["matrix", "0", "1"]
|
||||
// 'nested["x.y"].z' -> ["nested", "x.y", "z"]
|
||||
// '["say \\"hi\\""]' -> ["say \"hi\""]
|
||||
function parsePath(path) {
|
||||
const keys = [];
|
||||
let i = 0;
|
||||
while (i < path.length) {
|
||||
if (path[i] === '.') {
|
||||
// Skip dot separator
|
||||
i++;
|
||||
} else if (path[i] === '[') {
|
||||
i++; // skip '['
|
||||
if (i < path.length && (path[i] === '\'' || path[i] === '"')) {
|
||||
// Quoted key — collect until matching unescaped quote + ']'
|
||||
const quote = path[i];
|
||||
i++; // skip opening quote
|
||||
let key = '';
|
||||
while (i < path.length && path[i] !== quote) {
|
||||
if (path[i] === '\\' && i + 1 < path.length && path[i + 1] === quote) {
|
||||
key += quote;
|
||||
i += 2; // skip backslash + escaped quote
|
||||
} else {
|
||||
key += path[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
i++; // skip closing quote
|
||||
i++; // skip ']'
|
||||
keys.push(key);
|
||||
} else {
|
||||
// Unquoted (numeric) key — collect until ']'
|
||||
let key = '';
|
||||
while (i < path.length && path[i] !== ']') {
|
||||
key += path[i];
|
||||
i++;
|
||||
}
|
||||
i++; // skip ']'
|
||||
keys.push(key);
|
||||
}
|
||||
} else {
|
||||
// Bare key — collect until '.', '[', or end
|
||||
let key = '';
|
||||
while (i < path.length && path[i] !== '.' && path[i] !== '[') {
|
||||
key += path[i];
|
||||
i++;
|
||||
}
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getNestedValue(obj, path) {
|
||||
const keys = parsePath(path);
|
||||
let current = obj;
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), key)) {
|
||||
return { found: false };
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
return { found: true, value: current };
|
||||
}
|
||||
|
||||
chai.Assertion.addMethod('jsonBody', function () {
|
||||
const obj = this._obj;
|
||||
const args = Array.prototype.slice.call(arguments);
|
||||
|
||||
if (args.length === 0) {
|
||||
// No args: check body is valid JSON (object or array)
|
||||
this.assert(
|
||||
typeof obj === 'object' && obj !== null,
|
||||
`expected ${utils.inspect(obj)} to be a JSON body (object or array)`,
|
||||
`expected ${utils.inspect(obj)} not to be a JSON body`
|
||||
);
|
||||
} else if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
||||
// Object arg: deep equality
|
||||
this.assert(
|
||||
utils.eql(obj, args[0]),
|
||||
`expected body to deeply equal ${utils.inspect(args[0])}`,
|
||||
`expected body to not deeply equal ${utils.inspect(args[0])}`
|
||||
);
|
||||
} else if (args.length === 1) {
|
||||
// String path: check nested property exists
|
||||
const result = getNestedValue(obj, String(args[0]));
|
||||
this.assert(
|
||||
result.found,
|
||||
`expected body to have nested property '${args[0]}'`,
|
||||
`expected body to not have nested property '${args[0]}'`
|
||||
);
|
||||
} else {
|
||||
// Path + value: check nested property equals value
|
||||
const result = getNestedValue(obj, String(args[0]));
|
||||
this.assert(
|
||||
result.found && utils.eql(result.value, args[1]),
|
||||
`expected body to have nested property '${args[0]}' equal to ${utils.inspect(args[1])}`,
|
||||
`expected body to not have nested property '${args[0]}' equal to ${utils.inspect(args[1])}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Assertion operators
|
||||
*
|
||||
|
||||
@@ -108,6 +108,130 @@ const addBruShimToContext = (vm, __brunoTestResults) => {
|
||||
})();
|
||||
`
|
||||
);
|
||||
// Register custom chai assertion for jsonBody (Postman parity)
|
||||
vm.evalCode(
|
||||
`
|
||||
(function() {
|
||||
var proto = Object.getPrototypeOf(expect(null));
|
||||
|
||||
// Parse a property path into an array of keys.
|
||||
// Handles: dot notation (a.b), numeric brackets (a[0]), quoted brackets (a["b.c"], a['key']),
|
||||
// and combinations like data[0]["a.b"].name
|
||||
//
|
||||
// Examples:
|
||||
// "a.b.c" -> ["a", "b", "c"]
|
||||
// "items[0].name" -> ["items", "0", "name"]
|
||||
// 'data["a.b"]' -> ["data", "a.b"]
|
||||
// "matrix[0][1]" -> ["matrix", "0", "1"]
|
||||
// 'nested["x.y"].z' -> ["nested", "x.y", "z"]
|
||||
// '["say \\"hi\\""]' -> ["say \\"hi\\""]
|
||||
function parsePath(path) {
|
||||
var keys = [];
|
||||
var i = 0;
|
||||
while (i < path.length) {
|
||||
if (path[i] === '.') {
|
||||
i++;
|
||||
} else if (path[i] === '[') {
|
||||
i++;
|
||||
if (i < path.length && (path[i] === "'" || path[i] === '"')) {
|
||||
var quote = path[i];
|
||||
i++;
|
||||
var key = '';
|
||||
while (i < path.length && path[i] !== quote) {
|
||||
if (path[i] === '\\\\' && i + 1 < path.length && path[i + 1] === quote) {
|
||||
key += quote;
|
||||
i += 2;
|
||||
} else {
|
||||
key += path[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
i++; // skip closing quote
|
||||
i++; // skip ']'
|
||||
keys.push(key);
|
||||
} else {
|
||||
var key = '';
|
||||
while (i < path.length && path[i] !== ']') {
|
||||
key += path[i];
|
||||
i++;
|
||||
}
|
||||
i++; // skip ']'
|
||||
keys.push(key);
|
||||
}
|
||||
} else {
|
||||
var key = '';
|
||||
while (i < path.length && path[i] !== '.' && path[i] !== '[') {
|
||||
key += path[i];
|
||||
i++;
|
||||
}
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getNestedValue(obj, path) {
|
||||
var keys = parsePath(path);
|
||||
var current = obj;
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), key)) {
|
||||
return { found: false };
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
return { found: true, value: current };
|
||||
}
|
||||
|
||||
function deepEqual(a, b) {
|
||||
if (a === b) return true;
|
||||
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
var keysA = Object.keys(a);
|
||||
var keysB = Object.keys(b);
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
for (var i = 0; i < keysA.length; i++) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, keysA[i]) || !deepEqual(a[keysA[i]], b[keysA[i]])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
proto.jsonBody = function() {
|
||||
var obj = this._obj;
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
|
||||
if (args.length === 0) {
|
||||
this.assert(
|
||||
typeof obj === 'object' && obj !== null,
|
||||
'expected value to be a JSON body (object or array)',
|
||||
'expected value not to be a JSON body'
|
||||
);
|
||||
} else if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
||||
this.assert(
|
||||
deepEqual(obj, args[0]),
|
||||
'expected body to deeply equal given object',
|
||||
'expected body to not deeply equal given object'
|
||||
);
|
||||
} else if (args.length === 1) {
|
||||
var result = getNestedValue(obj, String(args[0]));
|
||||
this.assert(
|
||||
result.found,
|
||||
"expected body to have nested property '" + args[0] + "'",
|
||||
"expected body to not have nested property '" + args[0] + "'"
|
||||
);
|
||||
} else {
|
||||
var result = getNestedValue(obj, String(args[0]));
|
||||
this.assert(
|
||||
result.found && deepEqual(result.value, args[1]),
|
||||
"expected body to have nested property '" + args[0] + "' equal to given value",
|
||||
"expected body to not have nested property '" + args[0] + "' equal to given value"
|
||||
);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
})();
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = addBruShimToContext;
|
||||
|
||||
Reference in New Issue
Block a user