mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: isJson assertion fails after res.setBody() with object in node-vm (#7191)
* fix: isJson assertion fails after res.setBody() with object in node-vm
Objects created inside Node's vm.createContext() have a different Object
constructor than the host realm. When res.setBody() is called with a JS
object from a script, _.cloneDeep preserves the cross-realm prototype,
causing obj.constructor === Object to fail in the isJson assertion.
Replace with Object.prototype.toString.call() which is cross-realm safe.
* fix: register isJson chai assertion in QuickJS test runtime
The bundled chai in QuickJS only exposes { expect, assert } via
requireObject — no Assertion class. Access the prototype through
Object.getPrototypeOf(expect(null)) and use Object.defineProperty
to register the json property directly.
* fix: enable assertion chaining on isJson in QuickJS runtime
The QuickJS isJson property getter was missing `return this`, preventing
chai assertion chaining (e.g. expect(body).to.be.json.and...).
This commit is contained in:
@@ -13,7 +13,12 @@ chai.use(function (chai, utils) {
|
||||
// Custom assertion for checking if a variable is JSON
|
||||
chai.Assertion.addProperty('json', function () {
|
||||
const obj = this._obj;
|
||||
const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj) && obj.constructor === Object;
|
||||
// Use Object.prototype.toString instead of constructor check for cross-realm compatibility.
|
||||
// Objects created inside Node's vm.createContext() have a different Object constructor,
|
||||
// so obj.constructor === Object fails for objects passed via res.setBody() from scripts.
|
||||
// Note: toString check is more permissive than constructor check — custom class instances
|
||||
const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj)
|
||||
&& Object.prototype.toString.call(obj) === '[object Object]';
|
||||
|
||||
this.assert(isJson, `expected ${utils.inspect(obj)} to be JSON`, `expected ${utils.inspect(obj)} not to be JSON`);
|
||||
});
|
||||
|
||||
@@ -58,6 +58,27 @@ const addBruShimToContext = (vm, __brunoTestResults) => {
|
||||
globalThis.test = Test(__brunoTestResults);
|
||||
`
|
||||
);
|
||||
|
||||
// Register custom chai assertion for isJson (expect(...).to.be.json)
|
||||
// The bundled chai only exposes { expect, assert } — no Assertion class.
|
||||
// Access the prototype through an expect() instance instead.
|
||||
vm.evalCode(
|
||||
`
|
||||
(function() {
|
||||
var proto = Object.getPrototypeOf(expect(null));
|
||||
Object.defineProperty(proto, 'json', {
|
||||
get: function () {
|
||||
var obj = this._obj;
|
||||
var isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj) &&
|
||||
Object.prototype.toString.call(obj) === '[object Object]';
|
||||
this.assert(isJson, 'expected #{this} to be JSON', 'expected #{this} not to be JSON');
|
||||
return this;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
})();
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = addBruShimToContext;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
const TestRuntime = require('../src/runtime/test-runtime');
|
||||
const ScriptRuntime = require('../src/runtime/script-runtime');
|
||||
const AssertRuntime = require('../src/runtime/assert-runtime');
|
||||
const Bru = require('../src/bru');
|
||||
const VarsRuntime = require('../src/runtime/vars-runtime');
|
||||
|
||||
@@ -258,4 +259,87 @@ describe('runtime', () => {
|
||||
expect(result.runtimeVariables.title).toBe('{{$randomFirstName}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('assert-runtime', () => {
|
||||
const baseRequest = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:3000/',
|
||||
headers: {},
|
||||
data: undefined
|
||||
};
|
||||
|
||||
const makeResponse = (data) => ({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
data,
|
||||
headers: {}
|
||||
});
|
||||
|
||||
const runAssertions = (assertions, response, runtime = 'nodevm') => {
|
||||
const assertRuntime = new AssertRuntime({ runtime });
|
||||
return assertRuntime.runAssertions(assertions, { ...baseRequest }, response, {}, {}, process.env);
|
||||
};
|
||||
|
||||
describe('isJson', () => {
|
||||
it('should pass for a plain object', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse({ id: 1, name: 'test' })
|
||||
);
|
||||
expect(results[0].status).toBe('pass');
|
||||
});
|
||||
|
||||
it('should pass for a nested object', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse({ user: { id: 1, tags: ['a', 'b'] } })
|
||||
);
|
||||
expect(results[0].status).toBe('pass');
|
||||
});
|
||||
|
||||
it('should pass for objects from a different realm (e.g. after res.setBody in node-vm)', async () => {
|
||||
const response = makeResponse({ id: 1, name: 'original' });
|
||||
|
||||
// res.setBody() inside node-vm creates a cross-realm object whose
|
||||
// constructor is the VM's Object, not the host's Object
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: 'nodevm' });
|
||||
await scriptRuntime.runResponseScript(
|
||||
`res.setBody({ id: 2, name: 'updated' });`,
|
||||
{ ...baseRequest },
|
||||
response,
|
||||
{}, {}, '.', null, process.env
|
||||
);
|
||||
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
response
|
||||
);
|
||||
expect(results[0].status).toBe('pass');
|
||||
});
|
||||
|
||||
it('should fail for an array', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse([1, 2, 3])
|
||||
);
|
||||
expect(results[0].status).toBe('fail');
|
||||
});
|
||||
|
||||
it('should fail for a string', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse('hello')
|
||||
);
|
||||
expect(results[0].status).toBe('fail');
|
||||
});
|
||||
|
||||
it('should fail for null', () => {
|
||||
const results = runAssertions(
|
||||
[{ name: 'res.body', value: 'isJson', enabled: true }],
|
||||
makeResponse(null)
|
||||
);
|
||||
expect(results[0].status).toBe('fail');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
meta {
|
||||
name: isJson after setBody
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"hello": "bruno"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
res.body: isJson
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
res.setBody({ id: 1, name: "updated", nested: { key: "value" } });
|
||||
}
|
||||
|
||||
tests {
|
||||
test("res.body should be json after setBody with object", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.be.json;
|
||||
expect(body.id).to.eql(1);
|
||||
expect(body.name).to.eql("updated");
|
||||
expect(body.nested.key).to.eql("value");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user