@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.
- package/package.json +6 -4
- package/src/bru.js +26 -3
- package/src/bruno-request.js +13 -7
- package/src/bruno-response.js +4 -0
- package/src/header-list.js +497 -0
- package/src/runtime/assert-runtime.js +156 -0
- package/src/runtime/script-runtime.js +11 -10
- package/src/runtime/scripted-entries.js +16 -0
- package/src/runtime/test-runtime.js +6 -5
- package/src/sandbox/bundle-browser-rollup.js +72 -66
- package/src/sandbox/bundle-libraries.js +9 -1
- package/src/sandbox/quickjs/index.js +27 -13
- package/src/sandbox/quickjs/shims/bru.js +16 -0
- package/src/sandbox/quickjs/shims/bruno-request.js +24 -3
- package/src/sandbox/quickjs/shims/bruno-response.js +28 -3
- package/src/sandbox/quickjs/shims/test.js +173 -0
- package/src/sandbox/quickjs/utils/index.js +131 -1
- package/src/sandbox/quickjs/utils/property-list-bridge.js +30 -5
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|