mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-12 10:21:30 +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:
@@ -40,6 +40,10 @@ const replacements = {
|
||||
'pm\\.response\\.to\\.not\\.have\\.jsonSchema\\(': 'expect(res.getBody()).to.not.have.jsonSchema(',
|
||||
'pm\\.response\\.not\\.to\\.have\\.jsonSchema\\(': 'expect(res.getBody()).not.to.have.jsonSchema(',
|
||||
'pm\\.response\\.to\\.have\\.not\\.jsonSchema\\(': 'expect(res.getBody()).to.have.not.jsonSchema(',
|
||||
'pm\\.response\\.to\\.have\\.jsonBody\\(': 'expect(res.getBody()).to.have.jsonBody(',
|
||||
'pm\\.response\\.to\\.not\\.have\\.jsonBody\\(': 'expect(res.getBody()).to.not.have.jsonBody(',
|
||||
'pm\\.response\\.not\\.to\\.have\\.jsonBody\\(': 'expect(res.getBody()).not.to.have.jsonBody(',
|
||||
'pm\\.response\\.to\\.have\\.not\\.jsonBody\\(': 'expect(res.getBody()).to.have.not.jsonBody(',
|
||||
'pm\\.response\\.to\\.have\\.body\\(': 'expect(res.getBody()).to.equal(',
|
||||
'pm\\.response\\.to\\.have\\.header\\(': 'expect(res.getHeaders()).to.have.property(',
|
||||
'pm\\.response\\.size\\(\\)': 'res.getSize()',
|
||||
|
||||
@@ -467,38 +467,31 @@ const complexTransformations = [
|
||||
}
|
||||
},
|
||||
|
||||
// pm.response.to.have.jsonBody(path) -> expect(res.getBody()).to.have.nested.property(path)
|
||||
// pm.response.to.have.jsonBody(...) -> expect(res.getBody()).to.have.jsonBody(...)
|
||||
{
|
||||
pattern: 'pm.response.to.have.jsonBody',
|
||||
transform: (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
const args = callExpr.arguments;
|
||||
const expectGetBody = j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]);
|
||||
return j.callExpression(
|
||||
j.memberExpression(expectGetBody, j.identifier('to.have.jsonBody')),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
if (args.length === 0) {
|
||||
// No path provided, just check that body exists
|
||||
return j.memberExpression(
|
||||
j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]),
|
||||
j.identifier('to.exist')
|
||||
);
|
||||
} else if (args.length === 1) {
|
||||
// Path provided, check property exists
|
||||
return j.callExpression(
|
||||
j.memberExpression(
|
||||
j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]),
|
||||
j.identifier('to.have.nested.property')
|
||||
),
|
||||
args
|
||||
);
|
||||
} else {
|
||||
// Path and value provided, check property equals value
|
||||
return j.callExpression(
|
||||
j.memberExpression(
|
||||
j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]),
|
||||
j.identifier('to.have.nested.property')
|
||||
),
|
||||
args
|
||||
);
|
||||
}
|
||||
// pm.response.to.not.have.jsonBody(...) -> expect(res.getBody()).to.not.have.jsonBody(...)
|
||||
{
|
||||
pattern: 'pm.response.to.not.have.jsonBody',
|
||||
transform: (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
const args = callExpr.arguments;
|
||||
const expectGetBody = j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]);
|
||||
return j.callExpression(
|
||||
j.memberExpression(expectGetBody, j.identifier('to.not.have.jsonBody')),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -570,6 +563,34 @@ const complexTransformations = [
|
||||
}
|
||||
},
|
||||
|
||||
// pm.response.not.to.have.jsonBody(...) -> expect(res.getBody()).not.to.have.jsonBody(...)
|
||||
{
|
||||
pattern: 'pm.response.not.to.have.jsonBody',
|
||||
transform: (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
const args = callExpr.arguments;
|
||||
const expectGetBody = j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]);
|
||||
return j.callExpression(
|
||||
j.memberExpression(expectGetBody, j.identifier('not.to.have.jsonBody')),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// pm.response.to.have.not.jsonBody(...) -> expect(res.getBody()).to.have.not.jsonBody(...)
|
||||
{
|
||||
pattern: 'pm.response.to.have.not.jsonBody',
|
||||
transform: (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
const args = callExpr.arguments;
|
||||
const expectGetBody = j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]);
|
||||
return j.callExpression(
|
||||
j.memberExpression(expectGetBody, j.identifier('to.have.not.jsonBody')),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Legacy postman.getResponseHeader(name) -> res.getHeader(name)
|
||||
{
|
||||
pattern: 'pm.getResponseHeader',
|
||||
|
||||
@@ -277,6 +277,10 @@ describe('Multiline Syntax Handling', () => {
|
||||
expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)');
|
||||
expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("content-type".toLowerCase())');
|
||||
|
||||
// Check jsonBody translations (positive and negation)
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody("success", true)');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.not.have.jsonBody("error")');
|
||||
|
||||
// Check flow control
|
||||
expect(translatedCode).toContain('if (res.getStatus() === 401)');
|
||||
expect(translatedCode).toContain('bru.runner.stopExecution()');
|
||||
|
||||
@@ -643,19 +643,38 @@ describe('Response Translation', () => {
|
||||
it('should translate pm.response.to.have.jsonBody with path', () => {
|
||||
const code = 'pm.response.to.have.jsonBody("user.id");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property("user.id")');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody("user.id")');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonBody with path and value', () => {
|
||||
const code = 'pm.response.to.have.jsonBody("status", "success");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property("status", "success")');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody("status", "success")');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonBody without arguments', () => {
|
||||
const code = 'pm.response.to.have.jsonBody();';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.exist');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody()');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonBody with object argument', () => {
|
||||
const code = 'pm.response.to.have.jsonBody({ success: true, data: { id: 1 } });';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody');
|
||||
expect(translatedCode).toContain('success: true');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonBody with path and numeric value', () => {
|
||||
const code = 'pm.response.to.have.jsonBody("data.count", 42);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody("data.count", 42)');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonBody with array argument as nested property', () => {
|
||||
const code = 'pm.response.to.have.jsonBody(["a", "b"]);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody(["a", "b"])');
|
||||
});
|
||||
|
||||
it('should handle pm.response.to.have.jsonBody inside test blocks', () => {
|
||||
@@ -667,20 +686,45 @@ describe('Response Translation', () => {
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('test("Response validation", function() {');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property("data")');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property("data.id", 123)');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody("data")');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody("data.id", 123)');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonBody with nested path', () => {
|
||||
const code = 'pm.response.to.have.jsonBody("response.data.items[0].name");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property("response.data.items[0].name")');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody("response.data.items[0].name")');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonBody with variable path', () => {
|
||||
const code = 'const path = "user.id"; pm.response.to.have.jsonBody(path);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property(path)');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonBody(path)');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.not.have.jsonBody without arguments', () => {
|
||||
const code = 'pm.response.to.not.have.jsonBody();';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.not.have.jsonBody()');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.not.have.jsonBody with path', () => {
|
||||
const code = 'pm.response.to.not.have.jsonBody("error");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.not.have.jsonBody("error")');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.not.have.jsonBody with path and value', () => {
|
||||
const code = 'pm.response.to.not.have.jsonBody("status", "error");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.not.have.jsonBody("status", "error")');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.not.have.jsonBody with object argument', () => {
|
||||
const code = 'pm.response.to.not.have.jsonBody({ error: true });';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.not.have.jsonBody');
|
||||
expect(translatedCode).toContain('error: true');
|
||||
});
|
||||
|
||||
// --- JSON Schema assertions ---------------------------
|
||||
@@ -772,4 +816,44 @@ describe('Response Translation', () => {
|
||||
expect(res.getBody()).to.not.have.jsonSchema(schema);
|
||||
`);
|
||||
});
|
||||
|
||||
// --- not.to.have.jsonBody ---
|
||||
|
||||
it('should translate pm.response.not.to.have.jsonBody without arguments', () => {
|
||||
const code = 'pm.response.not.to.have.jsonBody();';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).not.to.have.jsonBody()');
|
||||
});
|
||||
|
||||
it('should translate pm.response.not.to.have.jsonBody with path', () => {
|
||||
const code = 'pm.response.not.to.have.jsonBody("error");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).not.to.have.jsonBody("error")');
|
||||
});
|
||||
|
||||
it('should translate pm.response.not.to.have.jsonBody with path and value', () => {
|
||||
const code = 'pm.response.not.to.have.jsonBody("status", "error");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).not.to.have.jsonBody("status", "error")');
|
||||
});
|
||||
|
||||
// --- to.have.not.jsonBody ---
|
||||
|
||||
it('should translate pm.response.to.have.not.jsonBody without arguments', () => {
|
||||
const code = 'pm.response.to.have.not.jsonBody();';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.not.jsonBody()');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.not.jsonBody with path', () => {
|
||||
const code = 'pm.response.to.have.not.jsonBody("error");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.not.jsonBody("error")');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.not.jsonBody with path and value', () => {
|
||||
const code = 'pm.response.to.have.not.jsonBody("status", "error");';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.not.jsonBody("status", "error")');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
220
packages/bruno-tests/collection/scripting/api/res/jsonBody.bru
Normal file
220
packages/bruno-tests/collection/scripting/api/res/jsonBody.bru
Normal file
@@ -0,0 +1,220 @@
|
||||
meta {
|
||||
name: jsonBody
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"hello": "bruno",
|
||||
"data": {
|
||||
"items": [
|
||||
{ "name": "first" },
|
||||
{ "name": "second" }
|
||||
]
|
||||
},
|
||||
"matrix": [[1, 2], [3, 4]],
|
||||
"tags": ["api", "test"],
|
||||
"some-key": "hyphenated",
|
||||
"a.b": "dotted-key",
|
||||
"nested": {
|
||||
"x.y": { "z": "deep-dotted" }
|
||||
},
|
||||
"it's": "apostrophe-key",
|
||||
"say \"hi\"": "quoted-key"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
|
||||
tests {
|
||||
test("jsonBody() - no args validates JSON body", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody();
|
||||
});
|
||||
|
||||
test("jsonBody(object) - deep equality", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody({
|
||||
"hello": "bruno",
|
||||
"data": {
|
||||
"items": [
|
||||
{ "name": "first" },
|
||||
{ "name": "second" }
|
||||
]
|
||||
},
|
||||
"matrix": [[1, 2], [3, 4]],
|
||||
"tags": ["api", "test"],
|
||||
"some-key": "hyphenated",
|
||||
"a.b": "dotted-key",
|
||||
"nested": {
|
||||
"x.y": { "z": "deep-dotted" }
|
||||
},
|
||||
"it's": "apostrophe-key",
|
||||
"say \"hi\"": "quoted-key"
|
||||
});
|
||||
});
|
||||
|
||||
test("jsonBody(path) - nested property exists", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody("hello");
|
||||
expect(body).to.have.jsonBody("data.items");
|
||||
});
|
||||
|
||||
test("jsonBody(path, value) - nested property equals value", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody("hello", "bruno");
|
||||
});
|
||||
|
||||
test("jsonBody with bracket notation", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody("data.items[0].name", "first");
|
||||
});
|
||||
|
||||
// --- bracket notation and array access ---
|
||||
|
||||
test("bracket notation - access array element returns object", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody("data.items[0]", { "name": "first" });
|
||||
expect(body).to.have.jsonBody("data.items[1]", { "name": "second" });
|
||||
});
|
||||
|
||||
test("bracket notation - access top-level array elements", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody("tags[0]", "api");
|
||||
expect(body).to.have.jsonBody("tags[1]", "test");
|
||||
});
|
||||
|
||||
test("bracket notation - consecutive brackets for nested arrays", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody("matrix[0][0]", 1);
|
||||
expect(body).to.have.jsonBody("matrix[0][1]", 2);
|
||||
expect(body).to.have.jsonBody("matrix[1][0]", 3);
|
||||
expect(body).to.have.jsonBody("matrix[1][1]", 4);
|
||||
});
|
||||
|
||||
test("bracket notation - access nested array as whole value", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody("matrix[0]", [1, 2]);
|
||||
expect(body).to.have.jsonBody("matrix[1]", [3, 4]);
|
||||
});
|
||||
|
||||
test("bracket notation - out of bounds index is not found", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.not.have.jsonBody("tags[5]");
|
||||
expect(body).to.not.have.jsonBody("data.items[99]");
|
||||
});
|
||||
|
||||
// --- edge cases: string bracket keys and keys with dots ---
|
||||
|
||||
test("quoted bracket notation - double quotes for string keys", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody('["some-key"]', "hyphenated");
|
||||
});
|
||||
|
||||
test("quoted bracket notation - single quotes for string keys", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody("['some-key']", "hyphenated");
|
||||
});
|
||||
|
||||
test("quoted bracket notation - keys containing dots", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody('["a.b"]', "dotted-key");
|
||||
});
|
||||
|
||||
test("quoted bracket notation - nested path with dotted keys", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.jsonBody('nested["x.y"].z', "deep-dotted");
|
||||
});
|
||||
|
||||
test("quoted bracket notation - key containing the other quote type", function() {
|
||||
const body = res.getBody();
|
||||
// Key is: it's — use double quotes so the single quote is not a delimiter
|
||||
expect(body).to.have.jsonBody("[\"it's\"]", "apostrophe-key");
|
||||
});
|
||||
|
||||
test("quoted bracket notation - escaped quotes in key", function() {
|
||||
const body = res.getBody();
|
||||
// Key is: say "hi" — use escaped double quotes inside double-quoted bracket
|
||||
expect(body).to.have.jsonBody('["say \\"hi\\""]', "quoted-key");
|
||||
});
|
||||
|
||||
test("to.not.have.jsonBody(path) - negation for missing property", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.not.have.jsonBody("nonexistent");
|
||||
});
|
||||
|
||||
test("to.not.have.jsonBody(path, value) - negation for wrong value", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.not.have.jsonBody("hello", "wrong");
|
||||
});
|
||||
|
||||
test("to.not.have.jsonBody(object) - negation for deep inequality", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.not.have.jsonBody({ "wrong": "data" });
|
||||
});
|
||||
|
||||
// --- not.to.have.jsonBody ---
|
||||
|
||||
test("not.to.have.jsonBody(path) - negation for missing property", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).not.to.have.jsonBody("nonexistent");
|
||||
});
|
||||
|
||||
test("not.to.have.jsonBody(path, value) - negation for wrong value", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).not.to.have.jsonBody("hello", "wrong");
|
||||
});
|
||||
|
||||
test("not.to.have.jsonBody(object) - negation for deep inequality", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).not.to.have.jsonBody({ "wrong": "data" });
|
||||
});
|
||||
|
||||
test("not.to.have.jsonBody fails when body matches", function() {
|
||||
const body = res.getBody();
|
||||
let failed = false;
|
||||
try {
|
||||
expect(body).not.to.have.jsonBody("hello");
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
|
||||
// --- to.have.not.jsonBody ---
|
||||
|
||||
test("to.have.not.jsonBody(path) - negation for missing property", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.not.jsonBody("nonexistent");
|
||||
});
|
||||
|
||||
test("to.have.not.jsonBody(path, value) - negation for wrong value", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.not.jsonBody("hello", "wrong");
|
||||
});
|
||||
|
||||
test("to.have.not.jsonBody(object) - negation for deep inequality", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.not.jsonBody({ "wrong": "data" });
|
||||
});
|
||||
|
||||
test("to.have.not.jsonBody fails when body matches", function() {
|
||||
const body = res.getBody();
|
||||
let failed = false;
|
||||
try {
|
||||
expect(body).to.have.not.jsonBody("hello");
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user