mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: add JSON Schema validation support with custom chai assertion (#7301)
* feat: add JSON Schema validation support with custom chai assertion - Introduced a new custom assertion for JSON Schema validation in chai, allowing users to validate response bodies against defined schemas. - Updated the postman translation logic to translate `pm.response.to.have.jsonSchema` to the new assertion format. - Enhanced tests to cover various scenarios for JSON Schema validation, ensuring accurate translations and functionality. - Updated package dependencies to include the latest versions of relevant libraries. * refactor: enhance JSON Schema validation assertion and add comprehensive test cases * chore: add @rollup/plugin-json dependency and enhance JSON Schema validation tests - Added @rollup/plugin-json as a development dependency in package.json and package-lock.json. - Introduced new test cases for JSON Schema validation, covering various scenarios including valid schema matching, type mismatches, and required field checks. - Updated existing assertions to utilize the new validation capabilities. * refactor: streamline JSON Schema validation with default Ajv instance - Updated the custom chai assertion for JSON Schema validation to utilize a default Ajv instance, improving consistency and reducing redundancy in the code. - Enhanced the error messages in the assertion to include the actual data being validated, providing clearer feedback during validation failures. * refactor: improve error messaging in JSON Schema validation assertion - Enhanced the custom chai assertion for JSON Schema validation to provide clearer error messages by including a stringified version of the data being validated, improving feedback during validation failures. * refactor: simplify Ajv instance creation in JSON Schema validation - Removed the default Ajv instance and streamlined the creation of Ajv instances in the custom chai assertion for JSON Schema validation, ensuring consistent handling of ajvOptions across the codebase. * feat: add support for negated JSON Schema assertions in Postman translations - Introduced translations for `pm.response.to.not.have.jsonSchema`, `pm.response.not.to.have.jsonSchema`, and `pm.response.to.have.not.jsonSchema` to the new assertion format using `expect`. - Enhanced the translation logic to handle these new patterns and added corresponding test cases to ensure accurate functionality. - Updated existing tests to cover various scenarios for negated assertions, improving overall test coverage for JSON Schema validation. * fix: improve error handling in JSON Schema validation assertions - Added error handling for JSON schema compilation in the custom chai assertion, ensuring that any compilation errors are caught and reported with a clear message. - Updated tests to verify that malformed schemas correctly trigger assertion errors, enhancing the robustness of JSON Schema validation.
This commit is contained in:
137
package-lock.json
generated
137
package-lock.json
generated
@@ -8957,48 +8957,6 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitl/quickjs-ffi-types": {
|
||||
"version": "0.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.29.2.tgz",
|
||||
"integrity": "sha512-069uQTiEla2PphXg6UpyyJ4QXHkTj3S9TeXgaMCd8NDYz3ODBw5U/rkg6fhuU8SMpoDrWjEzybmV5Mi2Pafb5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jitl/quickjs-wasmfile-debug-asyncify": {
|
||||
"version": "0.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.29.2.tgz",
|
||||
"integrity": "sha512-YdRw2414pFkxzyyoJGv81Grbo9THp/5athDMKipaSBNNQvFE9FGRrgE9tt2DT2mhNnBx1kamtOGj0dX84Yy9bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.29.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitl/quickjs-wasmfile-debug-sync": {
|
||||
"version": "0.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.29.2.tgz",
|
||||
"integrity": "sha512-VgisubjyPMWEr44g+OU0QWGyIxu7VkApkLHMxdORX351cw22aLTJ+Z79DJ8IVrTWc7jh4CBPsaK71RBQDuVB7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.29.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitl/quickjs-wasmfile-release-asyncify": {
|
||||
"version": "0.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.29.2.tgz",
|
||||
"integrity": "sha512-sf3luCPr8wBVmGV6UV8Set+ie8wcO6mz5wMvDVO0b90UVCKfgnx65A1JfeA+zaSGoaFyTZ3sEpXSGJU+6qJmLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.29.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitl/quickjs-wasmfile-release-sync": {
|
||||
"version": "0.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.29.2.tgz",
|
||||
"integrity": "sha512-UFIcbY3LxBRUjEqCHq3Oa6bgX5znt51V5NQck8L2US4u989ErasiMLUjmhq6UPC837Sjqu37letEK/ZpqlJ7aA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.29.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -27099,31 +27057,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/quickjs-emscripten": {
|
||||
"version": "0.29.2",
|
||||
"resolved": "https://registry.npmjs.org/quickjs-emscripten/-/quickjs-emscripten-0.29.2.tgz",
|
||||
"integrity": "sha512-SlvkvyZgarReu2nr4rkf+xz1vN0YDUz7sx4WHz8LFtK6RNg4/vzAGcFjE7nfHYBEbKrzfIWvKnMnxZkctQ898w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-wasmfile-debug-asyncify": "0.29.2",
|
||||
"@jitl/quickjs-wasmfile-debug-sync": "0.29.2",
|
||||
"@jitl/quickjs-wasmfile-release-asyncify": "0.29.2",
|
||||
"@jitl/quickjs-wasmfile-release-sync": "0.29.2",
|
||||
"quickjs-emscripten-core": "0.29.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/quickjs-emscripten-core": {
|
||||
"version": "0.29.2",
|
||||
"resolved": "https://registry.npmjs.org/quickjs-emscripten-core/-/quickjs-emscripten-core-0.29.2.tgz",
|
||||
"integrity": "sha512-jEAiURW4jGqwO/fW01VwlWqa2G0AJxnN5FBy1xnVu8VIVhVhiaxUfCe+bHqS6zWzfjFm86HoO40lzpteusvyJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.29.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ramda": {
|
||||
"version": "0.30.1",
|
||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz",
|
||||
@@ -35882,7 +35815,7 @@
|
||||
"nanoid": "3.3.8",
|
||||
"node-fetch": "^2.7.0",
|
||||
"path": "^0.12.7",
|
||||
"quickjs-emscripten": "^0.29.2",
|
||||
"quickjs-emscripten": "^0.32.0",
|
||||
"tv4": "^1.3.0",
|
||||
"uuid": "^10.0.0",
|
||||
"xml-formatter": "^3.5.0",
|
||||
@@ -35891,11 +35824,54 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-terser": "^1.0.0",
|
||||
"rollup": "3.30.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/@jitl/quickjs-ffi-types": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.32.0.tgz",
|
||||
"integrity": "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-js/node_modules/@jitl/quickjs-wasmfile-debug-asyncify": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.32.0.tgz",
|
||||
"integrity": "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/@jitl/quickjs-wasmfile-debug-sync": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.32.0.tgz",
|
||||
"integrity": "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/@jitl/quickjs-wasmfile-release-asyncify": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.32.0.tgz",
|
||||
"integrity": "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/@jitl/quickjs-wasmfile-release-sync": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.32.0.tgz",
|
||||
"integrity": "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
@@ -35945,6 +35921,31 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-js/node_modules/quickjs-emscripten": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/quickjs-emscripten/-/quickjs-emscripten-0.32.0.tgz",
|
||||
"integrity": "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0",
|
||||
"@jitl/quickjs-wasmfile-debug-sync": "0.32.0",
|
||||
"@jitl/quickjs-wasmfile-release-asyncify": "0.32.0",
|
||||
"@jitl/quickjs-wasmfile-release-sync": "0.32.0",
|
||||
"quickjs-emscripten-core": "0.32.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/quickjs-emscripten-core": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/quickjs-emscripten-core/-/quickjs-emscripten-core-0.32.0.tgz",
|
||||
"integrity": "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
|
||||
@@ -36,6 +36,10 @@ const replacements = {
|
||||
'pm\\.variables\\.toObject\\(': 'bru.getAllVars(',
|
||||
'pm\\.request\\.headers\\.remove\\(': 'req.deleteHeader(',
|
||||
'pm\\.response\\.headers\\.get\\(': 'res.getHeader(',
|
||||
'pm\\.response\\.to\\.have\\.jsonSchema\\(': 'expect(res.getBody()).to.have.jsonSchema(',
|
||||
'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\\.body\\(': 'expect(res.getBody()).to.equal(',
|
||||
'pm\\.response\\.to\\.have\\.header\\(': 'expect(res.getHeaders()).to.have.property(',
|
||||
'pm\\.response\\.size\\(\\)': 'res.getSize()',
|
||||
|
||||
@@ -502,6 +502,74 @@ const complexTransformations = [
|
||||
}
|
||||
},
|
||||
|
||||
// pm.response.to.have.jsonSchema(schema, options?) -> expect(res.getBody()).to.have.jsonSchema(schema, options?)
|
||||
{
|
||||
pattern: 'pm.response.to.have.jsonSchema',
|
||||
transform: (path, j) => {
|
||||
const args = path.parent.value.arguments;
|
||||
return j.callExpression(
|
||||
j.memberExpression(
|
||||
j.callExpression(j.identifier('expect'), [
|
||||
j.callExpression(j.identifier('res.getBody'), [])
|
||||
]),
|
||||
j.identifier('to.have.jsonSchema')
|
||||
),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// pm.response.to.not.have.jsonSchema(schema, options?) -> expect(res.getBody()).to.not.have.jsonSchema(schema, options?)
|
||||
{
|
||||
pattern: 'pm.response.to.not.have.jsonSchema',
|
||||
transform: (path, j) => {
|
||||
const args = path.parent.value.arguments;
|
||||
return j.callExpression(
|
||||
j.memberExpression(
|
||||
j.callExpression(j.identifier('expect'), [
|
||||
j.callExpression(j.identifier('res.getBody'), [])
|
||||
]),
|
||||
j.identifier('to.not.have.jsonSchema')
|
||||
),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// pm.response.not.to.have.jsonSchema(schema, options?) -> expect(res.getBody()).not.to.have.jsonSchema(schema, options?)
|
||||
{
|
||||
pattern: 'pm.response.not.to.have.jsonSchema',
|
||||
transform: (path, j) => {
|
||||
const args = path.parent.value.arguments;
|
||||
return j.callExpression(
|
||||
j.memberExpression(
|
||||
j.callExpression(j.identifier('expect'), [
|
||||
j.callExpression(j.identifier('res.getBody'), [])
|
||||
]),
|
||||
j.identifier('not.to.have.jsonSchema')
|
||||
),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// pm.response.to.have.not.jsonSchema(schema, options?) -> expect(res.getBody()).to.have.not.jsonSchema(schema, options?)
|
||||
{
|
||||
pattern: 'pm.response.to.have.not.jsonSchema',
|
||||
transform: (path, j) => {
|
||||
const args = path.parent.value.arguments;
|
||||
return j.callExpression(
|
||||
j.memberExpression(
|
||||
j.callExpression(j.identifier('expect'), [
|
||||
j.callExpression(j.identifier('res.getBody'), [])
|
||||
]),
|
||||
j.identifier('to.have.not.jsonSchema')
|
||||
),
|
||||
args
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Legacy postman.getResponseHeader(name) -> res.getHeader(name)
|
||||
{
|
||||
pattern: 'pm.getResponseHeader',
|
||||
|
||||
@@ -682,4 +682,94 @@ describe('Response Translation', () => {
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property(path)');
|
||||
});
|
||||
|
||||
// --- JSON Schema assertions ---------------------------
|
||||
|
||||
it('should translate pm.response.to.have.jsonSchema with variable reference', () => {
|
||||
const code = 'pm.response.to.have.jsonSchema(schema);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('expect(res.getBody()).to.have.jsonSchema(schema);');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonSchema with two arguments', () => {
|
||||
const code = 'pm.response.to.have.jsonSchema(schema, options);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('expect(res.getBody()).to.have.jsonSchema(schema, options);');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonSchema with inline object', () => {
|
||||
const code = 'pm.response.to.have.jsonSchema({type: "object"});';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonSchema({type: "object"})');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonSchema inside a test block', () => {
|
||||
const code = `
|
||||
pm.test("Schema is valid", function() {
|
||||
pm.response.to.have.jsonSchema(schema);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('test("Schema is valid", function() {');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.have.jsonSchema(schema)');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.jsonSchema with response alias', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
resp.to.have.jsonSchema(schema);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
`);
|
||||
});
|
||||
|
||||
// --- Negated JSON Schema assertions --------------------
|
||||
|
||||
it('should translate pm.response.to.not.have.jsonSchema', () => {
|
||||
const code = 'pm.response.to.not.have.jsonSchema(schema);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('expect(res.getBody()).to.not.have.jsonSchema(schema);');
|
||||
});
|
||||
|
||||
it('should translate pm.response.not.to.have.jsonSchema', () => {
|
||||
const code = 'pm.response.not.to.have.jsonSchema(schema);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('expect(res.getBody()).not.to.have.jsonSchema(schema);');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.have.not.jsonSchema', () => {
|
||||
const code = 'pm.response.to.have.not.jsonSchema(schema);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('expect(res.getBody()).to.have.not.jsonSchema(schema);');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.not.have.jsonSchema with two arguments', () => {
|
||||
const code = 'pm.response.to.not.have.jsonSchema(schema, options);';
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe('expect(res.getBody()).to.not.have.jsonSchema(schema, options);');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.not.have.jsonSchema inside a test block', () => {
|
||||
const code = `
|
||||
pm.test("Schema should not match", function() {
|
||||
pm.response.to.not.have.jsonSchema(schema);
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('test("Schema should not match", function() {');
|
||||
expect(translatedCode).toContain('expect(res.getBody()).to.not.have.jsonSchema(schema)');
|
||||
});
|
||||
|
||||
it('should translate pm.response.to.not.have.jsonSchema with response alias', () => {
|
||||
const code = `
|
||||
const resp = pm.response;
|
||||
resp.to.not.have.jsonSchema(schema);
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
expect(res.getBody()).to.not.have.jsonSchema(schema);
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"nanoid": "3.3.8",
|
||||
"node-fetch": "^2.7.0",
|
||||
"path": "^0.12.7",
|
||||
"quickjs-emscripten": "^0.29.2",
|
||||
"quickjs-emscripten": "^0.32.0",
|
||||
"tv4": "^1.3.0",
|
||||
"uuid": "^10.0.0",
|
||||
"xml-formatter": "^3.5.0",
|
||||
@@ -39,6 +39,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"rollup": "3.30.0",
|
||||
"@rollup/plugin-terser": "^1.0.0"
|
||||
|
||||
@@ -7,6 +7,7 @@ const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser }
|
||||
const { interpolateString } = require('../interpolate-string');
|
||||
const { executeQuickJsVm } = require('../sandbox/quickjs');
|
||||
|
||||
const Ajv = require('ajv');
|
||||
const { expect } = chai;
|
||||
chai.use(require('chai-string'));
|
||||
chai.use(function (chai, utils) {
|
||||
@@ -24,6 +25,27 @@ chai.use(function (chai, utils) {
|
||||
});
|
||||
});
|
||||
|
||||
// Custom assertion for JSON Schema validation
|
||||
chai.use(function (chai) {
|
||||
chai.Assertion.addMethod('jsonSchema', function (schema, ajvOptions) {
|
||||
const ajv = new Ajv({ allErrors: true, ...ajvOptions });
|
||||
let validate;
|
||||
try {
|
||||
validate = ajv.compile(schema);
|
||||
} catch (e) {
|
||||
this.assert(false, 'JSON schema compile error: ' + e.message, 'JSON schema compile error: ' + e.message);
|
||||
}
|
||||
const data = this._obj;
|
||||
const isValid = validate(data);
|
||||
|
||||
this.assert(
|
||||
isValid,
|
||||
'expected #{this} to match JSON schema, validation errors: ' + (validate.errors ? JSON.stringify(validate.errors) : 'none'),
|
||||
'expected #{this} to not match JSON schema'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Custom assertion for matching regex
|
||||
chai.use(function (chai, utils) {
|
||||
chai.Assertion.addMethod('match', function (regex) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const rollup = require('rollup');
|
||||
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const json = require('@rollup/plugin-json');
|
||||
const fs = require('fs');
|
||||
const terser = require('@rollup/plugin-terser').default;
|
||||
|
||||
@@ -13,6 +14,7 @@ const bundleLibraries = async () => {
|
||||
import atob from "atob";
|
||||
import * as cryptoJs from 'crypto-js';
|
||||
import tv4 from "tv4";
|
||||
import Ajv from "ajv";
|
||||
globalThis.expect = expect;
|
||||
globalThis.assert = assert;
|
||||
globalThis.moment = moment;
|
||||
@@ -20,6 +22,7 @@ const bundleLibraries = async () => {
|
||||
globalThis.atob = atob;
|
||||
globalThis.Buffer = Buffer;
|
||||
globalThis.tv4 = tv4;
|
||||
globalThis.Ajv = Ajv;
|
||||
globalThis.requireObject = {
|
||||
...(globalThis.requireObject || {}),
|
||||
'chai': { expect, assert },
|
||||
@@ -28,7 +31,8 @@ const bundleLibraries = async () => {
|
||||
'btoa': btoa,
|
||||
'atob': atob,
|
||||
'crypto-js': cryptoJs,
|
||||
'tv4': tv4
|
||||
'tv4': tv4,
|
||||
'ajv': Ajv
|
||||
};
|
||||
`;
|
||||
|
||||
@@ -56,6 +60,7 @@ const bundleLibraries = async () => {
|
||||
browser: false
|
||||
}),
|
||||
commonjs(),
|
||||
json(),
|
||||
terser()
|
||||
]
|
||||
},
|
||||
|
||||
@@ -79,6 +79,35 @@ const addBruShimToContext = (vm, __brunoTestResults) => {
|
||||
})();
|
||||
`
|
||||
);
|
||||
// Register custom chai assertion for jsonSchema (expect(...).to.have.jsonSchema(schema, options))
|
||||
vm.evalCode(
|
||||
`
|
||||
(function() {
|
||||
var Ajv = require('ajv');
|
||||
var proto = Object.getPrototypeOf(expect(null));
|
||||
proto.jsonSchema = function(schema, ajvOptions) {
|
||||
var ajv = new Ajv(Object.assign({ allErrors: true }, ajvOptions || {}));
|
||||
var validate;
|
||||
try {
|
||||
validate = ajv.compile(schema);
|
||||
} catch (e) {
|
||||
this.assert(false, 'JSON schema compile error: ' + e.message, 'JSON schema compile error: ' + e.message);
|
||||
}
|
||||
var data = this._obj;
|
||||
var isValid = validate(data);
|
||||
|
||||
var dataStr;
|
||||
try { dataStr = JSON.stringify(data); } catch (e) { dataStr = '[unserializable value]'; }
|
||||
this.assert(
|
||||
isValid,
|
||||
'expected ' + dataStr + ' to match JSON schema, validation errors: ' + (validate.errors ? JSON.stringify(validate.errors) : 'none'),
|
||||
'expected ' + dataStr + ' to not match JSON schema'
|
||||
);
|
||||
return this;
|
||||
};
|
||||
})();
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = addBruShimToContext;
|
||||
|
||||
@@ -365,5 +365,67 @@ describe('runtime', () => {
|
||||
expect(results[0].status).toBe('fail');
|
||||
});
|
||||
});
|
||||
|
||||
describe('jsonSchema', () => {
|
||||
const chai = require('chai');
|
||||
|
||||
it('should pass when body matches a valid schema', () => {
|
||||
const body = { name: 'John', age: 30 };
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' }
|
||||
},
|
||||
required: ['name', 'age']
|
||||
};
|
||||
chai.expect(body).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
it('should fail when body has a type mismatch', () => {
|
||||
const body = { name: 'John', age: 'thirty' };
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' }
|
||||
},
|
||||
required: ['name', 'age']
|
||||
};
|
||||
expect(() => chai.expect(body).to.have.jsonSchema(schema)).toThrow(/validation errors/);
|
||||
});
|
||||
|
||||
it('should fail when a required field is missing', () => {
|
||||
const body = { name: 'John' };
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' }
|
||||
},
|
||||
required: ['name', 'age']
|
||||
};
|
||||
expect(() => chai.expect(body).to.have.jsonSchema(schema)).toThrow(/validation errors/);
|
||||
});
|
||||
|
||||
it('should pass with custom ajvOptions', () => {
|
||||
const body = { name: 'John', age: 30 };
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' }
|
||||
},
|
||||
required: ['name', 'age']
|
||||
};
|
||||
chai.expect(body).to.have.jsonSchema(schema, { allErrors: false });
|
||||
});
|
||||
|
||||
it('should support negation with .not', () => {
|
||||
const body = { name: 'John' };
|
||||
const schema = { type: 'array' };
|
||||
chai.expect(body).to.not.have.jsonSchema(schema);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
349
packages/bruno-tests/collection/scripting/api/res/jsonSchema.bru
Normal file
349
packages/bruno-tests/collection/scripting/api/res/jsonSchema.bru
Normal file
@@ -0,0 +1,349 @@
|
||||
meta {
|
||||
name: jsonSchema
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
"email": "john@example.com",
|
||||
"status": "active",
|
||||
"score": 95.5,
|
||||
"isVerified": true,
|
||||
"tags": ["developer", "tester"],
|
||||
"address": {
|
||||
"street": "123 Main St",
|
||||
"city": "Springfield",
|
||||
"zip": "62701"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
// --- Passing validations ---
|
||||
|
||||
test("Basic object with properties and required", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
email: { type: 'string' },
|
||||
status: { type: 'string' },
|
||||
score: { type: 'number' },
|
||||
isVerified: { type: 'boolean' },
|
||||
tags: { type: 'array' },
|
||||
address: { type: 'object' }
|
||||
},
|
||||
required: ['name', 'age', 'email']
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("Nested object validation", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string' },
|
||||
city: { type: 'string' },
|
||||
zip: { type: 'string' }
|
||||
},
|
||||
required: ['street', 'city', 'zip']
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("Array items validation", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' }
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("String pattern (regex)", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
pattern: '^[^@]+@[^@]+\\.[^@]+$'
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("Enum validation", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive', 'pending']
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("Number range (minimum/maximum)", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
age: {
|
||||
type: 'number',
|
||||
minimum: 0,
|
||||
maximum: 150
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("String length constraints (minLength/maxLength)", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("allOf composition", function() {
|
||||
const schema = {
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
required: ['name']
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: { age: { type: 'number' } },
|
||||
required: ['age']
|
||||
}
|
||||
]
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("anyOf composition", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
score: {
|
||||
anyOf: [
|
||||
{ type: 'number' },
|
||||
{ type: 'string' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("jsonSchema with ajvOptions", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' }
|
||||
},
|
||||
required: ['name', 'age']
|
||||
};
|
||||
expect(res.getBody()).to.have.jsonSchema(schema, { allErrors: true });
|
||||
});
|
||||
|
||||
// --- Failure validations ---
|
||||
|
||||
test("Type mismatch - schema expects array, response is object", function() {
|
||||
const schema = { type: 'array' };
|
||||
let failed = false;
|
||||
try {
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
|
||||
test("Missing required field", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
required: ['nonExistentField']
|
||||
};
|
||||
let failed = false;
|
||||
try {
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
|
||||
test("additionalProperties false rejects extra fields", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' }
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
let failed = false;
|
||||
try {
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
|
||||
test("Enum mismatch", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['deleted', 'archived']
|
||||
}
|
||||
},
|
||||
required: ['status']
|
||||
};
|
||||
let failed = false;
|
||||
try {
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
|
||||
test("Pattern mismatch - name does not match digits-only", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
pattern: '^[0-9]+$'
|
||||
}
|
||||
},
|
||||
required: ['name']
|
||||
};
|
||||
let failed = false;
|
||||
try {
|
||||
expect(res.getBody()).to.have.jsonSchema(schema);
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
|
||||
// --- Malformed schema (ajv.compile error) ---
|
||||
|
||||
test("Malformed schema - invalid type throws assertion error", function() {
|
||||
const schema = { type: 'invalidType' };
|
||||
expect(() => expect(res.getBody()).to.have.jsonSchema(schema)).to.throw('JSON schema compile error');
|
||||
});
|
||||
|
||||
// --- .not (negation) validations ---
|
||||
|
||||
test(".not with mismatched type - body is object, not array", function() {
|
||||
const schema = { type: 'array' };
|
||||
expect(res.getBody()).to.not.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test(".not with missing required field", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
required: ['nonExistent']
|
||||
};
|
||||
expect(res.getBody()).to.not.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test(".not fails when schema actually matches", function() {
|
||||
const schema = { type: 'object' };
|
||||
let failed = false;
|
||||
try {
|
||||
expect(res.getBody()).to.not.have.jsonSchema(schema);
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
|
||||
// --- not.to.have (negation) validations ---
|
||||
|
||||
test("not.to.have with mismatched type - body is object, not array", function() {
|
||||
const schema = { type: 'array' };
|
||||
expect(res.getBody()).not.to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("not.to.have with missing required field", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
required: ['nonExistent']
|
||||
};
|
||||
expect(res.getBody()).not.to.have.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("not.to.have fails when schema actually matches", function() {
|
||||
const schema = { type: 'object' };
|
||||
let failed = false;
|
||||
try {
|
||||
expect(res.getBody()).not.to.have.jsonSchema(schema);
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
|
||||
// --- to.have.not (negation) validations ---
|
||||
|
||||
test("to.have.not with mismatched type - body is object, not array", function() {
|
||||
const schema = { type: 'array' };
|
||||
expect(res.getBody()).to.have.not.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("to.have.not with missing required field", function() {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
required: ['nonExistent']
|
||||
};
|
||||
expect(res.getBody()).to.have.not.jsonSchema(schema);
|
||||
});
|
||||
|
||||
test("to.have.not fails when schema actually matches", function() {
|
||||
const schema = { type: 'object' };
|
||||
let failed = false;
|
||||
try {
|
||||
expect(res.getBody()).to.have.not.jsonSchema(schema);
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed).to.be.true;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user