@usebruno/js 0.43.0 → 0.44.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 +4 -7
- package/src/bru.js +7 -10
- package/src/bruno-request.js +8 -8
- package/src/bruno-response.js +1 -2
- package/src/runtime/assert-runtime.js +1 -1
- package/src/runtime/script-runtime.js +14 -204
- package/src/runtime/test-runtime.js +10 -105
- package/src/runtime/vars-runtime.js +1 -1
- package/src/sandbox/node-vm/index.js +79 -33
- package/src/sandbox/node-vm/index.spec.js +252 -0
- package/src/sandbox/quickjs/index.js +2 -2
- package/src/sandbox/quickjs/shims/bru.js +3 -3
- package/src/sandbox/quickjs/shims/lib/crypto-utils.js +7 -9
- package/src/sandbox/quickjs/shims/lib/crypto-utils.spec.js +6 -6
- package/src/sandbox/quickjs/shims/lib/utils.js +1 -1
- package/src/utils/results.js +4 -4
- package/src/utils.js +6 -6
package/package.json
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebruno/js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"src",
|
|
8
8
|
"package.json"
|
|
9
9
|
],
|
|
10
|
-
"peerDependencies": {
|
|
11
|
-
"@usebruno/vm2": "^3.9.13"
|
|
12
|
-
},
|
|
13
10
|
"scripts": {
|
|
14
11
|
"test": "node --experimental-vm-modules $(npx which jest) --testPathIgnorePatterns test.js",
|
|
15
12
|
"sandbox:bundle-libraries": "node ./src/sandbox/bundle-libraries.js",
|
|
16
13
|
"prepack": "npm run test"
|
|
17
14
|
},
|
|
18
15
|
"dependencies": {
|
|
19
|
-
"@usebruno/common": "0.
|
|
20
|
-
"@usebruno/query": "0.
|
|
16
|
+
"@usebruno/common": "0.17.0",
|
|
17
|
+
"@usebruno/query": "0.2.0",
|
|
21
18
|
"ajv": "^8.12.0",
|
|
22
19
|
"ajv-formats": "^2.1.1",
|
|
23
20
|
"atob": "^2.1.2",
|
|
@@ -28,7 +25,7 @@
|
|
|
28
25
|
"cheerio": "^1.0.0",
|
|
29
26
|
"crypto-js": "^4.2.0",
|
|
30
27
|
"json-query": "^2.2.2",
|
|
31
|
-
"jsonwebtoken": "^9.0.
|
|
28
|
+
"jsonwebtoken": "^9.0.3",
|
|
32
29
|
"lodash": "^4.17.21",
|
|
33
30
|
"moment": "^2.29.4",
|
|
34
31
|
"nanoid": "3.3.8",
|
package/src/bru.js
CHANGED
|
@@ -27,7 +27,7 @@ class Bru {
|
|
|
27
27
|
this.cookies = {
|
|
28
28
|
jar: () => {
|
|
29
29
|
const cookieJar = createCookieJar();
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
return {
|
|
32
32
|
getCookie: (url, cookieName, callback) => {
|
|
33
33
|
const interpolatedUrl = this.interpolate(url);
|
|
@@ -82,7 +82,7 @@ class Bru {
|
|
|
82
82
|
iterationData: new IterationDataManager(iterationDetails?.iterationData),
|
|
83
83
|
iterationIndex: iterationDetails?.iterationIndex,
|
|
84
84
|
totalIterations: iterationDetails?.totalIterations
|
|
85
|
-
};
|
|
85
|
+
};
|
|
86
86
|
|
|
87
87
|
this.utils = {
|
|
88
88
|
minifyJson: (json) => {
|
|
@@ -128,7 +128,6 @@ class Bru {
|
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
-
|
|
132
131
|
|
|
133
132
|
interpolate = (strOrObj) => {
|
|
134
133
|
if (!strOrObj) return strOrObj;
|
|
@@ -179,7 +178,7 @@ class Bru {
|
|
|
179
178
|
if (!key) {
|
|
180
179
|
throw new Error('Creating a env variable without specifying a name is not allowed.');
|
|
181
180
|
}
|
|
182
|
-
|
|
181
|
+
|
|
183
182
|
if (variableNameRegex.test(key) === false) {
|
|
184
183
|
throw new Error(
|
|
185
184
|
`Variable name: "${key}" contains invalid characters! Names must only contain alpha-numeric characters, "-", "_", "."`
|
|
@@ -242,8 +241,8 @@ class Bru {
|
|
|
242
241
|
|
|
243
242
|
if (variableNameRegex.test(key) === false) {
|
|
244
243
|
throw new Error(
|
|
245
|
-
`Variable name: "${key}" contains invalid characters!`
|
|
246
|
-
|
|
244
|
+
`Variable name: "${key}" contains invalid characters!`
|
|
245
|
+
+ ' Names must only contain alpha-numeric characters, "-", "_", "."'
|
|
247
246
|
);
|
|
248
247
|
}
|
|
249
248
|
|
|
@@ -262,8 +261,8 @@ class Bru {
|
|
|
262
261
|
getVar(key) {
|
|
263
262
|
if (variableNameRegex.test(key) === false) {
|
|
264
263
|
throw new Error(
|
|
265
|
-
`Variable name: "${key}" contains invalid characters!`
|
|
266
|
-
|
|
264
|
+
`Variable name: "${key}" contains invalid characters!`
|
|
265
|
+
+ ' Names must only contain alpha-numeric characters, "-", "_", "."'
|
|
267
266
|
);
|
|
268
267
|
}
|
|
269
268
|
|
|
@@ -298,12 +297,10 @@ class Bru {
|
|
|
298
297
|
this.nextRequest = nextRequest;
|
|
299
298
|
}
|
|
300
299
|
|
|
301
|
-
|
|
302
300
|
getSecretVar(key) {
|
|
303
301
|
return this.secretVariables?.[`$secrets.${key}`];
|
|
304
302
|
}
|
|
305
303
|
|
|
306
|
-
|
|
307
304
|
visualize(type, data) {
|
|
308
305
|
if (type == 'table') {
|
|
309
306
|
if (data?.provider == 'ag-grid') {
|
package/src/bruno-request.js
CHANGED
|
@@ -8,7 +8,7 @@ class BrunoRequest {
|
|
|
8
8
|
* - req.headers
|
|
9
9
|
* - req.timeout
|
|
10
10
|
* - req.body
|
|
11
|
-
*
|
|
11
|
+
*
|
|
12
12
|
* Above shorthands are useful for accessing the request properties directly in the scripts
|
|
13
13
|
* It must be noted that the user cannot set these properties directly.
|
|
14
14
|
* They should use the respective setter methods to set these properties.
|
|
@@ -26,9 +26,9 @@ class BrunoRequest {
|
|
|
26
26
|
/**
|
|
27
27
|
* We automatically parse the JSON body if the content type is JSON
|
|
28
28
|
* This is to make it easier for the user to access the body directly
|
|
29
|
-
*
|
|
29
|
+
*
|
|
30
30
|
* It must be noted that the request data is always a string and is what gets sent over the network
|
|
31
|
-
* If the user wants to access the raw data, they can use getBody({raw: true}) method
|
|
31
|
+
* If the user wants to access the raw data, they can use getBody({raw: true}) method
|
|
32
32
|
*/
|
|
33
33
|
const isJson = this.hasJSONContentType(this.req.headers);
|
|
34
34
|
if (isJson) {
|
|
@@ -48,6 +48,7 @@ class BrunoRequest {
|
|
|
48
48
|
getMethod() {
|
|
49
49
|
return this.req.method;
|
|
50
50
|
}
|
|
51
|
+
|
|
51
52
|
getAuthMode() {
|
|
52
53
|
if (this.req?.oauth2) {
|
|
53
54
|
return 'oauth2';
|
|
@@ -104,7 +105,7 @@ class BrunoRequest {
|
|
|
104
105
|
|
|
105
106
|
/**
|
|
106
107
|
* Get the body of the request
|
|
107
|
-
*
|
|
108
|
+
*
|
|
108
109
|
* We automatically parse and return the JSON body if the content type is JSON
|
|
109
110
|
* If the user wants the raw body, they can pass the raw option as true
|
|
110
111
|
*/
|
|
@@ -128,7 +129,7 @@ class BrunoRequest {
|
|
|
128
129
|
* Otherwise
|
|
129
130
|
* - We set the request data as the data itself
|
|
130
131
|
* - We set the body property as the data itself
|
|
131
|
-
*
|
|
132
|
+
*
|
|
132
133
|
* If the user wants to override this behavior, they can pass the raw option as true
|
|
133
134
|
*/
|
|
134
135
|
setBody(data, options = {}) {
|
|
@@ -140,7 +141,7 @@ class BrunoRequest {
|
|
|
140
141
|
createdAt: new Date().toISOString()
|
|
141
142
|
});
|
|
142
143
|
}
|
|
143
|
-
|
|
144
|
+
|
|
144
145
|
if (options.raw) {
|
|
145
146
|
this.req.data = data;
|
|
146
147
|
this.body = data;
|
|
@@ -170,7 +171,7 @@ class BrunoRequest {
|
|
|
170
171
|
this.timeout = timeout;
|
|
171
172
|
this.req.timeout = timeout;
|
|
172
173
|
}
|
|
173
|
-
|
|
174
|
+
|
|
174
175
|
onFail(callback) {
|
|
175
176
|
if (typeof callback === 'function') {
|
|
176
177
|
this.req.onFailHandler = callback;
|
|
@@ -198,7 +199,6 @@ class BrunoRequest {
|
|
|
198
199
|
__isObject(obj) {
|
|
199
200
|
return obj !== null && typeof obj === 'object';
|
|
200
201
|
}
|
|
201
|
-
|
|
202
202
|
|
|
203
203
|
disableParsingResponseJson() {
|
|
204
204
|
this.req.__brunoDisableParsingResponseJson = true;
|
package/src/bruno-response.js
CHANGED
|
@@ -65,7 +65,7 @@ class BrunoResponse {
|
|
|
65
65
|
|
|
66
66
|
const { data, dataBuffer, headers } = this.res;
|
|
67
67
|
let bodySize = 0;
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
// Use raw received bytes
|
|
70
70
|
if (Buffer.isBuffer(dataBuffer)) {
|
|
71
71
|
bodySize = dataBuffer.length;
|
|
@@ -94,7 +94,6 @@ class BrunoResponse {
|
|
|
94
94
|
const headerSize = Buffer.byteLength(headerLines.join('\r\n'));
|
|
95
95
|
|
|
96
96
|
return { header: headerSize, body: bodySize, total: headerSize + bodySize };
|
|
97
|
-
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
getDataBuffer() {
|
|
@@ -241,7 +241,7 @@ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
|
|
|
241
241
|
|
|
242
242
|
class AssertRuntime {
|
|
243
243
|
constructor(props) {
|
|
244
|
-
this.runtime = props?.runtime || '
|
|
244
|
+
this.runtime = props?.runtime || 'quickjs';
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars, historyLogger, secretVariables) {
|
|
@@ -1,45 +1,15 @@
|
|
|
1
|
-
const
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const http = require('http');
|
|
4
|
-
const https = require('https');
|
|
5
|
-
const stream = require('stream');
|
|
6
|
-
const util = require('util');
|
|
7
|
-
const zlib = require('zlib');
|
|
8
|
-
const url = require('url');
|
|
9
|
-
const punycode = require('punycode');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const { get } = require('lodash');
|
|
1
|
+
const chai = require('chai');
|
|
12
2
|
const Bru = require('../bru');
|
|
13
3
|
const BrunoRequest = require('../bruno-request');
|
|
14
4
|
const BrunoResponse = require('../bruno-response');
|
|
15
5
|
const { cleanJson } = require('../utils');
|
|
16
6
|
const { createBruTestResultMethods } = require('../utils/results');
|
|
17
7
|
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
18
|
-
|
|
19
|
-
// Inbuilt Library Support
|
|
20
|
-
const ajv = require('ajv');
|
|
21
|
-
const addFormats = require('ajv-formats');
|
|
22
|
-
const atob = require('atob');
|
|
23
|
-
const btoa = require('btoa');
|
|
24
|
-
const lodash = require('lodash');
|
|
25
|
-
const moment = require('moment');
|
|
26
|
-
const uuid = require('uuid');
|
|
27
|
-
const nanoid = require('nanoid');
|
|
28
|
-
const axios = require('axios');
|
|
29
|
-
const fetch = require('node-fetch');
|
|
30
|
-
const chai = require('chai');
|
|
31
|
-
const CryptoJS = require('crypto-js');
|
|
32
|
-
const NodeVault = require('node-vault');
|
|
33
|
-
const xml2js = require('xml2js');
|
|
34
|
-
const cheerio = require('cheerio');
|
|
35
|
-
const tv4 = require('tv4');
|
|
36
|
-
const jsonwebtoken = require('jsonwebtoken');
|
|
37
8
|
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
|
38
|
-
const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
|
|
39
9
|
|
|
40
10
|
class ScriptRuntime {
|
|
41
11
|
constructor(props) {
|
|
42
|
-
this.runtime = props?.runtime || '
|
|
12
|
+
this.runtime = props?.runtime || 'quickjs';
|
|
43
13
|
}
|
|
44
14
|
|
|
45
15
|
// This approach is getting out of hand
|
|
@@ -61,7 +31,7 @@ class ScriptRuntime {
|
|
|
61
31
|
let visualizations = [];
|
|
62
32
|
let setVisualizations = (data) => {
|
|
63
33
|
visualizations.push(data);
|
|
64
|
-
}
|
|
34
|
+
};
|
|
65
35
|
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
|
66
36
|
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
|
67
37
|
const collectionVariables = request?.collectionVariables || {};
|
|
@@ -73,24 +43,6 @@ class ScriptRuntime {
|
|
|
73
43
|
// TODO please clean this up
|
|
74
44
|
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables);
|
|
75
45
|
const req = new BrunoRequest(request);
|
|
76
|
-
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
|
77
|
-
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
|
78
|
-
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
79
|
-
const additionalContextRootsAbsolute = lodash
|
|
80
|
-
.chain(additionalContextRoots)
|
|
81
|
-
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
|
|
82
|
-
.value();
|
|
83
|
-
|
|
84
|
-
const whitelistedModules = {};
|
|
85
|
-
|
|
86
|
-
for (let module of moduleWhitelist) {
|
|
87
|
-
try {
|
|
88
|
-
whitelistedModules[module] = require(module);
|
|
89
|
-
} catch (e) {
|
|
90
|
-
// Ignore
|
|
91
|
-
console.warn(e);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
46
|
|
|
95
47
|
// extend bru with result getter methods
|
|
96
48
|
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
|
|
@@ -104,10 +56,6 @@ class ScriptRuntime {
|
|
|
104
56
|
__brunoTestResults: __brunoTestResults
|
|
105
57
|
};
|
|
106
58
|
|
|
107
|
-
if (this.runtime === 'vm2') {
|
|
108
|
-
mixinTypedArrays(context);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
59
|
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
|
112
60
|
const customLogger = (type) => {
|
|
113
61
|
return (...args) => {
|
|
@@ -149,70 +97,12 @@ class ScriptRuntime {
|
|
|
149
97
|
};
|
|
150
98
|
}
|
|
151
99
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
request,
|
|
161
|
-
envVariables: cleanJson(envVariables),
|
|
162
|
-
runtimeVariables: cleanJson(runtimeVariables),
|
|
163
|
-
visualizations,
|
|
164
|
-
persistentEnvVariables: bru.persistentEnvVariables,
|
|
165
|
-
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
|
166
|
-
results: cleanJson(__brunoTestResults.getResults()),
|
|
167
|
-
nextRequestName: bru.nextRequest,
|
|
168
|
-
skipRequest: bru.skipRequest,
|
|
169
|
-
stopExecution: bru.stopExecution
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// default runtime is vm2
|
|
174
|
-
const vm = new NodeVM({
|
|
175
|
-
sandbox: context,
|
|
176
|
-
require: {
|
|
177
|
-
context: 'sandbox',
|
|
178
|
-
builtin: [ "*" ],
|
|
179
|
-
external: true,
|
|
180
|
-
root: [collectionPath, ...additionalContextRootsAbsolute],
|
|
181
|
-
mock: {
|
|
182
|
-
// node libs
|
|
183
|
-
path,
|
|
184
|
-
stream,
|
|
185
|
-
util,
|
|
186
|
-
url,
|
|
187
|
-
http,
|
|
188
|
-
https,
|
|
189
|
-
punycode,
|
|
190
|
-
zlib,
|
|
191
|
-
// 3rd party libs
|
|
192
|
-
ajv,
|
|
193
|
-
'ajv-formats': addFormats,
|
|
194
|
-
atob,
|
|
195
|
-
btoa,
|
|
196
|
-
lodash,
|
|
197
|
-
moment,
|
|
198
|
-
uuid,
|
|
199
|
-
nanoid,
|
|
200
|
-
axios,
|
|
201
|
-
chai,
|
|
202
|
-
'node-fetch': fetch,
|
|
203
|
-
'crypto-js': CryptoJS,
|
|
204
|
-
xml2js: xml2js,
|
|
205
|
-
jsonwebtoken,
|
|
206
|
-
cheerio,
|
|
207
|
-
tv4,
|
|
208
|
-
...whitelistedModules,
|
|
209
|
-
fs: allowScriptFilesystemAccess ? fs : undefined,
|
|
210
|
-
'node-vault': NodeVault
|
|
211
|
-
}
|
|
212
|
-
}
|
|
100
|
+
// default runtime is `quickjs`
|
|
101
|
+
await executeQuickJsVmAsync({
|
|
102
|
+
script: script,
|
|
103
|
+
context: context,
|
|
104
|
+
collectionPath
|
|
213
105
|
});
|
|
214
|
-
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
|
|
215
|
-
await asyncVM();
|
|
216
106
|
|
|
217
107
|
return {
|
|
218
108
|
request,
|
|
@@ -246,7 +136,7 @@ class ScriptRuntime {
|
|
|
246
136
|
let visualizations = [];
|
|
247
137
|
let setVisualizations = (data) => {
|
|
248
138
|
visualizations.push(data);
|
|
249
|
-
}
|
|
139
|
+
};
|
|
250
140
|
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
|
251
141
|
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
|
252
142
|
const collectionVariables = request?.collectionVariables || {};
|
|
@@ -258,24 +148,6 @@ class ScriptRuntime {
|
|
|
258
148
|
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables);
|
|
259
149
|
const req = new BrunoRequest(request);
|
|
260
150
|
const res = new BrunoResponse(response);
|
|
261
|
-
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
|
262
|
-
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
|
263
|
-
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
264
|
-
const additionalContextRootsAbsolute = lodash
|
|
265
|
-
.chain(additionalContextRoots)
|
|
266
|
-
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
|
|
267
|
-
.value();
|
|
268
|
-
|
|
269
|
-
const whitelistedModules = {};
|
|
270
|
-
|
|
271
|
-
for (let module of moduleWhitelist) {
|
|
272
|
-
try {
|
|
273
|
-
whitelistedModules[module] = require(module);
|
|
274
|
-
} catch (e) {
|
|
275
|
-
// Ignore
|
|
276
|
-
console.warn(e);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
151
|
|
|
280
152
|
// extend bru with result getter methods
|
|
281
153
|
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
|
|
@@ -290,10 +162,6 @@ class ScriptRuntime {
|
|
|
290
162
|
__brunoTestResults: __brunoTestResults
|
|
291
163
|
};
|
|
292
164
|
|
|
293
|
-
if (this.runtime === 'vm2') {
|
|
294
|
-
mixinTypedArrays(context);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
165
|
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
|
298
166
|
const customLogger = (type) => {
|
|
299
167
|
return (...args) => {
|
|
@@ -335,71 +203,13 @@ class ScriptRuntime {
|
|
|
335
203
|
};
|
|
336
204
|
}
|
|
337
205
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
response,
|
|
347
|
-
envVariables: cleanJson(envVariables),
|
|
348
|
-
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
|
|
349
|
-
runtimeVariables: cleanJson(runtimeVariables),
|
|
350
|
-
visualizations,
|
|
351
|
-
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
|
352
|
-
results: cleanJson(__brunoTestResults.getResults()),
|
|
353
|
-
nextRequestName: bru.nextRequest,
|
|
354
|
-
skipRequest: bru.skipRequest,
|
|
355
|
-
stopExecution: bru.stopExecution
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// default runtime is vm2
|
|
360
|
-
const vm = new NodeVM({
|
|
361
|
-
sandbox: context,
|
|
362
|
-
require: {
|
|
363
|
-
context: 'sandbox',
|
|
364
|
-
builtin: [ "*" ],
|
|
365
|
-
external: true,
|
|
366
|
-
root: [collectionPath, ...additionalContextRootsAbsolute],
|
|
367
|
-
mock: {
|
|
368
|
-
// node libs
|
|
369
|
-
path,
|
|
370
|
-
stream,
|
|
371
|
-
util,
|
|
372
|
-
url,
|
|
373
|
-
http,
|
|
374
|
-
https,
|
|
375
|
-
punycode,
|
|
376
|
-
zlib,
|
|
377
|
-
// 3rd party libs
|
|
378
|
-
ajv,
|
|
379
|
-
'ajv-formats': addFormats,
|
|
380
|
-
atob,
|
|
381
|
-
btoa,
|
|
382
|
-
lodash,
|
|
383
|
-
moment,
|
|
384
|
-
uuid,
|
|
385
|
-
nanoid,
|
|
386
|
-
axios,
|
|
387
|
-
'node-fetch': fetch,
|
|
388
|
-
'crypto-js': CryptoJS,
|
|
389
|
-
'xml2js': xml2js,
|
|
390
|
-
jsonwebtoken,
|
|
391
|
-
cheerio,
|
|
392
|
-
tv4,
|
|
393
|
-
...whitelistedModules,
|
|
394
|
-
fs: allowScriptFilesystemAccess ? fs : undefined,
|
|
395
|
-
'node-vault': NodeVault
|
|
396
|
-
}
|
|
397
|
-
}
|
|
206
|
+
// default runtime is `quickjs`
|
|
207
|
+
await executeQuickJsVmAsync({
|
|
208
|
+
script: script,
|
|
209
|
+
context: context,
|
|
210
|
+
collectionPath
|
|
398
211
|
});
|
|
399
212
|
|
|
400
|
-
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
|
|
401
|
-
await asyncVM();
|
|
402
|
-
|
|
403
213
|
return {
|
|
404
214
|
response,
|
|
405
215
|
envVariables: cleanJson(envVariables),
|
|
@@ -1,47 +1,17 @@
|
|
|
1
|
-
const { NodeVM } = require('@usebruno/vm2');
|
|
2
|
-
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
3
1
|
const chai = require('chai');
|
|
4
|
-
const
|
|
5
|
-
const http = require('http');
|
|
6
|
-
const https = require('https');
|
|
7
|
-
const stream = require('stream');
|
|
8
|
-
const util = require('util');
|
|
9
|
-
const zlib = require('zlib');
|
|
10
|
-
const url = require('url');
|
|
11
|
-
const punycode = require('punycode');
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const { get } = require('lodash');
|
|
2
|
+
const uuid = require('uuid');
|
|
14
3
|
const Bru = require('../bru');
|
|
15
4
|
const BrunoRequest = require('../bruno-request');
|
|
16
5
|
const BrunoResponse = require('../bruno-response');
|
|
17
|
-
const Test = require('../test');
|
|
18
|
-
const TestResults = require('../test-results');
|
|
19
6
|
const { cleanJson } = require('../utils');
|
|
20
7
|
const { createBruTestResultMethods } = require('../utils/results');
|
|
21
|
-
|
|
22
|
-
// Inbuilt Library Support
|
|
23
|
-
const ajv = require('ajv');
|
|
24
|
-
const addFormats = require('ajv-formats');
|
|
25
|
-
const atob = require('atob');
|
|
26
|
-
const btoa = require('btoa');
|
|
27
|
-
const lodash = require('lodash');
|
|
28
|
-
const moment = require('moment');
|
|
29
|
-
const uuid = require('uuid');
|
|
30
|
-
const nanoid = require('nanoid');
|
|
31
|
-
const axios = require('axios');
|
|
32
|
-
const fetch = require('node-fetch');
|
|
33
|
-
const CryptoJS = require('crypto-js');
|
|
34
|
-
const NodeVault = require('node-vault');
|
|
35
|
-
const xml2js = require('xml2js');
|
|
36
|
-
const cheerio = require('cheerio');
|
|
37
|
-
const tv4 = require('tv4');
|
|
8
|
+
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
38
9
|
const jsonwebtoken = require('jsonwebtoken');
|
|
39
10
|
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
|
40
|
-
const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
|
|
41
11
|
|
|
42
12
|
class TestRuntime {
|
|
43
13
|
constructor(props) {
|
|
44
|
-
this.runtime = props?.runtime || '
|
|
14
|
+
this.runtime = props?.runtime || 'quickjs';
|
|
45
15
|
}
|
|
46
16
|
|
|
47
17
|
async runTests(
|
|
@@ -70,24 +40,6 @@ class TestRuntime {
|
|
|
70
40
|
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables);
|
|
71
41
|
const req = new BrunoRequest(request, historyLogger);
|
|
72
42
|
const res = new BrunoResponse(response);
|
|
73
|
-
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
|
74
|
-
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
|
75
|
-
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
76
|
-
const additionalContextRootsAbsolute = lodash
|
|
77
|
-
.chain(additionalContextRoots)
|
|
78
|
-
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
|
|
79
|
-
.value();
|
|
80
|
-
|
|
81
|
-
const whitelistedModules = {};
|
|
82
|
-
|
|
83
|
-
for (let module of moduleWhitelist) {
|
|
84
|
-
try {
|
|
85
|
-
whitelistedModules[module] = require(module);
|
|
86
|
-
} catch (e) {
|
|
87
|
-
// Ignore
|
|
88
|
-
console.warn(e);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
43
|
|
|
92
44
|
// extend bru with result getter methods
|
|
93
45
|
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
|
|
@@ -114,10 +66,6 @@ class TestRuntime {
|
|
|
114
66
|
jwt: jsonwebtoken
|
|
115
67
|
};
|
|
116
68
|
|
|
117
|
-
if (this.runtime === 'vm2') {
|
|
118
|
-
mixinTypedArrays(context);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
69
|
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
|
122
70
|
const customLogger = (type) => {
|
|
123
71
|
return (...args) => {
|
|
@@ -133,20 +81,14 @@ class TestRuntime {
|
|
|
133
81
|
};
|
|
134
82
|
}
|
|
135
83
|
|
|
136
|
-
if(runRequestByItemPathname) {
|
|
84
|
+
if (runRequestByItemPathname) {
|
|
137
85
|
context.bru.runRequest = runRequestByItemPathname;
|
|
138
86
|
}
|
|
139
87
|
|
|
140
88
|
let scriptError = null;
|
|
141
89
|
|
|
142
90
|
try {
|
|
143
|
-
if (this.runtime === '
|
|
144
|
-
await executeQuickJsVmAsync({
|
|
145
|
-
script: testsFile,
|
|
146
|
-
context: context,
|
|
147
|
-
collectionPath
|
|
148
|
-
});
|
|
149
|
-
} else if (this.runtime === 'nodevm') {
|
|
91
|
+
if (this.runtime === 'nodevm') {
|
|
150
92
|
await runScriptInNodeVm({
|
|
151
93
|
script: testsFile,
|
|
152
94
|
context,
|
|
@@ -154,49 +96,12 @@ class TestRuntime {
|
|
|
154
96
|
scriptingConfig
|
|
155
97
|
});
|
|
156
98
|
} else {
|
|
157
|
-
// default runtime is
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
external: true,
|
|
163
|
-
builtin: ['*'],
|
|
164
|
-
root: [collectionPath, ...additionalContextRootsAbsolute],
|
|
165
|
-
mock: {
|
|
166
|
-
// node libs
|
|
167
|
-
path,
|
|
168
|
-
stream,
|
|
169
|
-
util,
|
|
170
|
-
url,
|
|
171
|
-
http,
|
|
172
|
-
https,
|
|
173
|
-
punycode,
|
|
174
|
-
zlib,
|
|
175
|
-
// 3rd party libs
|
|
176
|
-
ajv,
|
|
177
|
-
'ajv-formats': addFormats,
|
|
178
|
-
btoa,
|
|
179
|
-
atob,
|
|
180
|
-
lodash,
|
|
181
|
-
moment,
|
|
182
|
-
uuid,
|
|
183
|
-
nanoid,
|
|
184
|
-
axios,
|
|
185
|
-
chai,
|
|
186
|
-
'node-fetch': fetch,
|
|
187
|
-
'crypto-js': CryptoJS,
|
|
188
|
-
'xml2js': xml2js,
|
|
189
|
-
cheerio,
|
|
190
|
-
tv4,
|
|
191
|
-
'jsonwebtoken': jsonwebtoken,
|
|
192
|
-
...whitelistedModules,
|
|
193
|
-
fs: allowScriptFilesystemAccess ? fs : undefined,
|
|
194
|
-
'node-vault': NodeVault
|
|
195
|
-
}
|
|
196
|
-
}
|
|
99
|
+
// default runtime is `quickjs`
|
|
100
|
+
await executeQuickJsVmAsync({
|
|
101
|
+
script: testsFile,
|
|
102
|
+
context: context,
|
|
103
|
+
collectionPath
|
|
197
104
|
});
|
|
198
|
-
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
|
|
199
|
-
await asyncVM();
|
|
200
105
|
}
|
|
201
106
|
} catch (error) {
|
|
202
107
|
scriptError = error;
|
|
@@ -20,7 +20,7 @@ const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
|
|
|
20
20
|
|
|
21
21
|
class VarsRuntime {
|
|
22
22
|
constructor(props) {
|
|
23
|
-
this.runtime = props?.runtime || '
|
|
23
|
+
this.runtime = props?.runtime || 'quickjs';
|
|
24
24
|
this.mode = props?.mode || 'developer';
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -3,7 +3,6 @@ const fs = require('node:fs');
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { get } = require('lodash');
|
|
5
5
|
const lodash = require('lodash');
|
|
6
|
-
const { cleanJson } = require('../../utils');
|
|
7
6
|
const { mixinTypedArrays } = require('../mixins/typed-arrays');
|
|
8
7
|
|
|
9
8
|
class ScriptError extends Error {
|
|
@@ -37,6 +36,17 @@ async function runScriptInNodeVm({
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
try {
|
|
39
|
+
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
|
40
|
+
|
|
41
|
+
// Compute additional context roots
|
|
42
|
+
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
43
|
+
const additionalContextRootsAbsolute = lodash
|
|
44
|
+
.chain(additionalContextRoots)
|
|
45
|
+
.map((acr) => (path.isAbsolute(acr) ? acr : path.join(collectionPath, acr)))
|
|
46
|
+
.map((acr) => path.normalize(acr))
|
|
47
|
+
.value();
|
|
48
|
+
additionalContextRootsAbsolute.push(path.normalize(collectionPath));
|
|
49
|
+
|
|
40
50
|
// Create script context with all necessary variables
|
|
41
51
|
const scriptContext = {
|
|
42
52
|
// Bruno context
|
|
@@ -77,7 +87,9 @@ async function runScriptInNodeVm({
|
|
|
77
87
|
collectionPath,
|
|
78
88
|
scriptContext,
|
|
79
89
|
currentModuleDir: collectionPath,
|
|
80
|
-
localModuleCache
|
|
90
|
+
localModuleCache,
|
|
91
|
+
allowScriptFilesystemAccess,
|
|
92
|
+
additionalContextRootsAbsolute
|
|
81
93
|
});
|
|
82
94
|
|
|
83
95
|
// Execute the script in an isolated VM context
|
|
@@ -96,7 +108,6 @@ async function runScriptInNodeVm({
|
|
|
96
108
|
return;
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
|
|
100
111
|
/**
|
|
101
112
|
* Creates a custom require function with enhanced security and local module support
|
|
102
113
|
* @param {Object} options - Configuration options
|
|
@@ -105,6 +116,8 @@ async function runScriptInNodeVm({
|
|
|
105
116
|
* @param {Object} options.scriptContext - Script execution context
|
|
106
117
|
* @param {string} options.currentModuleDir - Current module directory for relative imports
|
|
107
118
|
* @param {Map} options.localModuleCache - Cache for loaded local modules
|
|
119
|
+
* @param {boolean} options.allowScriptFilesystemAccess - Whether to allow fs module access
|
|
120
|
+
* @param {Array<string>} options.additionalContextRootsAbsolute - Pre-computed absolute context roots
|
|
108
121
|
* @returns {Function} Custom require function
|
|
109
122
|
*/
|
|
110
123
|
function createCustomRequire({
|
|
@@ -112,32 +125,54 @@ function createCustomRequire({
|
|
|
112
125
|
collectionPath,
|
|
113
126
|
scriptContext,
|
|
114
127
|
currentModuleDir = collectionPath,
|
|
115
|
-
localModuleCache = new Map()
|
|
128
|
+
localModuleCache = new Map(),
|
|
129
|
+
allowScriptFilesystemAccess = false,
|
|
130
|
+
additionalContextRootsAbsolute = []
|
|
116
131
|
}) {
|
|
117
|
-
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
118
|
-
const additionalContextRootsAbsolute = lodash
|
|
119
|
-
.chain(additionalContextRoots)
|
|
120
|
-
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
|
|
121
|
-
.value();
|
|
122
|
-
additionalContextRootsAbsolute.push(collectionPath);
|
|
123
|
-
|
|
124
132
|
return (moduleName) => {
|
|
125
|
-
// Check if it's a local module (starts with ./ or ../)
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
// Check if it's a local module (starts with ./ or ../ or .\ or ..\)
|
|
134
|
+
// Normalize backslashes to forward slashes for cross-platform compatibility
|
|
135
|
+
const normalizedModuleName = moduleName.replace(/\\/g, '/');
|
|
136
|
+
if (normalizedModuleName.startsWith('./') || normalizedModuleName.startsWith('../')) {
|
|
137
|
+
return loadLocalModule({ moduleName: normalizedModuleName, collectionPath, scriptContext, localModuleCache, currentModuleDir, additionalContextRootsAbsolute });
|
|
128
138
|
}
|
|
129
139
|
|
|
140
|
+
// Helper function to check if a module is the fs module or a submodule
|
|
141
|
+
const isFsModule = (module) => {
|
|
142
|
+
if (!module) return false;
|
|
143
|
+
const fsModule = require('fs');
|
|
144
|
+
// Check if it's the fs module itself
|
|
145
|
+
if (module === fsModule) return true;
|
|
146
|
+
// Check if it's fs/promises submodule
|
|
147
|
+
if (module === fsModule.promises) return true;
|
|
148
|
+
// Check if it's fs/promises by comparing with require('fs/promises')
|
|
149
|
+
try {
|
|
150
|
+
if (module === require('fs/promises')) return true;
|
|
151
|
+
} catch {
|
|
152
|
+
// fs/promises might not be available in all Node versions
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
};
|
|
156
|
+
|
|
130
157
|
// First try to require as a native/npm module
|
|
131
158
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} catch (error) {
|
|
139
|
-
throw new Error(`Could not resolve module "${moduleName}": ${error.message}\n\nThis most likely means you did not install the module under "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map(root => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
|
|
159
|
+
const requiredModulePath = require.resolve(moduleName, { paths: [...additionalContextRootsAbsolute, ...module.paths] });
|
|
160
|
+
const requiredModule = require(requiredModulePath);
|
|
161
|
+
|
|
162
|
+
// Block filesystem module access if filesystem access is not allowed
|
|
163
|
+
if (!allowScriptFilesystemAccess && isFsModule(requiredModule)) {
|
|
164
|
+
throw new Error('Filesystem access is not allowed. Enable "filesystemAccess.allow" in scripting config to use the fs module.');
|
|
140
165
|
}
|
|
166
|
+
|
|
167
|
+
return requiredModule;
|
|
168
|
+
} catch (requireError) {
|
|
169
|
+
// Re-throw if it's our filesystem access error
|
|
170
|
+
if (requireError.message && requireError.message.includes('Enable "filesystemAccess.allow"')) {
|
|
171
|
+
throw requireError;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If that fails, try to resolve from additionalContextRoots
|
|
175
|
+
throw new Error(`Could not resolve module "${moduleName}": ${requireError.message}\n\nThis most likely means you did not install the module under the collection or the "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map((root) => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
|
|
141
176
|
}
|
|
142
177
|
};
|
|
143
178
|
}
|
|
@@ -150,6 +185,7 @@ function createCustomRequire({
|
|
|
150
185
|
* @param {Object} options.scriptContext - Script execution context to inherit
|
|
151
186
|
* @param {Map} options.localModuleCache - Cache for loaded modules
|
|
152
187
|
* @param {string} options.currentModuleDir - Directory of the current module for relative resolution
|
|
188
|
+
* @param {Array<string>} options.additionalContextRootsAbsolute - Additional allowed context root paths
|
|
153
189
|
* @returns {*} The exported content of the loaded module
|
|
154
190
|
* @throws {Error} When module is outside collection path or cannot be loaded
|
|
155
191
|
*/
|
|
@@ -158,7 +194,8 @@ function loadLocalModule({
|
|
|
158
194
|
collectionPath,
|
|
159
195
|
scriptContext,
|
|
160
196
|
localModuleCache,
|
|
161
|
-
currentModuleDir
|
|
197
|
+
currentModuleDir,
|
|
198
|
+
additionalContextRootsAbsolute = []
|
|
162
199
|
}) {
|
|
163
200
|
// Check if the filename has an extension
|
|
164
201
|
const hasExtension = path.extname(moduleName) !== '';
|
|
@@ -167,12 +204,19 @@ function loadLocalModule({
|
|
|
167
204
|
// Resolve the file path relative to the current module's directory
|
|
168
205
|
const filePath = path.resolve(currentModuleDir, resolvedFilename);
|
|
169
206
|
const normalizedFilePath = path.normalize(filePath);
|
|
170
|
-
const normalizedCollectionPath = path.normalize(collectionPath);
|
|
171
207
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
208
|
+
const isWithinAllowedRoot = additionalContextRootsAbsolute.some((allowedRoot) => {
|
|
209
|
+
const normalizedAllowedRoot = path.normalize(allowedRoot);
|
|
210
|
+
const relativePath = path.relative(normalizedAllowedRoot, normalizedFilePath);
|
|
211
|
+
return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (!isWithinAllowedRoot) {
|
|
215
|
+
const allowedRootsDisplay = additionalContextRootsAbsolute.map((root) => ` - ${root}`).join('\n');
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Access to files outside of the allowed context roots is not allowed: ${moduleName}\n\n`
|
|
218
|
+
+ `Allowed context roots:\n${allowedRootsDisplay}`
|
|
219
|
+
);
|
|
176
220
|
}
|
|
177
221
|
|
|
178
222
|
// Check cache first (use normalized path as key)
|
|
@@ -202,11 +246,13 @@ function loadLocalModule({
|
|
|
202
246
|
__dirname: moduleDir,
|
|
203
247
|
// Create a custom require function for this module that resolves relative to its directory
|
|
204
248
|
require: createCustomRequire({
|
|
205
|
-
scriptingConfig: scriptContext.scriptingConfig || {},
|
|
206
|
-
collectionPath,
|
|
207
|
-
scriptContext,
|
|
208
|
-
currentModuleDir: moduleDir,
|
|
209
|
-
localModuleCache
|
|
249
|
+
scriptingConfig: scriptContext.scriptingConfig || {},
|
|
250
|
+
collectionPath,
|
|
251
|
+
scriptContext,
|
|
252
|
+
currentModuleDir: moduleDir,
|
|
253
|
+
localModuleCache,
|
|
254
|
+
allowScriptFilesystemAccess: get(scriptContext.scriptingConfig, 'filesystemAccess.allow', false),
|
|
255
|
+
additionalContextRootsAbsolute
|
|
210
256
|
})
|
|
211
257
|
};
|
|
212
258
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { runScriptInNodeVm } = require('./index');
|
|
6
|
+
|
|
7
|
+
describe('node-vm sandbox', () => {
|
|
8
|
+
let testDir;
|
|
9
|
+
let collectionPath;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Create a temporary test directory
|
|
13
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-test-'));
|
|
14
|
+
collectionPath = path.join(testDir, 'collection');
|
|
15
|
+
fs.mkdirSync(collectionPath);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
// Clean up test directory
|
|
20
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('createCustomRequire - local modules', () => {
|
|
24
|
+
it('should load local module with ./ path', async () => {
|
|
25
|
+
// Create a local module
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(collectionPath, 'helper.js'),
|
|
28
|
+
'module.exports = { value: 42 };'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const script = `
|
|
32
|
+
const helper = require('./helper');
|
|
33
|
+
bru.setVar('result', helper.value);
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const context = {
|
|
37
|
+
bru: { setVar: jest.fn() },
|
|
38
|
+
console: console
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
42
|
+
|
|
43
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 42);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should load local module with ../ path', async () => {
|
|
47
|
+
// Create a subdirectory and modules
|
|
48
|
+
const subDir = path.join(collectionPath, 'subdir');
|
|
49
|
+
fs.mkdirSync(subDir);
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
path.join(collectionPath, 'parent.js'),
|
|
52
|
+
'module.exports = { name: "parent" };'
|
|
53
|
+
);
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(subDir, 'child.js'),
|
|
56
|
+
'const parent = require("../parent"); module.exports = parent;'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const script = `
|
|
60
|
+
const child = require('./subdir/child');
|
|
61
|
+
bru.setVar('result', child.name);
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const context = {
|
|
65
|
+
bru: { setVar: jest.fn() },
|
|
66
|
+
console: console
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
70
|
+
|
|
71
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'parent');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle backslashes on Windows', async () => {
|
|
75
|
+
const subDir = path.join(collectionPath, 'utils');
|
|
76
|
+
fs.mkdirSync(subDir);
|
|
77
|
+
fs.writeFileSync(
|
|
78
|
+
path.join(subDir, 'module.js'),
|
|
79
|
+
'module.exports = { platform: "cross-platform" };'
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Simulate Windows-style path with backslashes
|
|
83
|
+
const script = `
|
|
84
|
+
const mod = require('.\\\\utils\\\\module');
|
|
85
|
+
bru.setVar('result', mod.platform);
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
const context = {
|
|
89
|
+
bru: { setVar: jest.fn() },
|
|
90
|
+
console: console
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
94
|
+
|
|
95
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'cross-platform');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should block access outside collection path', async () => {
|
|
99
|
+
const script = `
|
|
100
|
+
const outside = require('../../outside');
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const context = { console: console };
|
|
104
|
+
|
|
105
|
+
await expect(
|
|
106
|
+
runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
|
|
107
|
+
).rejects.toThrow('Access to files outside of the allowed context roots is not allowed');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('createCustomRequire - additionalContextRoots', () => {
|
|
112
|
+
it('should allow module access from additionalContextRoots', async () => {
|
|
113
|
+
// Create an additional context root at same level as collection
|
|
114
|
+
const additionalRoot = path.join(testDir, 'shared');
|
|
115
|
+
fs.mkdirSync(additionalRoot);
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
path.join(additionalRoot, 'shared.js'),
|
|
118
|
+
'module.exports = { shared: true };'
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// From collection, traverse up to testDir, then into shared directory
|
|
122
|
+
const script = `
|
|
123
|
+
const shared = require('../shared/shared');
|
|
124
|
+
bru.setVar('result', shared.shared);
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
const context = {
|
|
128
|
+
bru: { setVar: jest.fn() },
|
|
129
|
+
console: console
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const scriptingConfig = {
|
|
133
|
+
additionalContextRoots: [additionalRoot]
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
137
|
+
|
|
138
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should handle relative additionalContextRoots path', async () => {
|
|
142
|
+
// Create a sibling directory to collection
|
|
143
|
+
const libsDir = path.join(testDir, 'libs');
|
|
144
|
+
fs.mkdirSync(libsDir);
|
|
145
|
+
fs.writeFileSync(
|
|
146
|
+
path.join(libsDir, 'lib.js'),
|
|
147
|
+
'module.exports = { fromLib: "yes" };'
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const script = `
|
|
151
|
+
const lib = require('../libs/lib');
|
|
152
|
+
bru.setVar('result', lib.fromLib);
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const context = {
|
|
156
|
+
bru: { setVar: jest.fn() },
|
|
157
|
+
console: console
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const scriptingConfig = {
|
|
161
|
+
additionalContextRoots: ['../libs']
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
165
|
+
|
|
166
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'yes');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle nested additional context roots modules', async () => {
|
|
170
|
+
// Create an additional context root
|
|
171
|
+
const additionalRoot = path.join(testDir, 'shared');
|
|
172
|
+
fs.mkdirSync(additionalRoot);
|
|
173
|
+
fs.writeFileSync(
|
|
174
|
+
path.join(additionalRoot, 'allowed.js'),
|
|
175
|
+
'module.exports = { allowed: true };'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Create a nested module that tries to require from additional root
|
|
179
|
+
fs.writeFileSync(
|
|
180
|
+
path.join(collectionPath, 'parent.js'),
|
|
181
|
+
`
|
|
182
|
+
const allowed = require('../shared/allowed');
|
|
183
|
+
module.exports = { nestedAccess: allowed.allowed };
|
|
184
|
+
`
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const script = `
|
|
188
|
+
const parent = require('./parent');
|
|
189
|
+
bru.setVar('result', parent.nestedAccess);
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const context = {
|
|
193
|
+
bru: { setVar: jest.fn() },
|
|
194
|
+
console: console
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const scriptingConfig = {
|
|
198
|
+
additionalContextRoots: [additionalRoot]
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
202
|
+
|
|
203
|
+
// Nested module should successfully access the additional root
|
|
204
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('createCustomRequire - npm modules', () => {
|
|
209
|
+
it('should load npm module', async () => {
|
|
210
|
+
const script = `
|
|
211
|
+
const lodash = require('lodash');
|
|
212
|
+
bru.setVar('result', typeof lodash.get);
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const context = {
|
|
216
|
+
bru: { setVar: jest.fn() },
|
|
217
|
+
console: console
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
221
|
+
|
|
222
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('createCustomRequire - module caching', () => {
|
|
227
|
+
it('should cache loaded modules', async () => {
|
|
228
|
+
let callCount = 0;
|
|
229
|
+
fs.writeFileSync(
|
|
230
|
+
path.join(collectionPath, 'cached.js'),
|
|
231
|
+
`
|
|
232
|
+
module.exports = { count: ${++callCount} };
|
|
233
|
+
`
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const script = `
|
|
237
|
+
const mod1 = require('./cached');
|
|
238
|
+
const mod2 = require('./cached');
|
|
239
|
+
bru.setVar('same', mod1.count === mod2.count);
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
const context = {
|
|
243
|
+
bru: { setVar: jest.fn() },
|
|
244
|
+
console: console
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
248
|
+
|
|
249
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('same', true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -24,7 +24,7 @@ const toNumber = (value) => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const removeQuotes = (str) => {
|
|
27
|
-
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith(
|
|
27
|
+
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith('\'') && str.endsWith('\''))) {
|
|
28
28
|
return str.slice(1, -1);
|
|
29
29
|
}
|
|
30
30
|
return str;
|
|
@@ -36,7 +36,7 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
36
36
|
}
|
|
37
37
|
externalScript = externalScript?.trim();
|
|
38
38
|
|
|
39
|
-
if(scriptType === 'template-literal') {
|
|
39
|
+
if (scriptType === 'template-literal') {
|
|
40
40
|
if (!isNaN(Number(externalScript))) {
|
|
41
41
|
const number = Number(externalScript);
|
|
42
42
|
|
|
@@ -341,7 +341,7 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
341
341
|
const promise = vm.newPromise();
|
|
342
342
|
const dumpedUrl = vm.dump(url);
|
|
343
343
|
const dumpedNameOrObj = vm.dump(nameOrCookieObj);
|
|
344
|
-
|
|
344
|
+
|
|
345
345
|
// Check if the second argument is an object (cookie object case)
|
|
346
346
|
if (typeof dumpedNameOrObj === 'object' && dumpedNameOrObj !== null) {
|
|
347
347
|
// Cookie object case: setCookie(url, cookieObject, callback)
|
|
@@ -363,7 +363,7 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
363
363
|
}
|
|
364
364
|
});
|
|
365
365
|
}
|
|
366
|
-
|
|
366
|
+
|
|
367
367
|
promise.settled.then(vm.runtime.executePendingJobs);
|
|
368
368
|
return promise.handle;
|
|
369
369
|
});
|
|
@@ -371,7 +371,7 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
371
371
|
|
|
372
372
|
const _setCookiesFn = vm.newFunction('_setCookies', (url, cookiesArray) => {
|
|
373
373
|
const promise = vm.newPromise();
|
|
374
|
-
|
|
374
|
+
|
|
375
375
|
nativeJar.setCookies(vm.dump(url), vm.dump(cookiesArray), (err) => {
|
|
376
376
|
if (err) {
|
|
377
377
|
promise.reject(marshallToVm(cleanJson(err), vm));
|
|
@@ -10,7 +10,7 @@ const addCryptoUtilsShimToContext = async (vm) => {
|
|
|
10
10
|
let randomBytesHandle = vm.newFunction('randomBytes', function (sizeHandle) {
|
|
11
11
|
try {
|
|
12
12
|
let size = vm.dump(sizeHandle);
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
if (typeof size !== 'number') {
|
|
15
15
|
throw new TypeError('The "size" argument must be of type number');
|
|
16
16
|
}
|
|
@@ -30,15 +30,14 @@ const addCryptoUtilsShimToContext = async (vm) => {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const buffer = crypto.randomBytes(size);
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
const byteArray = Array.from(buffer);
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
return marshallToVm(byteArray, vm);
|
|
37
|
-
|
|
38
37
|
} catch (error) {
|
|
39
38
|
const vmError = vm.newError(error.message);
|
|
40
39
|
vm.setProp(vmError, 'name', vm.newString(error.name));
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
throw vmError;
|
|
43
42
|
}
|
|
44
43
|
});
|
|
@@ -48,7 +47,7 @@ const addCryptoUtilsShimToContext = async (vm) => {
|
|
|
48
47
|
// Receive the serialized array data directly
|
|
49
48
|
const serializedArray = vm.dump(arrayHandle);
|
|
50
49
|
const typedArray = deserializeTypedArray(serializedArray);
|
|
51
|
-
|
|
50
|
+
|
|
52
51
|
if (typedArray.length === 0) {
|
|
53
52
|
return marshallToVm([], vm);
|
|
54
53
|
}
|
|
@@ -62,11 +61,10 @@ const addCryptoUtilsShimToContext = async (vm) => {
|
|
|
62
61
|
const byteArray = Array.from(typedArray);
|
|
63
62
|
|
|
64
63
|
return marshallToVm(byteArray, vm);
|
|
65
|
-
|
|
66
64
|
} catch (error) {
|
|
67
65
|
const vmError = vm.newError(error.message);
|
|
68
66
|
vm.setProp(vmError, 'name', vm.newString(error.name));
|
|
69
|
-
|
|
67
|
+
|
|
70
68
|
throw vmError;
|
|
71
69
|
}
|
|
72
70
|
});
|
|
@@ -101,4 +99,4 @@ const addCryptoUtilsShimToContext = async (vm) => {
|
|
|
101
99
|
`);
|
|
102
100
|
};
|
|
103
101
|
|
|
104
|
-
module.exports = addCryptoUtilsShimToContext;
|
|
102
|
+
module.exports = addCryptoUtilsShimToContext;
|
|
@@ -27,7 +27,7 @@ describe('crypto-utils shims tests', () => {
|
|
|
27
27
|
const handle = vm.unwrapResult(result);
|
|
28
28
|
const type = vm.dump(handle);
|
|
29
29
|
handle.dispose();
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
expect(type).toBe('function');
|
|
32
32
|
});
|
|
33
33
|
|
|
@@ -36,7 +36,7 @@ describe('crypto-utils shims tests', () => {
|
|
|
36
36
|
const handle = vm.unwrapResult(result);
|
|
37
37
|
const type = vm.dump(handle);
|
|
38
38
|
handle.dispose();
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
expect(type).toBe('function');
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -45,7 +45,7 @@ describe('crypto-utils shims tests', () => {
|
|
|
45
45
|
const handle = vm.unwrapResult(result);
|
|
46
46
|
const length = vm.dump(handle);
|
|
47
47
|
handle.dispose();
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
expect(length).toBe(8);
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -54,7 +54,7 @@ describe('crypto-utils shims tests', () => {
|
|
|
54
54
|
const handle = vm.unwrapResult(result);
|
|
55
55
|
const hexLength = vm.dump(handle);
|
|
56
56
|
handle.dispose();
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
expect(hexLength).toBe(8); // 4 bytes = 8 hex chars
|
|
59
59
|
});
|
|
60
60
|
|
|
@@ -67,7 +67,7 @@ describe('crypto-utils shims tests', () => {
|
|
|
67
67
|
const handle = vm.unwrapResult(result);
|
|
68
68
|
const length = vm.dump(handle);
|
|
69
69
|
handle.dispose();
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
expect(length).toBe(5);
|
|
72
72
|
});
|
|
73
|
-
});
|
|
73
|
+
});
|
package/src/utils/results.js
CHANGED
|
@@ -7,7 +7,7 @@ const getResultsSummary = (results) => {
|
|
|
7
7
|
total: results.length,
|
|
8
8
|
passed: 0,
|
|
9
9
|
failed: 0,
|
|
10
|
-
skipped: 0
|
|
10
|
+
skipped: 0
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
results.forEach((r) => {
|
|
@@ -34,7 +34,7 @@ const setupBruTestMethods = (bru, __brunoTestResults, assertionResults) => {
|
|
|
34
34
|
const summary = getResultsSummary(results);
|
|
35
35
|
return {
|
|
36
36
|
summary,
|
|
37
|
-
results: results.map(r => ({
|
|
37
|
+
results: results.map((r) => ({
|
|
38
38
|
status: r.status,
|
|
39
39
|
description: r.description,
|
|
40
40
|
expected: r.expected,
|
|
@@ -49,7 +49,7 @@ const setupBruTestMethods = (bru, __brunoTestResults, assertionResults) => {
|
|
|
49
49
|
const summary = getResultsSummary(results);
|
|
50
50
|
return {
|
|
51
51
|
summary,
|
|
52
|
-
results: results.map(r => ({
|
|
52
|
+
results: results.map((r) => ({
|
|
53
53
|
status: r.status,
|
|
54
54
|
lhsExpr: r.lhsExpr,
|
|
55
55
|
rhsExpr: r.rhsExpr,
|
|
@@ -77,4 +77,4 @@ module.exports = {
|
|
|
77
77
|
getResultsSummary,
|
|
78
78
|
createBruTestResultMethods,
|
|
79
79
|
setupBruTestMethods
|
|
80
|
-
};
|
|
80
|
+
};
|
package/src/utils.js
CHANGED
|
@@ -90,7 +90,7 @@ const evaluateJsTemplateLiteral = (templateLiteral, context) => {
|
|
|
90
90
|
return templateLiteral.slice(1, -1);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
if (templateLiteral.startsWith(
|
|
93
|
+
if (templateLiteral.startsWith('\'') && templateLiteral.endsWith('\'')) {
|
|
94
94
|
return templateLiteral.slice(1, -1);
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -129,7 +129,7 @@ const createResponseParser = (response = {}) => {
|
|
|
129
129
|
};
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Objects that are created inside
|
|
132
|
+
* Objects that are created inside developer mode execution context result in an serialization error when sent to the renderer process
|
|
133
133
|
* Error sending from webFrameMain: Error: Failed to serialize arguments
|
|
134
134
|
* at s.send (node:electron/js2c/browser_init:169:631)
|
|
135
135
|
* at g.send (node:electron/js2c/browser_init:165:2156)
|
|
@@ -201,7 +201,7 @@ const uuid = () => {
|
|
|
201
201
|
const customNanoId = customAlphabet(urlAlphabet, 21);
|
|
202
202
|
|
|
203
203
|
return customNanoId();
|
|
204
|
-
}
|
|
204
|
+
};
|
|
205
205
|
|
|
206
206
|
const appendAwaitToTestFunc = (str) => {
|
|
207
207
|
return str.replace(/(?<!\.\s*)(?<!await\s)(test\()/g, 'await $1');
|
|
@@ -211,18 +211,18 @@ const cleanCircularJson = (data) => {
|
|
|
211
211
|
try {
|
|
212
212
|
// Handle circular references by keeping track of seen objects
|
|
213
213
|
const seen = new WeakSet();
|
|
214
|
-
|
|
214
|
+
|
|
215
215
|
const replacer = (key, value) => {
|
|
216
216
|
// Skip non-objects and null
|
|
217
217
|
if (typeof value !== 'object' || value === null) {
|
|
218
218
|
return value;
|
|
219
219
|
}
|
|
220
|
-
|
|
220
|
+
|
|
221
221
|
// Detect circular reference
|
|
222
222
|
if (seen.has(value)) {
|
|
223
223
|
return '[Circular Reference]';
|
|
224
224
|
}
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
seen.add(value);
|
|
227
227
|
return value;
|
|
228
228
|
};
|