@usebruno/js 0.43.0 → 0.45.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 +5 -12
- package/src/bru.js +37 -15
- package/src/bruno-request.js +66 -8
- package/src/bruno-response.js +15 -2
- package/src/runtime/assert-runtime.js +5 -2
- package/src/runtime/script-runtime.js +18 -207
- package/src/runtime/test-runtime.js +12 -106
- package/src/runtime/vars-runtime.js +3 -2
- package/src/sandbox/node-vm/cjs-loader.js +369 -0
- package/src/sandbox/node-vm/constants.js +99 -0
- package/src/sandbox/node-vm/index.js +58 -188
- package/src/sandbox/node-vm/index.spec.js +1198 -0
- package/src/sandbox/node-vm/utils.js +42 -0
- package/src/sandbox/quickjs/index.js +2 -2
- package/src/sandbox/quickjs/shims/bru.js +9 -3
- package/src/sandbox/quickjs/shims/bruno-request.js +27 -0
- 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
|
@@ -0,0 +1,1198 @@
|
|
|
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
|
+
it('should block absolute paths outside allowed roots', async () => {
|
|
111
|
+
// Try to require an absolute path outside the collection
|
|
112
|
+
const script = `
|
|
113
|
+
const secret = require('/etc/passwd');
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
const context = { console: console };
|
|
117
|
+
|
|
118
|
+
await expect(
|
|
119
|
+
runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
|
|
120
|
+
).rejects.toThrow('Access to files outside of the allowed context roots is not allowed');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should allow absolute paths within allowed roots', async () => {
|
|
124
|
+
// Create a module in the collection
|
|
125
|
+
fs.writeFileSync(
|
|
126
|
+
path.join(collectionPath, 'absolute-test.js'),
|
|
127
|
+
'module.exports = { loaded: true };'
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Use absolute path to require it
|
|
131
|
+
const absolutePath = path.join(collectionPath, 'absolute-test.js');
|
|
132
|
+
const script = `
|
|
133
|
+
const mod = require('${absolutePath.replace(/\\/g, '\\\\')}');
|
|
134
|
+
bru.setVar('result', mod.loaded);
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
const context = {
|
|
138
|
+
bru: { setVar: jest.fn() },
|
|
139
|
+
console: console
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
143
|
+
|
|
144
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('createCustomRequire - additionalContextRoots', () => {
|
|
149
|
+
it('should allow module access from additionalContextRoots', async () => {
|
|
150
|
+
// Create an additional context root at same level as collection
|
|
151
|
+
const additionalRoot = path.join(testDir, 'shared');
|
|
152
|
+
fs.mkdirSync(additionalRoot);
|
|
153
|
+
fs.writeFileSync(
|
|
154
|
+
path.join(additionalRoot, 'shared.js'),
|
|
155
|
+
'module.exports = { shared: true };'
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// From collection, traverse up to testDir, then into shared directory
|
|
159
|
+
const script = `
|
|
160
|
+
const shared = require('../shared/shared');
|
|
161
|
+
bru.setVar('result', shared.shared);
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
const context = {
|
|
165
|
+
bru: { setVar: jest.fn() },
|
|
166
|
+
console: console
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const scriptingConfig = {
|
|
170
|
+
additionalContextRoots: [additionalRoot]
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
174
|
+
|
|
175
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should handle relative additionalContextRoots path', async () => {
|
|
179
|
+
// Create a sibling directory to collection
|
|
180
|
+
const libsDir = path.join(testDir, 'libs');
|
|
181
|
+
fs.mkdirSync(libsDir);
|
|
182
|
+
fs.writeFileSync(
|
|
183
|
+
path.join(libsDir, 'lib.js'),
|
|
184
|
+
'module.exports = { fromLib: "yes" };'
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const script = `
|
|
188
|
+
const lib = require('../libs/lib');
|
|
189
|
+
bru.setVar('result', lib.fromLib);
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const context = {
|
|
193
|
+
bru: { setVar: jest.fn() },
|
|
194
|
+
console: console
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const scriptingConfig = {
|
|
198
|
+
additionalContextRoots: ['../libs']
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
202
|
+
|
|
203
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'yes');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle nested additional context roots modules', async () => {
|
|
207
|
+
// Create an additional context root
|
|
208
|
+
const additionalRoot = path.join(testDir, 'shared');
|
|
209
|
+
fs.mkdirSync(additionalRoot);
|
|
210
|
+
fs.writeFileSync(
|
|
211
|
+
path.join(additionalRoot, 'allowed.js'),
|
|
212
|
+
'module.exports = { allowed: true };'
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Create a nested module that tries to require from additional root
|
|
216
|
+
fs.writeFileSync(
|
|
217
|
+
path.join(collectionPath, 'parent.js'),
|
|
218
|
+
`
|
|
219
|
+
const allowed = require('../shared/allowed');
|
|
220
|
+
module.exports = { nestedAccess: allowed.allowed };
|
|
221
|
+
`
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const script = `
|
|
225
|
+
const parent = require('./parent');
|
|
226
|
+
bru.setVar('result', parent.nestedAccess);
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
const context = {
|
|
230
|
+
bru: { setVar: jest.fn() },
|
|
231
|
+
console: console
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const scriptingConfig = {
|
|
235
|
+
additionalContextRoots: [additionalRoot]
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
239
|
+
|
|
240
|
+
// Nested module should successfully access the additional root
|
|
241
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('createCustomRequire - npm modules', () => {
|
|
246
|
+
it('should load npm module', async () => {
|
|
247
|
+
const script = `
|
|
248
|
+
const lodash = require('lodash');
|
|
249
|
+
bru.setVar('result', typeof lodash.get);
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
const context = {
|
|
253
|
+
bru: { setVar: jest.fn() },
|
|
254
|
+
console: console
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
258
|
+
|
|
259
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('createCustomRequire - module caching', () => {
|
|
264
|
+
it('should cache loaded modules', async () => {
|
|
265
|
+
// Module increments a counter each time it's executed
|
|
266
|
+
// If caching works, counter should only be 1 after multiple requires
|
|
267
|
+
fs.writeFileSync(
|
|
268
|
+
path.join(collectionPath, 'cached.js'),
|
|
269
|
+
`
|
|
270
|
+
if (!global._cacheTestCount) global._cacheTestCount = 0;
|
|
271
|
+
global._cacheTestCount++;
|
|
272
|
+
module.exports = { id: Date.now() };
|
|
273
|
+
`
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const script = `
|
|
277
|
+
const mod1 = require('./cached');
|
|
278
|
+
const mod2 = require('./cached');
|
|
279
|
+
const mod3 = require('./cached');
|
|
280
|
+
bru.setVar('sameInstance', mod1 === mod2 && mod2 === mod3);
|
|
281
|
+
bru.setVar('loadCount', global._cacheTestCount);
|
|
282
|
+
`;
|
|
283
|
+
|
|
284
|
+
const context = {
|
|
285
|
+
bru: { setVar: jest.fn() },
|
|
286
|
+
console: console
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
290
|
+
|
|
291
|
+
// All requires should return the same cached instance
|
|
292
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('sameInstance', true);
|
|
293
|
+
// Module should only be executed once
|
|
294
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('loadCount', 1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle circular dependencies', async () => {
|
|
298
|
+
// Create two modules that require each other
|
|
299
|
+
fs.writeFileSync(
|
|
300
|
+
path.join(collectionPath, 'circularA.js'),
|
|
301
|
+
`
|
|
302
|
+
exports.name = 'A';
|
|
303
|
+
const B = require('./circularB');
|
|
304
|
+
exports.fromB = B.name;
|
|
305
|
+
`
|
|
306
|
+
);
|
|
307
|
+
fs.writeFileSync(
|
|
308
|
+
path.join(collectionPath, 'circularB.js'),
|
|
309
|
+
`
|
|
310
|
+
exports.name = 'B';
|
|
311
|
+
const A = require('./circularA');
|
|
312
|
+
exports.fromA = A.name;
|
|
313
|
+
`
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const script = `
|
|
317
|
+
const A = require('./circularA');
|
|
318
|
+
// A loads first, sets exports.name='A', then requires B
|
|
319
|
+
// B loads, sets exports.name='B', requires A (gets partial: {name:'A'})
|
|
320
|
+
// B finishes with {name:'B', fromA:'A'}
|
|
321
|
+
// A finishes with {name:'A', fromB:'B'}
|
|
322
|
+
bru.setVar('result', A.name + '-' + A.fromB);
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
const context = {
|
|
326
|
+
bru: { setVar: jest.fn() },
|
|
327
|
+
console: console
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
331
|
+
|
|
332
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'A-B');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('createCustomRequire - Node.js builtin modules', () => {
|
|
337
|
+
it('should load builtin modules (crypto)', async () => {
|
|
338
|
+
const script = `
|
|
339
|
+
const crypto = require('crypto');
|
|
340
|
+
bru.setVar('result', typeof crypto.createHash);
|
|
341
|
+
`;
|
|
342
|
+
|
|
343
|
+
const context = {
|
|
344
|
+
bru: { setVar: jest.fn() },
|
|
345
|
+
console: console
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
349
|
+
|
|
350
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should support node: prefix syntax', async () => {
|
|
354
|
+
const script = `
|
|
355
|
+
const path = require('node:path');
|
|
356
|
+
bru.setVar('result', typeof path.join);
|
|
357
|
+
`;
|
|
358
|
+
|
|
359
|
+
const context = {
|
|
360
|
+
bru: { setVar: jest.fn() },
|
|
361
|
+
console: console
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
365
|
+
|
|
366
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should allow all builtin modules including fs', async () => {
|
|
370
|
+
const script = `
|
|
371
|
+
const fs = require('fs');
|
|
372
|
+
bru.setVar('result', typeof fs.readFileSync);
|
|
373
|
+
`;
|
|
374
|
+
|
|
375
|
+
const context = {
|
|
376
|
+
bru: { setVar: jest.fn() },
|
|
377
|
+
console: console
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
381
|
+
|
|
382
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should load multiple builtins', async () => {
|
|
386
|
+
const script = `
|
|
387
|
+
const url = require('url');
|
|
388
|
+
const util = require('util');
|
|
389
|
+
const buffer = require('buffer');
|
|
390
|
+
const fs = require('fs');
|
|
391
|
+
bru.setVar('result', typeof url.parse + '-' + typeof util.format + '-' + typeof buffer.Buffer + '-' + typeof fs.readFileSync);
|
|
392
|
+
`;
|
|
393
|
+
|
|
394
|
+
const context = {
|
|
395
|
+
bru: { setVar: jest.fn() },
|
|
396
|
+
console: console
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
400
|
+
|
|
401
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function-function-function-function');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('createCustomRequire - npm modules in vm context', () => {
|
|
406
|
+
it('should load npm modules from collection into vm context', async () => {
|
|
407
|
+
// Create a mock npm module in collection's node_modules
|
|
408
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'test-module');
|
|
409
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
410
|
+
fs.writeFileSync(
|
|
411
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
412
|
+
'module.exports = { name: "test-module", value: 123 };'
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
const script = `
|
|
416
|
+
const testMod = require('test-module');
|
|
417
|
+
bru.setVar('result', testMod.name + '-' + testMod.value);
|
|
418
|
+
`;
|
|
419
|
+
|
|
420
|
+
const context = {
|
|
421
|
+
bru: { setVar: jest.fn() },
|
|
422
|
+
console: console
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
426
|
+
|
|
427
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'test-module-123');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should handle npm module with dependencies', async () => {
|
|
431
|
+
// Create a mock npm module with internal dependencies
|
|
432
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'parent-module');
|
|
433
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
434
|
+
fs.writeFileSync(
|
|
435
|
+
path.join(nodeModulesDir, 'helper.js'),
|
|
436
|
+
'module.exports = { helper: true };'
|
|
437
|
+
);
|
|
438
|
+
fs.writeFileSync(
|
|
439
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
440
|
+
'const helper = require("./helper"); module.exports = { hasHelper: helper.helper };'
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const script = `
|
|
444
|
+
const parentMod = require('parent-module');
|
|
445
|
+
bru.setVar('result', parentMod.hasHelper);
|
|
446
|
+
`;
|
|
447
|
+
|
|
448
|
+
const context = {
|
|
449
|
+
bru: { setVar: jest.fn() },
|
|
450
|
+
console: console
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
454
|
+
|
|
455
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should provide bru object to npm modules', async () => {
|
|
459
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'bru-access-module');
|
|
460
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
461
|
+
fs.writeFileSync(
|
|
462
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
463
|
+
`module.exports = {
|
|
464
|
+
getEnvVar: function(name) { return bru.getEnvVar(name); },
|
|
465
|
+
setVar: function(name, value) { bru.setVar(name, value); }
|
|
466
|
+
};`
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const script = `
|
|
470
|
+
const bruModule = require('bru-access-module');
|
|
471
|
+
const envValue = bruModule.getEnvVar('TEST_VAR');
|
|
472
|
+
bruModule.setVar('result', envValue);
|
|
473
|
+
`;
|
|
474
|
+
|
|
475
|
+
const getEnvVarMock = jest.fn().mockReturnValue('test-value');
|
|
476
|
+
const setVarMock = jest.fn();
|
|
477
|
+
const context = {
|
|
478
|
+
bru: {
|
|
479
|
+
getEnvVar: getEnvVarMock,
|
|
480
|
+
setVar: setVarMock
|
|
481
|
+
},
|
|
482
|
+
console: console
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
486
|
+
|
|
487
|
+
expect(getEnvVarMock).toHaveBeenCalledWith('TEST_VAR');
|
|
488
|
+
expect(setVarMock).toHaveBeenCalledWith('result', 'test-value');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should provide req object to npm modules', async () => {
|
|
492
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'req-access-module');
|
|
493
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
494
|
+
fs.writeFileSync(
|
|
495
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
496
|
+
`module.exports = {
|
|
497
|
+
getUrl: function() { return req.getUrl(); },
|
|
498
|
+
getMethod: function() { return req.getMethod(); },
|
|
499
|
+
setHeader: function(name, value) { req.setHeader(name, value); }
|
|
500
|
+
};`
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const script = `
|
|
504
|
+
const reqModule = require('req-access-module');
|
|
505
|
+
const url = reqModule.getUrl();
|
|
506
|
+
const method = reqModule.getMethod();
|
|
507
|
+
reqModule.setHeader('X-Custom', 'value');
|
|
508
|
+
bru.setVar('result', method + ':' + url);
|
|
509
|
+
`;
|
|
510
|
+
|
|
511
|
+
const setVarMock = jest.fn();
|
|
512
|
+
const getUrlMock = jest.fn().mockReturnValue('https://api.example.com');
|
|
513
|
+
const getMethodMock = jest.fn().mockReturnValue('POST');
|
|
514
|
+
const setHeaderMock = jest.fn();
|
|
515
|
+
const context = {
|
|
516
|
+
bru: { setVar: setVarMock },
|
|
517
|
+
req: {
|
|
518
|
+
getUrl: getUrlMock,
|
|
519
|
+
getMethod: getMethodMock,
|
|
520
|
+
setHeader: setHeaderMock
|
|
521
|
+
},
|
|
522
|
+
console: console
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
526
|
+
|
|
527
|
+
expect(getUrlMock).toHaveBeenCalled();
|
|
528
|
+
expect(getMethodMock).toHaveBeenCalled();
|
|
529
|
+
expect(setHeaderMock).toHaveBeenCalledWith('X-Custom', 'value');
|
|
530
|
+
expect(setVarMock).toHaveBeenCalledWith('result', 'POST:https://api.example.com');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should provide res object to npm modules', async () => {
|
|
534
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'res-access-module');
|
|
535
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
536
|
+
fs.writeFileSync(
|
|
537
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
538
|
+
`module.exports = {
|
|
539
|
+
getStatus: function() { return res.getStatus(); },
|
|
540
|
+
getBody: function() { return res.getBody(); },
|
|
541
|
+
getHeader: function(name) { return res.getHeader(name); }
|
|
542
|
+
};`
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const script = `
|
|
546
|
+
const resModule = require('res-access-module');
|
|
547
|
+
const status = resModule.getStatus();
|
|
548
|
+
const body = resModule.getBody();
|
|
549
|
+
const contentType = resModule.getHeader('content-type');
|
|
550
|
+
bru.setVar('result', status + ':' + contentType + ':' + body.message);
|
|
551
|
+
`;
|
|
552
|
+
|
|
553
|
+
const context = {
|
|
554
|
+
bru: { setVar: jest.fn() },
|
|
555
|
+
res: {
|
|
556
|
+
getStatus: jest.fn().mockReturnValue(200),
|
|
557
|
+
getBody: jest.fn().mockReturnValue({ message: 'success' }),
|
|
558
|
+
getHeader: jest.fn().mockReturnValue('application/json')
|
|
559
|
+
},
|
|
560
|
+
console: console
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
564
|
+
|
|
565
|
+
expect(context.res.getStatus).toHaveBeenCalled();
|
|
566
|
+
expect(context.res.getBody).toHaveBeenCalled();
|
|
567
|
+
expect(context.res.getHeader).toHaveBeenCalledWith('content-type');
|
|
568
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', '200:application/json:success');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should provide bru, req, res to nested npm module dependencies', async () => {
|
|
572
|
+
// Create parent module
|
|
573
|
+
const parentDir = path.join(collectionPath, 'node_modules', 'parent-ctx-module');
|
|
574
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
575
|
+
fs.writeFileSync(
|
|
576
|
+
path.join(parentDir, 'index.js'),
|
|
577
|
+
`const child = require('./child');
|
|
578
|
+
module.exports = { childResult: child.getData() };`
|
|
579
|
+
);
|
|
580
|
+
// Create child module that accesses context
|
|
581
|
+
fs.writeFileSync(
|
|
582
|
+
path.join(parentDir, 'child.js'),
|
|
583
|
+
`module.exports = {
|
|
584
|
+
getData: function() {
|
|
585
|
+
return {
|
|
586
|
+
envVar: bru.getEnvVar('NESTED_VAR'),
|
|
587
|
+
reqUrl: req.getUrl(),
|
|
588
|
+
resStatus: res.getStatus()
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
};`
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const script = `
|
|
595
|
+
const parent = require('parent-ctx-module');
|
|
596
|
+
const data = parent.childResult;
|
|
597
|
+
bru.setVar('result', data.envVar + '|' + data.reqUrl + '|' + data.resStatus);
|
|
598
|
+
`;
|
|
599
|
+
|
|
600
|
+
const getEnvVarMock = jest.fn().mockReturnValue('nested-value');
|
|
601
|
+
const setVarMock = jest.fn();
|
|
602
|
+
const getUrlMock = jest.fn().mockReturnValue('https://nested.example.com');
|
|
603
|
+
const getStatusMock = jest.fn().mockReturnValue(201);
|
|
604
|
+
const context = {
|
|
605
|
+
bru: {
|
|
606
|
+
getEnvVar: getEnvVarMock,
|
|
607
|
+
setVar: setVarMock
|
|
608
|
+
},
|
|
609
|
+
req: {
|
|
610
|
+
getUrl: getUrlMock
|
|
611
|
+
},
|
|
612
|
+
res: {
|
|
613
|
+
getStatus: getStatusMock
|
|
614
|
+
},
|
|
615
|
+
console: console
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
619
|
+
|
|
620
|
+
expect(getEnvVarMock).toHaveBeenCalledWith('NESTED_VAR');
|
|
621
|
+
expect(getUrlMock).toHaveBeenCalled();
|
|
622
|
+
expect(getStatusMock).toHaveBeenCalled();
|
|
623
|
+
expect(setVarMock).toHaveBeenCalledWith('result', 'nested-value|https://nested.example.com|201');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe('CommonJS module patterns', () => {
|
|
627
|
+
it('should handle module.exports = object pattern', async () => {
|
|
628
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-object');
|
|
629
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
630
|
+
fs.writeFileSync(
|
|
631
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
632
|
+
'module.exports = { foo: "bar", num: 42 };'
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const script = `
|
|
636
|
+
const mod = require('cjs-object');
|
|
637
|
+
bru.setVar('result', mod.foo + '-' + mod.num);
|
|
638
|
+
`;
|
|
639
|
+
|
|
640
|
+
const context = {
|
|
641
|
+
bru: { setVar: jest.fn() },
|
|
642
|
+
console: console
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
646
|
+
|
|
647
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'bar-42');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should handle module.exports = function pattern', async () => {
|
|
651
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-function');
|
|
652
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
653
|
+
fs.writeFileSync(
|
|
654
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
655
|
+
'module.exports = function(x) { return x * 2; };'
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
const script = `
|
|
659
|
+
const double = require('cjs-function');
|
|
660
|
+
bru.setVar('result', double(21));
|
|
661
|
+
`;
|
|
662
|
+
|
|
663
|
+
const context = {
|
|
664
|
+
bru: { setVar: jest.fn() },
|
|
665
|
+
console: console
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
669
|
+
|
|
670
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 42);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('should handle module.exports = class pattern', async () => {
|
|
674
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-class');
|
|
675
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
676
|
+
fs.writeFileSync(
|
|
677
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
678
|
+
`class Calculator {
|
|
679
|
+
constructor(val) { this.val = val; }
|
|
680
|
+
add(x) { return this.val + x; }
|
|
681
|
+
}
|
|
682
|
+
module.exports = Calculator;`
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
const script = `
|
|
686
|
+
const Calculator = require('cjs-class');
|
|
687
|
+
const calc = new Calculator(10);
|
|
688
|
+
bru.setVar('result', calc.add(5));
|
|
689
|
+
`;
|
|
690
|
+
|
|
691
|
+
const context = {
|
|
692
|
+
bru: { setVar: jest.fn() },
|
|
693
|
+
console: console
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
697
|
+
|
|
698
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 15);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('should handle exports.property pattern', async () => {
|
|
702
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-exports');
|
|
703
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
704
|
+
fs.writeFileSync(
|
|
705
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
706
|
+
`exports.add = function(a, b) { return a + b; };
|
|
707
|
+
exports.multiply = function(a, b) { return a * b; };
|
|
708
|
+
exports.VERSION = '1.0.0';`
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
const script = `
|
|
712
|
+
const math = require('cjs-exports');
|
|
713
|
+
bru.setVar('result', math.add(2, 3) + '-' + math.multiply(4, 5) + '-' + math.VERSION);
|
|
714
|
+
`;
|
|
715
|
+
|
|
716
|
+
const context = {
|
|
717
|
+
bru: { setVar: jest.fn() },
|
|
718
|
+
console: console
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
722
|
+
|
|
723
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', '5-20-1.0.0');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('should handle mixed module.exports and exports pattern', async () => {
|
|
727
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-mixed');
|
|
728
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
729
|
+
fs.writeFileSync(
|
|
730
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
731
|
+
`// module.exports takes precedence
|
|
732
|
+
exports.ignored = 'this will be ignored';
|
|
733
|
+
module.exports = { actual: 'value' };`
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const script = `
|
|
737
|
+
const mod = require('cjs-mixed');
|
|
738
|
+
bru.setVar('result', mod.actual + '-' + (mod.ignored || 'undefined'));
|
|
739
|
+
`;
|
|
740
|
+
|
|
741
|
+
const context = {
|
|
742
|
+
bru: { setVar: jest.fn() },
|
|
743
|
+
console: console
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
747
|
+
|
|
748
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'value-undefined');
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
describe('File extension handling', () => {
|
|
753
|
+
it('should load .cjs files as CommonJS', async () => {
|
|
754
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-ext-module');
|
|
755
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
756
|
+
fs.writeFileSync(
|
|
757
|
+
path.join(nodeModulesDir, 'package.json'),
|
|
758
|
+
'{"name": "cjs-ext-module", "main": "index.cjs"}'
|
|
759
|
+
);
|
|
760
|
+
fs.writeFileSync(
|
|
761
|
+
path.join(nodeModulesDir, 'index.cjs'),
|
|
762
|
+
'module.exports = { format: "cjs", value: 100 };'
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
const script = `
|
|
766
|
+
const mod = require('cjs-ext-module');
|
|
767
|
+
bru.setVar('result', mod.format + '-' + mod.value);
|
|
768
|
+
`;
|
|
769
|
+
|
|
770
|
+
const context = {
|
|
771
|
+
bru: { setVar: jest.fn() },
|
|
772
|
+
console: console
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
776
|
+
|
|
777
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'cjs-100');
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('should fail when loading .mjs files (ES modules)', async () => {
|
|
781
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'mjs-ext-module');
|
|
782
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
783
|
+
fs.writeFileSync(
|
|
784
|
+
path.join(nodeModulesDir, 'package.json'),
|
|
785
|
+
'{"name": "mjs-ext-module", "main": "index.mjs"}'
|
|
786
|
+
);
|
|
787
|
+
fs.writeFileSync(
|
|
788
|
+
path.join(nodeModulesDir, 'index.mjs'),
|
|
789
|
+
'export default { format: "esm" };'
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const script = `
|
|
793
|
+
const mod = require('mjs-ext-module');
|
|
794
|
+
`;
|
|
795
|
+
|
|
796
|
+
const context = { console: console };
|
|
797
|
+
|
|
798
|
+
await expect(
|
|
799
|
+
runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
|
|
800
|
+
).rejects.toThrow();
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('should load module with package.json main field', async () => {
|
|
804
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'custom-main');
|
|
805
|
+
fs.mkdirSync(path.join(nodeModulesDir, 'lib'), { recursive: true });
|
|
806
|
+
fs.writeFileSync(
|
|
807
|
+
path.join(nodeModulesDir, 'package.json'),
|
|
808
|
+
'{"name": "custom-main", "main": "lib/entry.js"}'
|
|
809
|
+
);
|
|
810
|
+
fs.writeFileSync(
|
|
811
|
+
path.join(nodeModulesDir, 'lib', 'entry.js'),
|
|
812
|
+
'module.exports = { entry: "custom-main-lib" };'
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
const script = `
|
|
816
|
+
const mod = require('custom-main');
|
|
817
|
+
bru.setVar('result', mod.entry);
|
|
818
|
+
`;
|
|
819
|
+
|
|
820
|
+
const context = {
|
|
821
|
+
bru: { setVar: jest.fn() },
|
|
822
|
+
console: console
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
826
|
+
|
|
827
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'custom-main-lib');
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('should require relative .cjs files within npm module', async () => {
|
|
831
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-relative');
|
|
832
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
833
|
+
fs.writeFileSync(
|
|
834
|
+
path.join(nodeModulesDir, 'helper.cjs'),
|
|
835
|
+
'module.exports = { helperValue: "from-cjs" };'
|
|
836
|
+
);
|
|
837
|
+
fs.writeFileSync(
|
|
838
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
839
|
+
'const helper = require("./helper.cjs"); module.exports = helper;'
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
const script = `
|
|
843
|
+
const mod = require('cjs-relative');
|
|
844
|
+
bru.setVar('result', mod.helperValue);
|
|
845
|
+
`;
|
|
846
|
+
|
|
847
|
+
const context = {
|
|
848
|
+
bru: { setVar: jest.fn() },
|
|
849
|
+
console: console
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
853
|
+
|
|
854
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'from-cjs');
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('should load .json files directly', async () => {
|
|
858
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'json-direct');
|
|
859
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
860
|
+
fs.writeFileSync(
|
|
861
|
+
path.join(nodeModulesDir, 'package.json'),
|
|
862
|
+
'{"name": "json-direct", "main": "data.json"}'
|
|
863
|
+
);
|
|
864
|
+
fs.writeFileSync(
|
|
865
|
+
path.join(nodeModulesDir, 'data.json'),
|
|
866
|
+
'{"type": "json-main", "count": 42}'
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
const script = `
|
|
870
|
+
const data = require('json-direct');
|
|
871
|
+
bru.setVar('result', data.type + '-' + data.count);
|
|
872
|
+
`;
|
|
873
|
+
|
|
874
|
+
const context = {
|
|
875
|
+
bru: { setVar: jest.fn() },
|
|
876
|
+
console: console
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
880
|
+
|
|
881
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'json-main-42');
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
describe('JSON file handling', () => {
|
|
886
|
+
it('should load JSON files from npm modules', async () => {
|
|
887
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'json-module');
|
|
888
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
889
|
+
fs.writeFileSync(
|
|
890
|
+
path.join(nodeModulesDir, 'config.json'),
|
|
891
|
+
'{"name": "test-config", "version": "1.0.0", "enabled": true}'
|
|
892
|
+
);
|
|
893
|
+
fs.writeFileSync(
|
|
894
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
895
|
+
'const config = require("./config.json"); module.exports = config;'
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
const script = `
|
|
899
|
+
const config = require('json-module');
|
|
900
|
+
bru.setVar('result', config.name + '-' + config.version + '-' + config.enabled);
|
|
901
|
+
`;
|
|
902
|
+
|
|
903
|
+
const context = {
|
|
904
|
+
bru: { setVar: jest.fn() },
|
|
905
|
+
console: console
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
909
|
+
|
|
910
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'test-config-1.0.0-true');
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('should handle nested JSON requires', async () => {
|
|
914
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'nested-json');
|
|
915
|
+
fs.mkdirSync(path.join(nodeModulesDir, 'data'), { recursive: true });
|
|
916
|
+
fs.writeFileSync(
|
|
917
|
+
path.join(nodeModulesDir, 'data', 'schema.json'),
|
|
918
|
+
'{"type": "object", "properties": {"id": {"type": "number"}}}'
|
|
919
|
+
);
|
|
920
|
+
fs.writeFileSync(
|
|
921
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
922
|
+
'const schema = require("./data/schema.json"); module.exports = { schema };'
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
const script = `
|
|
926
|
+
const mod = require('nested-json');
|
|
927
|
+
bru.setVar('result', mod.schema.type + '-' + mod.schema.properties.id.type);
|
|
928
|
+
`;
|
|
929
|
+
|
|
930
|
+
const context = {
|
|
931
|
+
bru: { setVar: jest.fn() },
|
|
932
|
+
console: console
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
936
|
+
|
|
937
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'object-number');
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
describe('Node.js globals in npm modules', () => {
|
|
942
|
+
it('should have access to Buffer', async () => {
|
|
943
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'buffer-module');
|
|
944
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
945
|
+
fs.writeFileSync(
|
|
946
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
947
|
+
`module.exports = {
|
|
948
|
+
encode: function(str) { return Buffer.from(str).toString('base64'); },
|
|
949
|
+
decode: function(b64) { return Buffer.from(b64, 'base64').toString('utf8'); }
|
|
950
|
+
};`
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
const script = `
|
|
954
|
+
const bufMod = require('buffer-module');
|
|
955
|
+
const encoded = bufMod.encode('hello');
|
|
956
|
+
const decoded = bufMod.decode(encoded);
|
|
957
|
+
bru.setVar('result', encoded + '-' + decoded);
|
|
958
|
+
`;
|
|
959
|
+
|
|
960
|
+
const context = {
|
|
961
|
+
bru: { setVar: jest.fn() },
|
|
962
|
+
console: console
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
966
|
+
|
|
967
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'aGVsbG8=-hello');
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it('should have access to URL and URLSearchParams', async () => {
|
|
971
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'url-module');
|
|
972
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
973
|
+
fs.writeFileSync(
|
|
974
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
975
|
+
`module.exports = {
|
|
976
|
+
parseUrl: function(urlStr) {
|
|
977
|
+
const url = new URL(urlStr);
|
|
978
|
+
return url.hostname;
|
|
979
|
+
},
|
|
980
|
+
buildQuery: function(params) {
|
|
981
|
+
const search = new URLSearchParams(params);
|
|
982
|
+
return search.toString();
|
|
983
|
+
}
|
|
984
|
+
};`
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
const script = `
|
|
988
|
+
const urlMod = require('url-module');
|
|
989
|
+
bru.setVar('result', urlMod.parseUrl('https://example.com/path'));
|
|
990
|
+
`;
|
|
991
|
+
|
|
992
|
+
const context = {
|
|
993
|
+
bru: { setVar: jest.fn() },
|
|
994
|
+
console: console
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
998
|
+
|
|
999
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'example.com');
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it('should have access to setTimeout/clearTimeout', async () => {
|
|
1003
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'timer-module');
|
|
1004
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
1005
|
+
fs.writeFileSync(
|
|
1006
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
1007
|
+
`module.exports = {
|
|
1008
|
+
hasTimers: function() {
|
|
1009
|
+
return typeof setTimeout === 'function' && typeof clearTimeout === 'function';
|
|
1010
|
+
}
|
|
1011
|
+
};`
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
const script = `
|
|
1015
|
+
const timerMod = require('timer-module');
|
|
1016
|
+
bru.setVar('result', timerMod.hasTimers());
|
|
1017
|
+
`;
|
|
1018
|
+
|
|
1019
|
+
const context = {
|
|
1020
|
+
bru: { setVar: jest.fn() },
|
|
1021
|
+
console: console
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
1025
|
+
|
|
1026
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
describe('Error handling', () => {
|
|
1031
|
+
it('should throw error for non-existent module', async () => {
|
|
1032
|
+
const script = `
|
|
1033
|
+
const mod = require('non-existent-module-xyz');
|
|
1034
|
+
`;
|
|
1035
|
+
|
|
1036
|
+
const context = { console: console };
|
|
1037
|
+
|
|
1038
|
+
await expect(
|
|
1039
|
+
runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
|
|
1040
|
+
).rejects.toThrow('Could not resolve module');
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it('should throw error for module with syntax error', async () => {
|
|
1044
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'syntax-error-module');
|
|
1045
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
1046
|
+
fs.writeFileSync(
|
|
1047
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
1048
|
+
'module.exports = { invalid syntax here'
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
const script = `
|
|
1052
|
+
const mod = require('syntax-error-module');
|
|
1053
|
+
`;
|
|
1054
|
+
|
|
1055
|
+
const context = { console: console };
|
|
1056
|
+
|
|
1057
|
+
await expect(
|
|
1058
|
+
runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
|
|
1059
|
+
).rejects.toThrow();
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it('should throw error for module with runtime error', async () => {
|
|
1063
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules', 'runtime-error-module');
|
|
1064
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
1065
|
+
fs.writeFileSync(
|
|
1066
|
+
path.join(nodeModulesDir, 'index.js'),
|
|
1067
|
+
'throw new Error("Module initialization failed");'
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
const script = `
|
|
1071
|
+
const mod = require('runtime-error-module');
|
|
1072
|
+
`;
|
|
1073
|
+
|
|
1074
|
+
const context = { console: console };
|
|
1075
|
+
|
|
1076
|
+
await expect(
|
|
1077
|
+
runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
|
|
1078
|
+
).rejects.toThrow('Module initialization failed');
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
describe('context isolation', () => {
|
|
1084
|
+
it('should have global pointing to isolated context (not host)', async () => {
|
|
1085
|
+
const context = {
|
|
1086
|
+
bru: { setVar: jest.fn() },
|
|
1087
|
+
console: console
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// global exists but points to isolated context, so global.bru should exist
|
|
1091
|
+
// process is a sanitized object in the isolated context
|
|
1092
|
+
const script = `bru.setVar('result', typeof global.bru === 'object' && typeof global.process === 'object')`;
|
|
1093
|
+
|
|
1094
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
1095
|
+
|
|
1096
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it('should not have access to host fs module via globalThis', async () => {
|
|
1100
|
+
const context = {
|
|
1101
|
+
bru: { setVar: jest.fn() },
|
|
1102
|
+
console: console
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
const script = `bru.setVar('result', typeof globalThis.fs)`;
|
|
1106
|
+
|
|
1107
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
1108
|
+
|
|
1109
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'undefined');
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
it('should throw ReferenceError for undeclared variables', async () => {
|
|
1113
|
+
const context = { console: console };
|
|
1114
|
+
|
|
1115
|
+
const script = `const x = someUndeclaredVar`;
|
|
1116
|
+
|
|
1117
|
+
await expect(
|
|
1118
|
+
runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
|
|
1119
|
+
).rejects.toThrow('someUndeclaredVar is not defined');
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it('should have access to context objects via globalThis', async () => {
|
|
1123
|
+
const context = {
|
|
1124
|
+
bru: { setVar: jest.fn() },
|
|
1125
|
+
req: { url: 'http://test.com' },
|
|
1126
|
+
console: console
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
const script = `bru.setVar('result', typeof globalThis.req)`;
|
|
1130
|
+
|
|
1131
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
1132
|
+
|
|
1133
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'object');
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it('should have access to allowed globals like Buffer', async () => {
|
|
1137
|
+
const context = {
|
|
1138
|
+
bru: { setVar: jest.fn() },
|
|
1139
|
+
console: console
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
const script = `bru.setVar('result', typeof globalThis.Buffer)`;
|
|
1143
|
+
|
|
1144
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
1145
|
+
|
|
1146
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it('should have access to process object with nextTick', async () => {
|
|
1150
|
+
const context = {
|
|
1151
|
+
bru: { setVar: jest.fn() },
|
|
1152
|
+
console: console
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
const script = `
|
|
1156
|
+
const hasSafeProps = typeof process.version === 'string' && typeof process.platform === 'string';
|
|
1157
|
+
const hasNextTick = typeof process.nextTick === 'function';
|
|
1158
|
+
bru.setVar('result', hasSafeProps && hasNextTick);
|
|
1159
|
+
`;
|
|
1160
|
+
|
|
1161
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
1162
|
+
|
|
1163
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
it('should work with Array.isArray across context boundaries', async () => {
|
|
1167
|
+
const context = {
|
|
1168
|
+
bru: { setVar: jest.fn() },
|
|
1169
|
+
console: console
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
const script = `
|
|
1173
|
+
const arr = [1, 2, 3];
|
|
1174
|
+
bru.setVar('result', Array.isArray(arr));
|
|
1175
|
+
`;
|
|
1176
|
+
|
|
1177
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
1178
|
+
|
|
1179
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it('should have working Object methods', async () => {
|
|
1183
|
+
const context = {
|
|
1184
|
+
bru: { setVar: jest.fn() },
|
|
1185
|
+
console: console
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
const script = `
|
|
1189
|
+
const obj = { a: 1, b: 2 };
|
|
1190
|
+
bru.setVar('result', Object.keys(obj).join(','));
|
|
1191
|
+
`;
|
|
1192
|
+
|
|
1193
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
1194
|
+
|
|
1195
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'a,b');
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
});
|