@usebruno/js 0.45.1 → 0.46.1
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 +5 -4
- package/src/bru.js +110 -18
- package/src/bruno-request.js +21 -19
- package/src/index.js +4 -1
- package/src/runtime/script-runtime.js +107 -62
- package/src/runtime/test-runtime.js +8 -3
- package/src/sandbox/node-vm/console.js +102 -0
- package/src/sandbox/node-vm/index.js +68 -8
- package/src/sandbox/node-vm/utils.js +15 -0
- package/src/sandbox/quickjs/index.js +6 -21
- package/src/sandbox/quickjs/shims/bru.js +108 -3
- package/src/sandbox/quickjs/shims/bruno-request.js +12 -0
- package/src/sandbox/quickjs/shims/console.js +97 -5
- package/src/sandbox/quickjs/shims/lib/axios.js +26 -16
- package/src/sandbox/quickjs/shims/lib/axios.spec.js +495 -0
- package/src/test.js +6 -4
- package/src/utils/error-formatter.js +426 -0
- package/src/utils/error-formatter.spec.js +388 -0
- package/src/utils/sandbox.js +64 -0
- package/src/utils.js +27 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebruno/js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"prepack": "npm run test"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@usebruno/common": "0.
|
|
16
|
+
"@usebruno/common": "0.20.0",
|
|
17
17
|
"@usebruno/query": "0.2.0",
|
|
18
18
|
"ajv": "^8.12.0",
|
|
19
19
|
"ajv-formats": "^2.1.1",
|
|
20
20
|
"atob": "^2.1.2",
|
|
21
|
-
"axios": "
|
|
21
|
+
"axios": "1.13.6",
|
|
22
22
|
"btoa": "^1.2.1",
|
|
23
23
|
"chai": "^4.3.7",
|
|
24
24
|
"chai-string": "^1.5.0",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"tv4": "^1.3.0",
|
|
36
36
|
"uuid": "^9.0.0",
|
|
37
37
|
"xml-formatter": "^3.5.0",
|
|
38
|
-
"xml2js": "^0.6.2"
|
|
38
|
+
"xml2js": "^0.6.2",
|
|
39
|
+
"yaml": "^2.3.4"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@rollup/plugin-commonjs": "^23.0.2",
|
package/src/bru.js
CHANGED
|
@@ -84,12 +84,19 @@ class Bru {
|
|
|
84
84
|
deleteCookie: (url, cookieName, callback) => {
|
|
85
85
|
const interpolatedUrl = this.interpolate(url);
|
|
86
86
|
return cookieJar.deleteCookie(interpolatedUrl, cookieName, callback);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
hasCookie: (url, cookieName, callback) => {
|
|
90
|
+
const interpolatedUrl = this.interpolate(url);
|
|
91
|
+
return cookieJar.hasCookie(interpolatedUrl, cookieName, callback);
|
|
87
92
|
}
|
|
88
93
|
};
|
|
89
94
|
}
|
|
90
95
|
};
|
|
91
96
|
// Holds variables that are marked as persistent by scripts
|
|
92
97
|
this.persistentEnvVariables = {};
|
|
98
|
+
// Holds credential IDs to be reset after script execution
|
|
99
|
+
this.oauth2CredentialsToReset = [];
|
|
93
100
|
this.runner = {
|
|
94
101
|
skipRequest: () => {
|
|
95
102
|
this.skipRequest = true;
|
|
@@ -211,15 +218,6 @@ class Bru {
|
|
|
211
218
|
throw new Error(`Persistent environment variables must be strings. Received ${typeof value} for key "${key}".`);
|
|
212
219
|
}
|
|
213
220
|
|
|
214
|
-
if (this.historyLogger) {
|
|
215
|
-
this.historyLogger({
|
|
216
|
-
uid: uuid(),
|
|
217
|
-
type: 'setEnvVar()',
|
|
218
|
-
data: { key, value },
|
|
219
|
-
createdAt: new Date().toISOString()
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
221
|
this.envVariables[key] = value;
|
|
224
222
|
|
|
225
223
|
if (options?.persist) {
|
|
@@ -235,6 +233,24 @@ class Bru {
|
|
|
235
233
|
delete this.envVariables[key];
|
|
236
234
|
}
|
|
237
235
|
|
|
236
|
+
getAllEnvVars() {
|
|
237
|
+
const vars = Object.assign({}, this.envVariables);
|
|
238
|
+
delete vars.__name__;
|
|
239
|
+
return vars;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
deleteAllEnvVars() {
|
|
243
|
+
const envName = this.envVariables.__name__;
|
|
244
|
+
for (let key in this.envVariables) {
|
|
245
|
+
if (this.envVariables.hasOwnProperty(key)) {
|
|
246
|
+
delete this.envVariables[key];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (envName !== undefined) {
|
|
250
|
+
this.envVariables.__name__ = envName;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
238
254
|
getGlobalEnvVar(key) {
|
|
239
255
|
return this.interpolate(this.globalEnvironmentVariables[key]);
|
|
240
256
|
}
|
|
@@ -247,10 +263,48 @@ class Bru {
|
|
|
247
263
|
this.globalEnvironmentVariables[key] = value;
|
|
248
264
|
}
|
|
249
265
|
|
|
266
|
+
// TODO: deleteGlobalEnvVar works in the request lifecycle but does not update the UI.
|
|
267
|
+
// Re-enable once the UI sync issue is resolved.
|
|
268
|
+
// deleteGlobalEnvVar(key) {
|
|
269
|
+
// delete this.globalEnvironmentVariables[key];
|
|
270
|
+
// }
|
|
271
|
+
|
|
272
|
+
getAllGlobalEnvVars() {
|
|
273
|
+
return Object.assign({}, this.globalEnvironmentVariables);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// TODO: deleteAllGlobalEnvVars works in the request lifecycle but does not update the UI.
|
|
277
|
+
// Re-enable once the UI sync issue is resolved.
|
|
278
|
+
// deleteAllGlobalEnvVars() {
|
|
279
|
+
// for (let key in this.globalEnvironmentVariables) {
|
|
280
|
+
// if (this.globalEnvironmentVariables.hasOwnProperty(key)) {
|
|
281
|
+
// delete this.globalEnvironmentVariables[key];
|
|
282
|
+
// }
|
|
283
|
+
// }
|
|
284
|
+
// }
|
|
285
|
+
|
|
250
286
|
getOauth2CredentialVar(key) {
|
|
251
287
|
return this.interpolate(this.oauth2CredentialVariables[key]);
|
|
252
288
|
}
|
|
253
289
|
|
|
290
|
+
resetOauth2Credential(credentialId) {
|
|
291
|
+
if (!credentialId || typeof credentialId !== 'string') {
|
|
292
|
+
throw new Error('credentialId must be a non-empty string');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!this.oauth2CredentialsToReset.includes(credentialId)) {
|
|
296
|
+
this.oauth2CredentialsToReset.push(credentialId);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Remove matching credential variables so subsequent getOauth2CredentialVar() calls return undefined
|
|
300
|
+
const prefix = `$oauth2.${credentialId}.`;
|
|
301
|
+
for (const key of Object.keys(this.oauth2CredentialVariables)) {
|
|
302
|
+
if (key.startsWith(prefix)) {
|
|
303
|
+
delete this.oauth2CredentialVariables[key];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
254
308
|
hasVar(key) {
|
|
255
309
|
return Object.hasOwn(this.runtimeVariables, key);
|
|
256
310
|
}
|
|
@@ -267,15 +321,6 @@ class Bru {
|
|
|
267
321
|
);
|
|
268
322
|
}
|
|
269
323
|
|
|
270
|
-
if (this.historyLogger) {
|
|
271
|
-
this.historyLogger({
|
|
272
|
-
uid: uuid(),
|
|
273
|
-
type: 'setVar()',
|
|
274
|
-
data: { key, value: value },
|
|
275
|
-
createdAt: new Date().toISOString()
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
324
|
this.runtimeVariables[key] = value;
|
|
280
325
|
}
|
|
281
326
|
|
|
@@ -302,10 +347,57 @@ class Bru {
|
|
|
302
347
|
}
|
|
303
348
|
}
|
|
304
349
|
|
|
350
|
+
getAllVars() {
|
|
351
|
+
return Object.assign({}, this.runtimeVariables);
|
|
352
|
+
}
|
|
353
|
+
|
|
305
354
|
getCollectionVar(key) {
|
|
306
355
|
return this.interpolate(this.collectionVariables[key]);
|
|
307
356
|
}
|
|
308
357
|
|
|
358
|
+
// TODO: setCollectionVar works in the request lifecycle but does not update the UI.
|
|
359
|
+
// Re-enable once the UI sync issue is resolved.
|
|
360
|
+
// setCollectionVar(key, value) {
|
|
361
|
+
// if (!key) {
|
|
362
|
+
// throw new Error('Creating a variable without specifying a name is not allowed.');
|
|
363
|
+
// }
|
|
364
|
+
//
|
|
365
|
+
// if (variableNameRegex.test(key) === false) {
|
|
366
|
+
// throw new Error(
|
|
367
|
+
// `Variable name: "${key}" contains invalid characters!`
|
|
368
|
+
// + ' Names must only contain alpha-numeric characters, "-", "_", "."'
|
|
369
|
+
// );
|
|
370
|
+
// }
|
|
371
|
+
//
|
|
372
|
+
// this.collectionVariables[key] = value;
|
|
373
|
+
// }
|
|
374
|
+
|
|
375
|
+
hasCollectionVar(key) {
|
|
376
|
+
return Object.hasOwn(this.collectionVariables, key);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// TODO: deleteCollectionVar works in the request lifecycle but does not update the UI.
|
|
380
|
+
// Re-enable once the UI sync issue is resolved.
|
|
381
|
+
// deleteCollectionVar(key) {
|
|
382
|
+
// delete this.collectionVariables[key];
|
|
383
|
+
// }
|
|
384
|
+
|
|
385
|
+
// TODO: deleteAllCollectionVars works in the request lifecycle but does not update the UI.
|
|
386
|
+
// Re-enable once the UI sync issue is resolved.
|
|
387
|
+
// deleteAllCollectionVars() {
|
|
388
|
+
// for (let key in this.collectionVariables) {
|
|
389
|
+
// if (this.collectionVariables.hasOwnProperty(key)) {
|
|
390
|
+
// delete this.collectionVariables[key];
|
|
391
|
+
// }
|
|
392
|
+
// }
|
|
393
|
+
// }
|
|
394
|
+
|
|
395
|
+
// TODO: getAllCollectionVars works in the request lifecycle but does not update the UI.
|
|
396
|
+
// Re-enable once the UI sync issue is resolved.
|
|
397
|
+
// getAllCollectionVars() {
|
|
398
|
+
// return Object.assign({}, this.collectionVariables);
|
|
399
|
+
// }
|
|
400
|
+
|
|
309
401
|
getFolderVar(key) {
|
|
310
402
|
return this.interpolate(this.folderVariables[key]);
|
|
311
403
|
}
|
package/src/bruno-request.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const { uuid } = require('./utils');
|
|
2
|
-
|
|
3
1
|
class BrunoRequest {
|
|
4
2
|
/**
|
|
5
3
|
* The following properties are available as shorthand:
|
|
@@ -129,23 +127,36 @@ class BrunoRequest {
|
|
|
129
127
|
this.req.headers = headers;
|
|
130
128
|
}
|
|
131
129
|
|
|
130
|
+
deleteHeaders(headers) {
|
|
131
|
+
headers.forEach((name) => this.deleteHeader(name));
|
|
132
|
+
}
|
|
133
|
+
|
|
132
134
|
getHeader(name) {
|
|
133
135
|
return this.req.headers[name];
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
setHeader(name, value) {
|
|
137
|
-
if (this.historyLogger) {
|
|
138
|
-
this.historyLogger({
|
|
139
|
-
uid: uuid(),
|
|
140
|
-
type: 'setHeader()',
|
|
141
|
-
data: { name, value },
|
|
142
|
-
createdAt: new Date().toISOString()
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
139
|
this.headers[name] = value;
|
|
146
140
|
this.req.headers[name] = value;
|
|
147
141
|
}
|
|
148
142
|
|
|
143
|
+
deleteHeader(name) {
|
|
144
|
+
delete this.headers[name];
|
|
145
|
+
delete this.req.headers[name];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
Store header name to be applied in the axios request interceptor.
|
|
149
|
+
Default headers (user-agent, accept, accept-encoding, etc.) are added after
|
|
150
|
+
the pre-request script runs, so we track them here and delete them later.
|
|
151
|
+
*/
|
|
152
|
+
if (!this.req.__headersToDelete) {
|
|
153
|
+
this.req.__headersToDelete = [];
|
|
154
|
+
}
|
|
155
|
+
if (!this.req.__headersToDelete.includes(name)) {
|
|
156
|
+
this.req.__headersToDelete.push(name);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
149
160
|
hasJSONContentType(headers) {
|
|
150
161
|
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
|
|
151
162
|
return contentType.includes('json');
|
|
@@ -181,15 +192,6 @@ class BrunoRequest {
|
|
|
181
192
|
* If the user wants to override this behavior, they can pass the raw option as true
|
|
182
193
|
*/
|
|
183
194
|
setBody(data, options = {}) {
|
|
184
|
-
if (this.historyLogger) {
|
|
185
|
-
this.historyLogger({
|
|
186
|
-
uid: uuid(),
|
|
187
|
-
type: 'setBody()',
|
|
188
|
-
data: { body: data },
|
|
189
|
-
createdAt: new Date().toISOString()
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
195
|
if (options.raw) {
|
|
194
196
|
this.req.data = data;
|
|
195
197
|
this.body = data;
|
package/src/index.js
CHANGED
|
@@ -3,11 +3,14 @@ const TestRuntime = require('./runtime/test-runtime');
|
|
|
3
3
|
const VarsRuntime = require('./runtime/vars-runtime');
|
|
4
4
|
const AssertRuntime = require('./runtime/assert-runtime');
|
|
5
5
|
const { runScriptInNodeVm } = require('./sandbox/node-vm');
|
|
6
|
+
const { formatErrorWithContext, SCRIPT_TYPES } = require('./utils/error-formatter');
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
ScriptRuntime,
|
|
9
10
|
TestRuntime,
|
|
10
11
|
VarsRuntime,
|
|
11
12
|
AssertRuntime,
|
|
12
|
-
runScriptInNodeVm
|
|
13
|
+
runScriptInNodeVm,
|
|
14
|
+
formatErrorWithContext,
|
|
15
|
+
SCRIPT_TYPES
|
|
13
16
|
};
|
|
@@ -6,6 +6,7 @@ const { cleanJson } = require('../utils');
|
|
|
6
6
|
const { createBruTestResultMethods } = require('../utils/results');
|
|
7
7
|
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
8
8
|
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
|
9
|
+
const { SANDBOX } = require('../utils/sandbox');
|
|
9
10
|
|
|
10
11
|
class ScriptRuntime {
|
|
11
12
|
constructor(props) {
|
|
@@ -41,6 +42,7 @@ class ScriptRuntime {
|
|
|
41
42
|
const iterationDetails = request?.runnerIterationDetails || {};
|
|
42
43
|
const assertionResults = request?.assertionResults || [];
|
|
43
44
|
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
|
45
|
+
const scriptPath = request?.pathname;
|
|
44
46
|
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables, certsAndProxyConfig);
|
|
45
47
|
const req = new BrunoRequest(request);
|
|
46
48
|
|
|
@@ -75,47 +77,68 @@ class ScriptRuntime {
|
|
|
75
77
|
context.bru.runRequest = runRequestByItemPathname;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
context,
|
|
82
|
-
collectionPath,
|
|
83
|
-
scriptingConfig
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
request,
|
|
88
|
-
envVariables: cleanJson(envVariables),
|
|
89
|
-
runtimeVariables: cleanJson(runtimeVariables),
|
|
90
|
-
visualizations,
|
|
91
|
-
persistentEnvVariables: bru.persistentEnvVariables,
|
|
92
|
-
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
|
93
|
-
results: cleanJson(__brunoTestResults.getResults()),
|
|
94
|
-
nextRequestName: bru.nextRequest,
|
|
95
|
-
skipRequest: bru.skipRequest,
|
|
96
|
-
stopExecution: bru.stopExecution
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// default runtime is `quickjs`
|
|
101
|
-
await executeQuickJsVmAsync({
|
|
102
|
-
script: script,
|
|
103
|
-
context: context,
|
|
104
|
-
collectionPath
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
return {
|
|
80
|
+
// Helper to build the result object for pre-request scripts
|
|
81
|
+
// Extracted to avoid duplication across runtime branches
|
|
82
|
+
const buildRequestScriptResult = () => ({
|
|
108
83
|
request,
|
|
109
84
|
envVariables: cleanJson(envVariables),
|
|
110
85
|
runtimeVariables: cleanJson(runtimeVariables),
|
|
111
86
|
visualizations,
|
|
112
87
|
persistentEnvVariables: bru.persistentEnvVariables,
|
|
113
88
|
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
|
89
|
+
oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
|
|
114
90
|
results: cleanJson(__brunoTestResults.getResults()),
|
|
115
91
|
nextRequestName: bru.nextRequest,
|
|
116
92
|
skipRequest: bru.skipRequest,
|
|
117
93
|
stopExecution: bru.stopExecution
|
|
118
|
-
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Track script errors to attach partial results before re-throwing
|
|
97
|
+
// This ensures that any test() calls that passed before the error are preserved
|
|
98
|
+
// Similar pattern to test-runtime.js which already handles this correctly
|
|
99
|
+
let scriptError = null;
|
|
100
|
+
|
|
101
|
+
if (this.runtime === SANDBOX.NODEVM) {
|
|
102
|
+
try {
|
|
103
|
+
await runScriptInNodeVm({
|
|
104
|
+
script,
|
|
105
|
+
context,
|
|
106
|
+
collectionPath,
|
|
107
|
+
scriptingConfig,
|
|
108
|
+
scriptPath
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
scriptError = error;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// If script errored, attach partial results so callers can display passed tests
|
|
115
|
+
// before the error occurred (e.g., 2 tests pass, then script throws)
|
|
116
|
+
if (scriptError) {
|
|
117
|
+
scriptError.partialResults = buildRequestScriptResult();
|
|
118
|
+
throw scriptError;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return buildRequestScriptResult();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// default runtime is `quickjs`
|
|
125
|
+
try {
|
|
126
|
+
await executeQuickJsVmAsync({
|
|
127
|
+
script: script,
|
|
128
|
+
context: context,
|
|
129
|
+
collectionPath,
|
|
130
|
+
scriptPath
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
scriptError = error;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (scriptError) {
|
|
137
|
+
scriptError.partialResults = buildRequestScriptResult();
|
|
138
|
+
throw scriptError;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return buildRequestScriptResult();
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
async runResponseScript(
|
|
@@ -146,6 +169,7 @@ class ScriptRuntime {
|
|
|
146
169
|
const iterationDetails = request?.runnerIterationDetails || {};
|
|
147
170
|
const assertionResults = request?.assertionResults || [];
|
|
148
171
|
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
|
172
|
+
const scriptPath = request?.pathname;
|
|
149
173
|
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables, certsAndProxyConfig);
|
|
150
174
|
const req = new BrunoRequest(request);
|
|
151
175
|
const res = new BrunoResponse(response);
|
|
@@ -182,47 +206,68 @@ class ScriptRuntime {
|
|
|
182
206
|
context.bru.runRequest = runRequestByItemPathname;
|
|
183
207
|
}
|
|
184
208
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
context,
|
|
189
|
-
collectionPath,
|
|
190
|
-
scriptingConfig
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
response,
|
|
195
|
-
envVariables: cleanJson(envVariables),
|
|
196
|
-
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
|
|
197
|
-
runtimeVariables: cleanJson(runtimeVariables),
|
|
198
|
-
visualizations,
|
|
199
|
-
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
|
200
|
-
results: cleanJson(__brunoTestResults.getResults()),
|
|
201
|
-
nextRequestName: bru.nextRequest,
|
|
202
|
-
skipRequest: bru.skipRequest,
|
|
203
|
-
stopExecution: bru.stopExecution
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// default runtime is `quickjs`
|
|
208
|
-
await executeQuickJsVmAsync({
|
|
209
|
-
script: script,
|
|
210
|
-
context: context,
|
|
211
|
-
collectionPath
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
return {
|
|
209
|
+
// Helper to build the result object for post-response scripts
|
|
210
|
+
// Extracted to avoid duplication across runtime branches
|
|
211
|
+
const buildResponseScriptResult = () => ({
|
|
215
212
|
response,
|
|
216
213
|
envVariables: cleanJson(envVariables),
|
|
217
214
|
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
|
|
218
215
|
runtimeVariables: cleanJson(runtimeVariables),
|
|
219
216
|
visualizations,
|
|
220
217
|
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
|
218
|
+
oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
|
|
221
219
|
results: cleanJson(__brunoTestResults.getResults()),
|
|
222
220
|
nextRequestName: bru.nextRequest,
|
|
223
221
|
skipRequest: bru.skipRequest,
|
|
224
222
|
stopExecution: bru.stopExecution
|
|
225
|
-
};
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Track script errors to attach partial results before re-throwing
|
|
226
|
+
// This ensures that any test() calls that passed before the error are preserved
|
|
227
|
+
// Similar pattern to test-runtime.js which already handles this correctly
|
|
228
|
+
let scriptError = null;
|
|
229
|
+
|
|
230
|
+
if (this.runtime === SANDBOX.NODEVM) {
|
|
231
|
+
try {
|
|
232
|
+
await runScriptInNodeVm({
|
|
233
|
+
script,
|
|
234
|
+
context,
|
|
235
|
+
collectionPath,
|
|
236
|
+
scriptingConfig,
|
|
237
|
+
scriptPath
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
scriptError = error;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// If script errored, attach partial results so callers can display passed tests
|
|
244
|
+
// before the error occurred (e.g., 2 tests pass, then script throws)
|
|
245
|
+
if (scriptError) {
|
|
246
|
+
scriptError.partialResults = buildResponseScriptResult();
|
|
247
|
+
throw scriptError;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return buildResponseScriptResult();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// default runtime is `quickjs`
|
|
254
|
+
try {
|
|
255
|
+
await executeQuickJsVmAsync({
|
|
256
|
+
script: script,
|
|
257
|
+
context: context,
|
|
258
|
+
collectionPath,
|
|
259
|
+
scriptPath
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
scriptError = error;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (scriptError) {
|
|
266
|
+
scriptError.partialResults = buildResponseScriptResult();
|
|
267
|
+
throw scriptError;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return buildResponseScriptResult();
|
|
226
271
|
}
|
|
227
272
|
}
|
|
228
273
|
|
|
@@ -8,6 +8,7 @@ const { createBruTestResultMethods } = require('../utils/results');
|
|
|
8
8
|
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
9
9
|
const jsonwebtoken = require('jsonwebtoken');
|
|
10
10
|
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
|
11
|
+
const { SANDBOX } = require('../utils/sandbox');
|
|
11
12
|
|
|
12
13
|
class TestRuntime {
|
|
13
14
|
constructor(props) {
|
|
@@ -38,6 +39,7 @@ class TestRuntime {
|
|
|
38
39
|
const iterationDetails = request?.runnerIterationDetails || {};
|
|
39
40
|
const assertionResults = request?.assertionResults || [];
|
|
40
41
|
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
|
42
|
+
const scriptPath = request?.pathname;
|
|
41
43
|
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables, certsAndProxyConfig);
|
|
42
44
|
const req = new BrunoRequest(request, historyLogger);
|
|
43
45
|
const res = new BrunoResponse(response);
|
|
@@ -89,19 +91,21 @@ class TestRuntime {
|
|
|
89
91
|
let scriptError = null;
|
|
90
92
|
|
|
91
93
|
try {
|
|
92
|
-
if (this.runtime ===
|
|
94
|
+
if (this.runtime === SANDBOX.NODEVM) {
|
|
93
95
|
await runScriptInNodeVm({
|
|
94
96
|
script: testsFile,
|
|
95
97
|
context,
|
|
96
98
|
collectionPath,
|
|
97
|
-
scriptingConfig
|
|
99
|
+
scriptingConfig,
|
|
100
|
+
scriptPath
|
|
98
101
|
});
|
|
99
102
|
} else {
|
|
100
103
|
// default runtime is `quickjs`
|
|
101
104
|
await executeQuickJsVmAsync({
|
|
102
105
|
script: testsFile,
|
|
103
106
|
context: context,
|
|
104
|
-
collectionPath
|
|
107
|
+
collectionPath,
|
|
108
|
+
scriptPath
|
|
105
109
|
});
|
|
106
110
|
}
|
|
107
111
|
} catch (error) {
|
|
@@ -123,6 +127,7 @@ class TestRuntime {
|
|
|
123
127
|
runtimeVariables: cleanJson(runtimeVariables),
|
|
124
128
|
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
|
125
129
|
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
|
|
130
|
+
oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
|
|
126
131
|
results: cleanJson(__brunoTestResults.getResults()),
|
|
127
132
|
nextRequestName: bru.nextRequest
|
|
128
133
|
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets the type tag of a value using Object.prototype.toString
|
|
3
|
+
* This works across VM context boundaries unlike instanceof
|
|
4
|
+
* @param {*} value - The value to check
|
|
5
|
+
* @returns {string} The type tag (e.g., 'Set', 'Map', 'Array', 'Object')
|
|
6
|
+
*/
|
|
7
|
+
function getTypeTag(value) {
|
|
8
|
+
return Object.prototype.toString.call(value).slice(8, -1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Transforms a value, converting Set and Map to a special format for display
|
|
13
|
+
* Uses Object.prototype.toString for cross-context type detection
|
|
14
|
+
* @param {*} value - The value to transform
|
|
15
|
+
* @param {WeakSet} seen - Set of already visited objects for circular ref detection
|
|
16
|
+
* @returns {*} Transformed value with Set/Map converted to __brunoType format
|
|
17
|
+
*/
|
|
18
|
+
function transformValue(value, seen = new WeakSet()) {
|
|
19
|
+
// Return primitives as-is
|
|
20
|
+
if (value === null || value === undefined || typeof value !== 'object' && typeof value !== 'function') {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Circular reference check for objects
|
|
25
|
+
if (typeof value === 'object') {
|
|
26
|
+
if (seen.has(value)) {
|
|
27
|
+
return '[Circular]';
|
|
28
|
+
}
|
|
29
|
+
seen.add(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const typeTag = getTypeTag(value);
|
|
33
|
+
|
|
34
|
+
if (typeTag === 'Set') {
|
|
35
|
+
return {
|
|
36
|
+
__brunoType: 'Set',
|
|
37
|
+
__brunoValue: Array.from(value).map((item) => transformValue(item, seen))
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeTag === 'Map') {
|
|
42
|
+
return {
|
|
43
|
+
__brunoType: 'Map',
|
|
44
|
+
__brunoValue: Array.from(value.entries()).map(([k, v]) => [
|
|
45
|
+
transformValue(k, seen),
|
|
46
|
+
transformValue(v, seen)
|
|
47
|
+
])
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeTag === 'Array') {
|
|
52
|
+
return value.map((item) => transformValue(item, seen));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeTag === 'Object') {
|
|
56
|
+
const transformed = {};
|
|
57
|
+
for (const [key, val] of Object.entries(value)) {
|
|
58
|
+
transformed[key] = transformValue(val, seen);
|
|
59
|
+
}
|
|
60
|
+
return transformed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle functions - show clean wrapper
|
|
64
|
+
if (typeTag === 'Function' || typeof value === 'function') {
|
|
65
|
+
const name = value.name || 'anonymous';
|
|
66
|
+
return `function ${name}() {\n [native code]\n}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle other built-in types (Date, RegExp, Error, etc.) - convert to string representation
|
|
70
|
+
try {
|
|
71
|
+
return value?.toString?.() ?? String(value);
|
|
72
|
+
} catch {
|
|
73
|
+
return `[${typeTag}]`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wraps a console object to add Set/Map support for logging
|
|
79
|
+
* @param {Object} originalConsole - The original console object
|
|
80
|
+
* @returns {Object} Wrapped console with Set/Map transformation
|
|
81
|
+
*/
|
|
82
|
+
function wrapConsoleWithSerializers(originalConsole) {
|
|
83
|
+
if (!originalConsole) return originalConsole;
|
|
84
|
+
|
|
85
|
+
const methodsToWrap = ['log', 'debug', 'info', 'warn', 'error'];
|
|
86
|
+
const wrappedConsole = { ...originalConsole };
|
|
87
|
+
|
|
88
|
+
for (const method of methodsToWrap) {
|
|
89
|
+
if (typeof originalConsole[method] === 'function') {
|
|
90
|
+
wrappedConsole[method] = (...args) => {
|
|
91
|
+
const transformedArgs = args.map((arg) => transformValue(arg));
|
|
92
|
+
originalConsole[method](...transformedArgs);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return wrappedConsole;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
wrapConsoleWithSerializers
|
|
102
|
+
};
|