diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js index ba752bf0d1d..3dd25f818f0 100644 --- a/packages/bruno-js/src/runtime/assert-runtime.js +++ b/packages/bruno-js/src/runtime/assert-runtime.js @@ -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`); }); diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/test.js b/packages/bruno-js/src/sandbox/quickjs/shims/test.js index 9da224a3962..12c6e5058f0 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/test.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/test.js @@ -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; diff --git a/packages/bruno-js/tests/runtime.spec.js b/packages/bruno-js/tests/runtime.spec.js index 797e305982d..2ecc19f4e52 100644 --- a/packages/bruno-js/tests/runtime.spec.js +++ b/packages/bruno-js/tests/runtime.spec.js @@ -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'); + }); + }); + }); }); diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/isJson after setBody.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/isJson after setBody.bru new file mode 100644 index 00000000000..1f858352a2a --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/isJson after setBody.bru @@ -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"); + }); +}