@usebruno/js 0.44.0 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -106,6 +106,43 @@ describe('node-vm sandbox', () => {
106
106
  runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
107
107
  ).rejects.toThrow('Access to files outside of the allowed context roots is not allowed');
108
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
+ });
109
146
  });
110
147
 
111
148
  describe('createCustomRequire - additionalContextRoots', () => {
@@ -225,28 +262,937 @@ describe('node-vm sandbox', () => {
225
262
 
226
263
  describe('createCustomRequire - module caching', () => {
227
264
  it('should cache loaded modules', async () => {
228
- let callCount = 0;
265
+ // Module increments a counter each time it's executed
266
+ // If caching works, counter should only be 1 after multiple requires
229
267
  fs.writeFileSync(
230
268
  path.join(collectionPath, 'cached.js'),
231
269
  `
232
- module.exports = { count: ${++callCount} };
270
+ if (!global._cacheTestCount) global._cacheTestCount = 0;
271
+ global._cacheTestCount++;
272
+ module.exports = { id: Date.now() };
233
273
  `
234
274
  );
235
275
 
236
276
  const script = `
237
277
  const mod1 = require('./cached');
238
278
  const mod2 = require('./cached');
239
- bru.setVar('same', mod1.count === mod2.count);
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));
240
1175
  `;
241
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 () => {
242
1183
  const context = {
243
1184
  bru: { setVar: jest.fn() },
244
1185
  console: console
245
1186
  };
246
1187
 
1188
+ const script = `
1189
+ const obj = { a: 1, b: 2 };
1190
+ bru.setVar('result', Object.keys(obj).join(','));
1191
+ `;
1192
+
247
1193
  await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
248
1194
 
249
- expect(context.bru.setVar).toHaveBeenCalledWith('same', true);
1195
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 'a,b');
250
1196
  });
251
1197
  });
252
1198
  });