@usebruno/js 0.47.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,8 @@ const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser, u
7
7
  const { interpolateString } = require('../interpolate-string');
8
8
  const { executeQuickJsVm } = require('../sandbox/quickjs');
9
9
 
10
+ const Ajv = require('ajv');
11
+ const addFormats = require('ajv-formats');
10
12
  const { expect } = chai;
11
13
  chai.use(require('chai-string'));
12
14
  chai.use(function (chai, utils) {
@@ -24,6 +26,48 @@ chai.use(function (chai, utils) {
24
26
  });
25
27
  });
26
28
 
29
+ // Custom assertion for JSON Schema validation
30
+ const defaultAjv = new Ajv({ allErrors: true });
31
+ addFormats(defaultAjv);
32
+
33
+ const SUPPORTED_SCHEMA_VERSIONS = [
34
+ 'http://json-schema.org/draft-07/schema#',
35
+ 'http://json-schema.org/draft-07/schema'
36
+ ];
37
+
38
+ chai.use(function (chai) {
39
+ chai.Assertion.addMethod('jsonSchema', function (schema, ajvOptions) {
40
+ if (schema && schema.$schema && !SUPPORTED_SCHEMA_VERSIONS.includes(schema.$schema)) {
41
+ this.assert(
42
+ false,
43
+ `Unsupported JSON Schema version: "${schema.$schema}". Bruno currently only supports Draft-07 (http://json-schema.org/draft-07/schema#). Please update your schema to be Draft-07 compatible and remove the $schema property.`,
44
+ `Unsupported JSON Schema version: "${schema.$schema}".`
45
+ );
46
+ }
47
+ let ajv;
48
+ if (ajvOptions) {
49
+ ajv = new Ajv({ allErrors: true, ...ajvOptions });
50
+ addFormats(ajv);
51
+ } else {
52
+ ajv = defaultAjv;
53
+ }
54
+ let validate;
55
+ try {
56
+ validate = ajv.compile(schema);
57
+ } catch (e) {
58
+ this.assert(false, 'JSON schema compile error: ' + e.message, 'JSON schema compile error: ' + e.message);
59
+ }
60
+ const data = this._obj;
61
+ const isValid = validate(data);
62
+
63
+ this.assert(
64
+ isValid,
65
+ 'expected #{this} to match JSON schema, validation errors: ' + (validate.errors ? JSON.stringify(validate.errors) : 'none'),
66
+ 'expected #{this} to not match JSON schema'
67
+ );
68
+ });
69
+ });
70
+
27
71
  // Custom assertion for matching regex
28
72
  chai.use(function (chai, utils) {
29
73
  chai.Assertion.addMethod('match', function (regex) {
@@ -43,6 +87,118 @@ chai.use(function (chai, utils) {
43
87
  });
44
88
  });
45
89
 
90
+ // Custom assertion for jsonBody (Postman parity)
91
+ chai.use(function (chai, utils) {
92
+ // Parse a property path into an array of keys.
93
+ // Handles: dot notation (a.b), numeric brackets (a[0]), quoted brackets (a["b.c"], a['key']),
94
+ // and combinations like data[0]["a.b"].name
95
+ //
96
+ // Examples:
97
+ // "a.b.c" -> ["a", "b", "c"]
98
+ // "items[0].name" -> ["items", "0", "name"]
99
+ // 'data["a.b"]' -> ["data", "a.b"]
100
+ // "matrix[0][1]" -> ["matrix", "0", "1"]
101
+ // 'nested["x.y"].z' -> ["nested", "x.y", "z"]
102
+ // '["say \\"hi\\""]' -> ["say \"hi\""]
103
+ function parsePath(path) {
104
+ const keys = [];
105
+ let i = 0;
106
+ while (i < path.length) {
107
+ if (path[i] === '.') {
108
+ // Skip dot separator
109
+ i++;
110
+ } else if (path[i] === '[') {
111
+ i++; // skip '['
112
+ if (i < path.length && (path[i] === '\'' || path[i] === '"')) {
113
+ // Quoted key — collect until matching unescaped quote + ']'
114
+ const quote = path[i];
115
+ i++; // skip opening quote
116
+ let key = '';
117
+ while (i < path.length && path[i] !== quote) {
118
+ if (path[i] === '\\' && i + 1 < path.length && path[i + 1] === quote) {
119
+ key += quote;
120
+ i += 2; // skip backslash + escaped quote
121
+ } else {
122
+ key += path[i];
123
+ i++;
124
+ }
125
+ }
126
+ i++; // skip closing quote
127
+ i++; // skip ']'
128
+ keys.push(key);
129
+ } else {
130
+ // Unquoted (numeric) key — collect until ']'
131
+ let key = '';
132
+ while (i < path.length && path[i] !== ']') {
133
+ key += path[i];
134
+ i++;
135
+ }
136
+ i++; // skip ']'
137
+ keys.push(key);
138
+ }
139
+ } else {
140
+ // Bare key — collect until '.', '[', or end
141
+ let key = '';
142
+ while (i < path.length && path[i] !== '.' && path[i] !== '[') {
143
+ key += path[i];
144
+ i++;
145
+ }
146
+ keys.push(key);
147
+ }
148
+ }
149
+ return keys;
150
+ }
151
+
152
+ function getNestedValue(obj, path) {
153
+ const keys = parsePath(path);
154
+ let current = obj;
155
+ for (const key of keys) {
156
+ if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), key)) {
157
+ return { found: false };
158
+ }
159
+ current = current[key];
160
+ }
161
+ return { found: true, value: current };
162
+ }
163
+
164
+ chai.Assertion.addMethod('jsonBody', function () {
165
+ const obj = this._obj;
166
+ const args = Array.prototype.slice.call(arguments);
167
+
168
+ if (args.length === 0) {
169
+ // No args: check body is valid JSON (object or array)
170
+ this.assert(
171
+ typeof obj === 'object' && obj !== null,
172
+ `expected ${utils.inspect(obj)} to be a JSON body (object or array)`,
173
+ `expected ${utils.inspect(obj)} not to be a JSON body`
174
+ );
175
+ } else if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
176
+ // Object arg: deep equality
177
+ this.assert(
178
+ utils.eql(obj, args[0]),
179
+ `expected body to deeply equal ${utils.inspect(args[0])}`,
180
+ `expected body to not deeply equal ${utils.inspect(args[0])}`
181
+ );
182
+ } else if (args.length === 1) {
183
+ // String path: check nested property exists
184
+ const result = getNestedValue(obj, String(args[0]));
185
+ this.assert(
186
+ result.found,
187
+ `expected body to have nested property '${args[0]}'`,
188
+ `expected body to not have nested property '${args[0]}'`
189
+ );
190
+ } else {
191
+ // Path + value: check nested property equals value
192
+ const result = getNestedValue(obj, String(args[0]));
193
+ this.assert(
194
+ result.found && utils.eql(result.value, args[1]),
195
+ `expected body to have nested property '${args[0]}' equal to ${utils.inspect(args[1])}`,
196
+ `expected body to not have nested property '${args[0]}' equal to ${utils.inspect(args[1])}`
197
+ );
198
+ }
199
+ });
200
+ });
201
+
46
202
  /**
47
203
  * Assertion operators
48
204
  *
@@ -8,6 +8,7 @@ const { createBruTestResultMethods } = require('../utils/results');
8
8
  const { runScriptInNodeVm } = require('../sandbox/node-vm');
9
9
  const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
10
10
  const { SANDBOX } = require('../utils/sandbox');
11
+ const { bindRunRequest, createScopeSetter } = require('./scripted-entries');
11
12
 
12
13
  class ScriptRuntime {
13
14
  constructor(props) {
@@ -80,7 +81,8 @@ class ScriptRuntime {
80
81
  test,
81
82
  expect: chai.expect,
82
83
  assert: chai.assert,
83
- __brunoTestResults: __brunoTestResults
84
+ __brunoTestResults: __brunoTestResults,
85
+ __bruSetScope: createScopeSetter(bru)
84
86
  };
85
87
 
86
88
  if (onConsoleLog && typeof onConsoleLog === 'function') {
@@ -98,9 +100,7 @@ class ScriptRuntime {
98
100
  };
99
101
  }
100
102
 
101
- if (runRequestByItemPathname) {
102
- context.bru.runRequest = runRequestByItemPathname;
103
- }
103
+ bindRunRequest(bru, runRequestByItemPathname);
104
104
 
105
105
  // Helper to build the result object for pre-request scripts
106
106
  // Extracted to avoid duplication across runtime branches
@@ -115,7 +115,8 @@ class ScriptRuntime {
115
115
  results: cleanJson(__brunoTestResults.getResults()),
116
116
  nextRequestName: bru.nextRequest,
117
117
  skipRequest: bru.skipRequest,
118
- stopExecution: bru.stopExecution
118
+ stopExecution: bru.stopExecution,
119
+ scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
119
120
  });
120
121
 
121
122
  // Track script errors to attach partial results before re-throwing
@@ -233,7 +234,8 @@ class ScriptRuntime {
233
234
  test,
234
235
  expect: chai.expect,
235
236
  assert: chai.assert,
236
- __brunoTestResults: __brunoTestResults
237
+ __brunoTestResults: __brunoTestResults,
238
+ __bruSetScope: createScopeSetter(bru)
237
239
  };
238
240
 
239
241
  if (onConsoleLog && typeof onConsoleLog === 'function') {
@@ -251,9 +253,7 @@ class ScriptRuntime {
251
253
  };
252
254
  }
253
255
 
254
- if (runRequestByItemPathname) {
255
- context.bru.runRequest = runRequestByItemPathname;
256
- }
256
+ bindRunRequest(bru, runRequestByItemPathname);
257
257
 
258
258
  // Helper to build the result object for post-response scripts
259
259
  // Extracted to avoid duplication across runtime branches
@@ -268,7 +268,8 @@ class ScriptRuntime {
268
268
  results: cleanJson(__brunoTestResults.getResults()),
269
269
  nextRequestName: bru.nextRequest,
270
270
  skipRequest: bru.skipRequest,
271
- stopExecution: bru.stopExecution
271
+ stopExecution: bru.stopExecution,
272
+ scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
272
273
  });
273
274
 
274
275
  // Track script errors to attach partial results before re-throwing
@@ -0,0 +1,16 @@
1
+ // Forwards the caller's bru as a second arg so the host can attribute the call.
2
+ const bindRunRequest = (bru, runRequestByItemPathname) => {
3
+ if (!runRequestByItemPathname) return;
4
+ bru.runRequest = (relativePathname) =>
5
+ runRequestByItemPathname(relativePathname, bru);
6
+ };
7
+
8
+ // Kept off bru to stay out of user-facing autocomplete.
9
+ const createScopeSetter = (bru) => (scope) => {
10
+ bru._currentScope = scope || null;
11
+ };
12
+
13
+ module.exports = {
14
+ bindRunRequest,
15
+ createScopeSetter
16
+ };
@@ -9,6 +9,7 @@ const { runScriptInNodeVm } = require('../sandbox/node-vm');
9
9
  const jsonwebtoken = require('jsonwebtoken');
10
10
  const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
11
11
  const { SANDBOX } = require('../utils/sandbox');
12
+ const { bindRunRequest, createScopeSetter } = require('./scripted-entries');
12
13
 
13
14
  class TestRuntime {
14
15
  constructor(props) {
@@ -84,7 +85,8 @@ class TestRuntime {
84
85
  expect: chai.expect,
85
86
  assert: chai.assert,
86
87
  __brunoTestResults: __brunoTestResults,
87
- jwt: jsonwebtoken
88
+ jwt: jsonwebtoken,
89
+ __bruSetScope: createScopeSetter(bru)
88
90
  };
89
91
 
90
92
  if (onConsoleLog && typeof onConsoleLog === 'function') {
@@ -102,9 +104,7 @@ class TestRuntime {
102
104
  };
103
105
  }
104
106
 
105
- if (runRequestByItemPathname) {
106
- context.bru.runRequest = runRequestByItemPathname;
107
- }
107
+ bindRunRequest(bru, runRequestByItemPathname);
108
108
 
109
109
  let scriptError = null;
110
110
 
@@ -147,7 +147,8 @@ class TestRuntime {
147
147
  persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
148
148
  oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
149
149
  results: cleanJson(__brunoTestResults.getResults()),
150
- nextRequestName: bru.nextRequest
150
+ nextRequestName: bru.nextRequest,
151
+ scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
151
152
  };
152
153
 
153
154
  if (scriptError) {