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:
sanish chirayath
2026-04-22 12:59:32 +05:30
committed by GitHub
parent c4dc0bc10d
commit e12b736516
7 changed files with 602 additions and 33 deletions

View File

@@ -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
*

View File

@@ -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;