From 8ebf68c4dd19d4acd4e1c164e1617b29c6a783a4 Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Tue, 23 Jan 2024 16:48:04 +0530 Subject: [PATCH 01/11] Add pm.require API --- lib/postman-sandbox.js | 2 + lib/sandbox/execute.js | 2 + lib/sandbox/pm-require.js | 91 ++++ lib/sandbox/pmapi.js | 7 +- package-lock.json | 5 +- package.json | 2 +- .../unit/sandbox-libraries/pm-require.test.js | 458 ++++++++++++++++++ 7 files changed, 561 insertions(+), 6 deletions(-) create mode 100644 lib/sandbox/pm-require.js create mode 100644 test/unit/sandbox-libraries/pm-require.test.js diff --git a/lib/postman-sandbox.js b/lib/postman-sandbox.js index 2b7b9c45..50568ae7 100644 --- a/lib/postman-sandbox.js +++ b/lib/postman-sandbox.js @@ -93,6 +93,7 @@ class PostmanSandbox extends UniversalVM { executionEventName = 'execution.result.' + id, executionTimeout = _.get(options, 'timeout', this.executionTimeout), cursor = _.clone(_.get(options, 'cursor', {})), // clone the cursor as it travels through IPC for mutation + resolvedPackages = _.get(options, 'resolvedPackages'), debugMode = _.has(options, 'debug') ? options.debug : this.debug; let waiting; @@ -126,6 +127,7 @@ class PostmanSandbox extends UniversalVM { cursor: cursor, debug: debugMode, timeout: executionTimeout, + resolvedPackages: resolvedPackages, legacy: _.get(options, 'legacy') }); } diff --git a/lib/sandbox/execute.js b/lib/sandbox/execute.js index ac2260fe..909ec00a 100644 --- a/lib/sandbox/execute.js +++ b/lib/sandbox/execute.js @@ -9,6 +9,7 @@ const _ = require('lodash'), PostmanTimers = require('./timers'), PostmanAPI = require('./pmapi'), PostmanCookieStore = require('./cookie-store'), + createPostmanRequire = require('./pm-require'), EXECUTION_RESULT_EVENT_BASE = 'execution.result.', EXECUTION_REQUEST_EVENT_BASE = 'execution.request.', @@ -228,6 +229,7 @@ module.exports = function (bridge, glob) { }, dispatchAssertions, new PostmanCookieStore(id, bridge, timers), + createPostmanRequire(options.resolvedPackages, scope), { disabledAPIs: initializationOptions.disabledAPIs }) diff --git a/lib/sandbox/pm-require.js b/lib/sandbox/pm-require.js new file mode 100644 index 00000000..6a1ec7ca --- /dev/null +++ b/lib/sandbox/pm-require.js @@ -0,0 +1,91 @@ +const MODULE_KEY = '__module_obj', // why not use `module`? + MODULE_WRAPPER = [ + '(function (exports, module) {\n', + `\n})(${MODULE_KEY}.exports, ${MODULE_KEY});` + ]; + +class PostmanRequireStore { + constructor (fileCache) { + this.fileCache = fileCache || {}; + } + + getResolvedPath (path) { + if (this.hasFile(path)) { + return path; + } + } + + hasFile (path) { + return Boolean(this.fileCache[path]); + } + + getFileData (path) { + return this.hasFile(path) && this.fileCache[path].data; + } +} + +function createPostmanRequire (fileCache, scope) { + const store = new PostmanRequireStore(fileCache), + cache = {}; + + return function postmanRequire (name) { + const path = store.getResolvedPath(name); + + // Any module should not be evaluated twice, so we use it from the + // cache. If there's a circular dependency, the partially evaluated + // module will be returned from the cache. + if (cache[path]) { + // Always use the resolved path as the ID of the module. This + // ensures that relative paths are handled correctly. + return cache[path].exports; + } + + /* eslint-disable-next-line one-var */ + const file = path && store.getFileData(path); + + if (!file) { + // Error should contain the name exactly as the user specified, + // and not the resolved path. + throw new Error(`Cannot find module '${name}'`); + } + + /* eslint-disable-next-line one-var */ + const moduleObj = { + id: path, + exports: {} + }; + + // Add to cache before executing. This ensures that any dependency + // that tries to import it's parent/ancestor gets the cached + // version and not end up in infinite loop. + cache[moduleObj.id] = moduleObj; + + /* eslint-disable-next-line one-var */ + const wrappedModule = MODULE_WRAPPER[0] + file + MODULE_WRAPPER[1]; + + scope.import({ + [MODULE_KEY]: moduleObj + }); + + // Note: We're executing the code in the same scope as the one + // which called the `pm.require` function. This is because we want + // to share the global scope across all the required modules. Any + // locals are available inside the required modules and any locals + // created inside the required modules are available to the parent. + // + // Why `async` = true? + // - We want to allow execution of async code like setTimeout etc. + scope.exec(wrappedModule, true, (err) => { + // Bubble up the error to be caught as execution error + if (err) { + throw err; + } + }); + + scope.unset(MODULE_KEY); + + return moduleObj.exports; + }; +} + +module.exports = createPostmanRequire; diff --git a/lib/sandbox/pmapi.js b/lib/sandbox/pmapi.js index db514c82..665dcc68 100644 --- a/lib/sandbox/pmapi.js +++ b/lib/sandbox/pmapi.js @@ -47,10 +47,11 @@ const _ = require('lodash'), * @param {Function} onSkipRequest - callback to execute when pm.execution.skipRequest() called * @param {Function} onAssertion - callback to execute when pm.expect() called * @param {Object} cookieStore - cookie store + * @param {Function} require - require * @param {Object} [options] - options * @param {Array.} [options.disabledAPIs] - list of disabled APIs */ -function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, options = {}) { +function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, require, options = {}) { // @todo - ensure runtime passes data in a scope format let iterationData = new VariableScope(); @@ -291,7 +292,9 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, */ current: execution.legacy._eventItemName }) - } + }, + + require: require }, options.disabledAPIs); // extend pm api with test runner abilities diff --git a/package-lock.json b/package-lock.json index 446ff81d..6971621e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6310,9 +6310,8 @@ "dev": true }, "uniscope": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/uniscope/-/uniscope-2.0.1.tgz", - "integrity": "sha512-qjEYNHjlJUbP87C8nVhAVFDrg2tt46Nqitkl+mOCukNz5tLtQCY0ifLR46VDyo7coaq+cD+BZi9CE/0WDjuWOg==", + "version": "github:postmanlabs/uniscope#17f4bb25e9b7694b5a0d36854694b2e7f8207d0e", + "from": "github:postmanlabs/uniscope#feat/options-reset-locals", "dev": true }, "universalify": { diff --git a/package.json b/package.json index a3fef1dd..a100b927 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "terser": "^5.24.0", "tsd-jsdoc": "^2.5.0", "tv4": "1.3.0", - "uniscope": "2.0.1", + "uniscope": "github:postmanlabs/uniscope#feat/options-reset-locals", "watchify": "^4.0.0", "xml2js": "0.4.23" }, diff --git a/test/unit/sandbox-libraries/pm-require.test.js b/test/unit/sandbox-libraries/pm-require.test.js new file mode 100644 index 00000000..ae4a0d5e --- /dev/null +++ b/test/unit/sandbox-libraries/pm-require.test.js @@ -0,0 +1,458 @@ +describe('sandbox library - pm.require api', function () { + this.timeout(1000 * 60); + var Sandbox = require('../../../'), + + sampleContextData = { + globals: [{ + key: 'var1', + value: 'one' + }, { + key: 'var2', + value: 2, + type: 'number' + }], + environment: [{ + key: 'var1', + value: 'one-env' + }, { + key: 'var2', + value: 2.5, + type: 'number' + }], + collectionVariables: [{ + key: 'var1', + value: 'collection-var1', + type: 'string' + }, { + key: 'var2', + value: 2.9, + type: 'number' + }], + data: { + var1: 'one-data' + } + }, + context; + + beforeEach(function (done) { + Sandbox.createContext({ debug: true }, function (err, ctx) { + context = ctx; + done(err); + }); + }); + + afterEach(function () { + context.dispose(); + context = null; + }); + + it('should be an exposed function', function (done) { + context.execute(` + var assert = require('assert'); + assert.strictEqual((typeof pm.require), 'function'); + `, { context: sampleContextData }, done); + }); + + it('should return the exports from the file', function (done) { + context.execute(` + var assert = require('assert'); + var mod = pm.require('mod1'); + assert.strictEqual(mod.a, 123); + assert.strictEqual(mod.b, 456); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: 'module.exports = { a: 123, b: 456 }' + } + } + }, done); + }); + + it('should throw error if required file is not found', function (done) { + context.execute(` + var assert = require('assert'); + try { + pm.require('mod1'); + } + catch (e) { + assert.strictEqual(e.message, "Cannot find module 'mod1'"); + } + `, { context: sampleContextData }, done); + }); + + it('should throw error if required file throws error', function (done) { + context.execute(` + var assert = require('assert'); + try { + pm.require('mod1'); + throw new Error('expected to throw'); + } + catch (e) { + assert.strictEqual(e.message, "my error"); + } + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + name: 'mod1', + data: 'throw new Error("my error");' + } + } + }, done); + }); + + it('should allow required files to access globals', function (done) { + context.execute(` + var assert = require('assert'); + var1 = 123; // global + var mod = pm.require('mod1'); + assert.strictEqual(mod.a, 123); + assert.strictEqual(mod.b, 456); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + var2 = 456; + module.exports = { a: var1, b: pm.require('mod2') }; + ` + }, + mod2: { + data: ` + module.exports = var2; + ` + } + } + }, done); + }); + + it('should allow setting globals from required files', function (done) { + context.execute(` + var assert = require('assert'); + pm.require('mod1'); + assert.strictEqual(var1, 123); + assert.strictEqual(var2, 456); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + var1 = 123; + var2 = 456; + ` + } + } + }, done); + }); + + it('should allow required files to access pm.* apis', function (done) { + context.execute(` + var assert = require('assert'); + var mod = pm.require('mod1'); + assert.strictEqual(mod.a, "one-env"); + assert.strictEqual(mod.b, 2); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + module.exports = { + a: pm.environment.get("var1"), + b: pm.globals.get("var2") + }; + ` + } + } + }, done); + }); + + it('should allow required files to require other files', function (done) { + context.execute(` + var assert = require('assert'); + var mod = pm.require('mod1'); + assert.strictEqual(mod.a, 123); + assert.strictEqual(mod.b, 345); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + module.exports = { + a: 123, + b: pm.require("mod2") + } + ` + }, + mod2: { + data: ` + module.exports = 345; + ` + } + } + }, done); + }); + + it('should not evaluate the same file twice', function (done) { + context.execute(` + var assert = require('assert'); + var mod1 = pm.require('mod1'); + var mod2 = pm.require('mod1'); + assert.strictEqual(mod1, mod2); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + module.exports = { a: 123 }; + ` + } + } + }, done); + }); + + it('should allow circular dependencies', function (done) { + context.execute(` + var assert = require('assert'); + var mod1 = pm.require('mod1'); + assert.strictEqual(mod1.a, 123); + assert.strictEqual(mod1.b, 123); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + module.exports.a = 123; + module.exports.b = pm.require("mod2"); + ` + }, + mod2: { + data: ` + module.exports = pm.require("mod1").a; + ` + } + } + }, done); + }); + + it('should allow required file to require itself', function (done) { + context.execute(` + var assert = require('assert'); + var mod1 = pm.require('mod1'); + assert.strictEqual(mod1.a, 123); + assert.strictEqual(mod1.b, 123); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + module.exports.a = 123; + module.exports.b = pm.require("mod1").a; + ` + } + } + }, done); + }); + + // TODO: fixit + it.skip('should not have access to __module_obj', function (done) { + context.execute(` + var assert = require('assert'); + try { + const val = pm.require('mod1'); + throw new Error('should not reach here'); + } + catch (e) { + assert.strictEqual(e.message, "__module_obj is not defined"); + } + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + module.exports = __module_obj; // __module_obj should not be defined + ` + } + } + }, done); + }); + + it('should allow async operations', function (done) { + const errorSpy = sinon.stub(); + + context.on('execution.error', errorSpy); + context.execute(` + const assert = require('assert'); + const mod1 = pm.require('mod1'); + + assert.strictEqual(mod1.a, 123); + assert.strictEqual(mod1.b, undefined); // should not be updated yet + + setTimeout(() => { + assert.strictEqual(mod1.a, 123); + assert.strictEqual(mod1.b, 456); + }, 10); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + module.exports.a = 123; + setTimeout(function () { + module.exports.b = 456; + }, 10); + ` + } + } + }, function (err) { + if (err) { + return done(err); + } + + expect(errorSpy, 'there was an error in the script').to.not.have.been.called; + done(); + }); + }); + + it('should catch errors in async code', function (done) { + const errorSpy = sinon.stub(); + + context.on('execution.error', errorSpy); + context.execute(` + pm.require('mod1'); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + setTimeout(function () { + pm.require('mod2'); + }, 10); + ` + }, + mod2: { + data: ` + setTimeout(function () { + throw new Error('my async error'); + }, 10); + ` + } + } + }, function (err) { + if (err) { + return done(err); + } + + expect(errorSpy, 'there was no error in the script').to.have.been.calledOnce; + expect(errorSpy.firstCall.args[1]).to.have.property('message', 'my async error'); + done(); + }); + }); + + it('should keep the references for the locals instead of values', function (done) { + const errorSpy = sinon.stub(); + + context.on('execution.error', errorSpy); + context.execute(` + const assert = require('assert'); + + a = 1; + b = 1; + obj = { + a: 1, + b: 1 + }; + + pm.require('mod1'); + pm.require('mod2'); + + assert(a === 2); + assert(obj.a === 2); + + setTimeout(function () { + assert(b === 2); + assert(obj.b === 2); + }, 3); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + const assert = require('assert'); + + assert(a === 1); + assert(obj.a === 1); + + a = 2; + obj.a = 2; + + setTimeout(function () { + assert(b === 1); + assert(obj.b === 1); + + b = 2; + obj.b = 2; + }, 1); + ` + }, + mod2: { + data: ` + const assert = require('assert'); + + assert.strictEqual(obj.a, 2, 'sync variables by reference not updated'); + assert.strictEqual(a, 2, 'sync variables by value not updated'); + + setTimeout(function () { + assert.strictEqual(obj.b, 2, 'async variables by reference not updated'); + assert.strictEqual(b, 2, 'async variables by value not updated'); + }, 2); + ` + } + } + }, function (err) { + if (err) { + return done(err); + } + + expect(errorSpy).to.not.have.been.called; + done(); + }); + }); + + it('should make "module" available in the required file', function (done) { + context.execute(` + pm.require('mod1'); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + var assert = require('assert'); + assert.ok(module); + assert.ok(module.exports); + assert.strictEqual(module.exports, exports); + assert.strictEqual(module.id, 'mod1'); + ` + } + } + }, done); + }); + + it('should not have access to cache', function (done) { + context.execute(` + var assert = require('assert'); + var mod1 = pm.require('mod1'); + + assert.strictEqual(pm.require.cache, undefined); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: ` + module.exports = { a: 123 }; + ` + } + } + }, done); + }); +}); From 6a0bcbf63229109274833e4fb92d25e5f72df052 Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Mon, 19 Feb 2024 10:57:56 +0530 Subject: [PATCH 02/11] fix: case when resolvedPackages is not passed --- lib/sandbox/pm-require.js | 4 ++++ .../unit/sandbox-libraries/pm-require.test.js | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/sandbox/pm-require.js b/lib/sandbox/pm-require.js index 6a1ec7ca..7a180fa0 100644 --- a/lib/sandbox/pm-require.js +++ b/lib/sandbox/pm-require.js @@ -25,6 +25,10 @@ class PostmanRequireStore { } function createPostmanRequire (fileCache, scope) { + if (!fileCache) { + return; + } + const store = new PostmanRequireStore(fileCache), cache = {}; diff --git a/test/unit/sandbox-libraries/pm-require.test.js b/test/unit/sandbox-libraries/pm-require.test.js index ae4a0d5e..40b86d46 100644 --- a/test/unit/sandbox-libraries/pm-require.test.js +++ b/test/unit/sandbox-libraries/pm-require.test.js @@ -50,7 +50,19 @@ describe('sandbox library - pm.require api', function () { context.execute(` var assert = require('assert'); assert.strictEqual((typeof pm.require), 'function'); - `, { context: sampleContextData }, done); + `, { + context: sampleContextData, + resolvedPackages: {} + }, done); + }); + + it('should not be a function if resolvedPackages is not present', function (done) { + context.execute(` + var assert = require('assert'); + assert.strictEqual((typeof pm.require), 'undefined'); + `, { + context: sampleContextData + }, done); }); it('should return the exports from the file', function (done) { @@ -78,7 +90,10 @@ describe('sandbox library - pm.require api', function () { catch (e) { assert.strictEqual(e.message, "Cannot find module 'mod1'"); } - `, { context: sampleContextData }, done); + `, { + context: sampleContextData, + resolvedPackages: {} + }, done); }); it('should throw error if required file throws error', function (done) { From e55287612c8dfd3fb3ccb8393ad93bae5ca94c1c Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Mon, 19 Feb 2024 11:03:34 +0530 Subject: [PATCH 03/11] Add JSDocs for pm.require --- lib/sandbox/pm-require.js | 47 +++++++++++++++++++++++++++++++++++++-- lib/sandbox/pmapi.js | 10 ++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/sandbox/pm-require.js b/lib/sandbox/pm-require.js index 7a180fa0..863acbd9 100644 --- a/lib/sandbox/pm-require.js +++ b/lib/sandbox/pm-require.js @@ -4,26 +4,63 @@ const MODULE_KEY = '__module_obj', // why not use `module`? `\n})(${MODULE_KEY}.exports, ${MODULE_KEY});` ]; +/** + * Cache of all files that are available to be required. + * + * @typedef {Record} FileCache + */ + class PostmanRequireStore { + /** + * @param {FileCache} fileCache - fileCache + */ constructor (fileCache) { - this.fileCache = fileCache || {}; + this.fileCache = fileCache; } + /** + * @param {string} path - path + * @returns {string|undefined} - resolved path + */ getResolvedPath (path) { if (this.hasFile(path)) { return path; } } + /** + * @param {string} path - path + * @returns {boolean} + */ hasFile (path) { return Boolean(this.fileCache[path]); } + /** + * @param {string} path - path + * @returns {string|undefined} + */ getFileData (path) { return this.hasFile(path) && this.fileCache[path].data; } } +/** + * @param {FileCache} fileCache - fileCache + * @param {Object} scope - scope + * @returns {Function} - postmanRequire + * @example + * const fileCache = { + * 'path/to/file.js': { + * data: 'module.exports = { foo: "bar" };' + * } + * }; + * + * const postmanRequire = createPostmanRequire(fileCache, scope); + * + * const module = postmanRequire('path/to/file.js'); + * console.log(module.foo); // bar + */ function createPostmanRequire (fileCache, scope) { if (!fileCache) { return; @@ -32,7 +69,11 @@ function createPostmanRequire (fileCache, scope) { const store = new PostmanRequireStore(fileCache), cache = {}; - return function postmanRequire (name) { + /** + * @param {string} name - name + * @returns {any} - module + */ + function postmanRequire (name) { const path = store.getResolvedPath(name); // Any module should not be evaluated twice, so we use it from the @@ -90,6 +131,8 @@ function createPostmanRequire (fileCache, scope) { return moduleObj.exports; }; + + return postmanRequire; } module.exports = createPostmanRequire; diff --git a/lib/sandbox/pmapi.js b/lib/sandbox/pmapi.js index 665dcc68..6f87ff72 100644 --- a/lib/sandbox/pmapi.js +++ b/lib/sandbox/pmapi.js @@ -47,11 +47,11 @@ const _ = require('lodash'), * @param {Function} onSkipRequest - callback to execute when pm.execution.skipRequest() called * @param {Function} onAssertion - callback to execute when pm.expect() called * @param {Object} cookieStore - cookie store - * @param {Function} require - require + * @param {Function} requireFn - requireFn * @param {Object} [options] - options * @param {Array.} [options.disabledAPIs] - list of disabled APIs */ -function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, require, options = {}) { +function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, requireFn, options = {}) { // @todo - ensure runtime passes data in a scope format let iterationData = new VariableScope(); @@ -294,7 +294,11 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, }) }, - require: require + /** + * @param {String} name - name + * @returns {any} - module + */ + require: requireFn }, options.disabledAPIs); // extend pm api with test runner abilities From 8f5a2e6a9c3b7d6bf1de2907584ae412a2986b44 Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Wed, 21 Feb 2024 12:49:25 +0530 Subject: [PATCH 04/11] Allow sending custom error from client --- lib/sandbox/pm-require.js | 78 ++++++++++++++----- .../unit/sandbox-libraries/pm-require.test.js | 20 +++++ 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/lib/sandbox/pm-require.js b/lib/sandbox/pm-require.js index 863acbd9..29ab78f6 100644 --- a/lib/sandbox/pm-require.js +++ b/lib/sandbox/pm-require.js @@ -7,7 +7,7 @@ const MODULE_KEY = '__module_obj', // why not use `module`? /** * Cache of all files that are available to be required. * - * @typedef {Record} FileCache + * @typedef {Record} FileCache */ class PostmanRequireStore { @@ -15,10 +15,36 @@ class PostmanRequireStore { * @param {FileCache} fileCache - fileCache */ constructor (fileCache) { + if (!fileCache) { + throw new Error('File cache is required'); + } + this.fileCache = fileCache; } /** + * Check if the file is available in the cache. + * + * @param {string} path - path + * @returns {boolean} + */ + hasFile (path) { + return Boolean(this.getFile(path)); + } + + /** + * Get the file from the cache. + * + * @param {string} path - path + * @returns {Object|undefined} - file + */ + getFile (path) { + return this.fileCache[path]; + } + + /** + * Get the resolved path for the file. + * * @param {string} path - path * @returns {string|undefined} - resolved path */ @@ -29,19 +55,33 @@ class PostmanRequireStore { } /** + * Get the file data. + * + * @param {string} path - path + * @returns {string|undefined} + */ + getFileData (path) { + return this.hasFile(path) && this.getFile(path).data; + } + + /** + * Check if the file has an error. + * * @param {string} path - path * @returns {boolean} */ - hasFile (path) { - return Boolean(this.fileCache[path]); + hasError (path) { + return this.hasFile(path) && Boolean(this.getFile(path).error); } /** + * Get the file error. + * * @param {string} path - path * @returns {string|undefined} */ - getFileData (path) { - return this.hasFile(path) && this.fileCache[path].data; + getFileError (path) { + return this.hasError(path) && this.getFile(path).error; } } @@ -76,6 +116,16 @@ function createPostmanRequire (fileCache, scope) { function postmanRequire (name) { const path = store.getResolvedPath(name); + if (!path) { + // Error should contain the name exactly as the user specified, + // and not the resolved path. + throw new Error(`Cannot find module '${name}'`); + } + + if (store.hasError(path)) { + throw new Error(`Error while loading module '${name}': ${store.getFileError(path)}`); + } + // Any module should not be evaluated twice, so we use it from the // cache. If there's a circular dependency, the partially evaluated // module will be returned from the cache. @@ -86,19 +136,11 @@ function createPostmanRequire (fileCache, scope) { } /* eslint-disable-next-line one-var */ - const file = path && store.getFileData(path); - - if (!file) { - // Error should contain the name exactly as the user specified, - // and not the resolved path. - throw new Error(`Cannot find module '${name}'`); - } - - /* eslint-disable-next-line one-var */ - const moduleObj = { - id: path, - exports: {} - }; + const file = store.getFileData(path), + moduleObj = { + id: path, + exports: {} + }; // Add to cache before executing. This ensures that any dependency // that tries to import it's parent/ancestor gets the cached diff --git a/test/unit/sandbox-libraries/pm-require.test.js b/test/unit/sandbox-libraries/pm-require.test.js index 40b86d46..b470369c 100644 --- a/test/unit/sandbox-libraries/pm-require.test.js +++ b/test/unit/sandbox-libraries/pm-require.test.js @@ -96,6 +96,26 @@ describe('sandbox library - pm.require api', function () { }, done); }); + it('should throw custom error if required file has error', function (done) { + context.execute(` + var assert = require('assert'); + try { + pm.require('mod1'); + throw new Error('should not reach here'); + } + catch (e) { + assert.strictEqual(e.message, "Error while loading module 'mod1': my error"); + } + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + error: 'my error' + } + } + }, done); + }); + it('should throw error if required file throws error', function (done) { context.execute(` var assert = require('assert'); From 729359295ba8850952680f3ef8bc581696262296 Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Wed, 21 Feb 2024 12:51:35 +0530 Subject: [PATCH 05/11] Fix lint --- lib/sandbox/pm-require.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sandbox/pm-require.js b/lib/sandbox/pm-require.js index 29ab78f6..4d4ec7a3 100644 --- a/lib/sandbox/pm-require.js +++ b/lib/sandbox/pm-require.js @@ -172,7 +172,7 @@ function createPostmanRequire (fileCache, scope) { scope.unset(MODULE_KEY); return moduleObj.exports; - }; + } return postmanRequire; } From 4b1a0e8c2be682a1955e4c8fe73627c91721d074 Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Wed, 21 Feb 2024 13:17:29 +0530 Subject: [PATCH 06/11] Add require to disabledAPIs if no resolvedPackages --- lib/sandbox/execute.js | 12 +++++++++--- lib/sandbox/pm-require.js | 6 +----- lib/sandbox/pmapi.js | 10 +++++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/sandbox/execute.js b/lib/sandbox/execute.js index 909ec00a..4e7b8ee6 100644 --- a/lib/sandbox/execute.js +++ b/lib/sandbox/execute.js @@ -121,6 +121,10 @@ module.exports = function (bridge, glob) { // create the execution object execution = new Execution(id, event, context, { ...options, initializeExecution }), + disabledAPIs = [ + ...(initializationOptions.disabledAPIs || []) + ], + /** * Dispatch assertions from `pm.test` or legacy `test` API. * @@ -206,6 +210,10 @@ module.exports = function (bridge, glob) { timers.clearEvent(id, err, res); }); + if (!options.resolvedPackages) { + disabledAPIs.push('require'); + } + // send control to the function that executes the context and prepares the scope executeContext(scope, code, execution, // if a console is sent, we use it. otherwise this also prevents erroneous referencing to any console @@ -230,9 +238,7 @@ module.exports = function (bridge, glob) { dispatchAssertions, new PostmanCookieStore(id, bridge, timers), createPostmanRequire(options.resolvedPackages, scope), - { - disabledAPIs: initializationOptions.disabledAPIs - }) + { disabledAPIs }) ), dispatchAssertions, { disableLegacyAPIs: initializationOptions.disableLegacyAPIs }); diff --git a/lib/sandbox/pm-require.js b/lib/sandbox/pm-require.js index 4d4ec7a3..e4abf13e 100644 --- a/lib/sandbox/pm-require.js +++ b/lib/sandbox/pm-require.js @@ -102,11 +102,7 @@ class PostmanRequireStore { * console.log(module.foo); // bar */ function createPostmanRequire (fileCache, scope) { - if (!fileCache) { - return; - } - - const store = new PostmanRequireStore(fileCache), + const store = new PostmanRequireStore(fileCache || {}), cache = {}; /** diff --git a/lib/sandbox/pmapi.js b/lib/sandbox/pmapi.js index 6f87ff72..95bdc1dc 100644 --- a/lib/sandbox/pmapi.js +++ b/lib/sandbox/pmapi.js @@ -295,10 +295,14 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, }, /** - * @param {String} name - name - * @returns {any} - module + * Imports a package in the script. + * + * @param {String} name - name of the module + * @returns {any} - exports from the module */ - require: requireFn + require: function (name) { + return requireFn(name); + } }, options.disabledAPIs); // extend pm api with test runner abilities From e08f0ff387bc9c1161ebe0da7c353c0294d0d3a4 Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Thu, 22 Feb 2024 12:48:24 +0530 Subject: [PATCH 07/11] Fix invalid JSDoc --- lib/sandbox/pm-require.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sandbox/pm-require.js b/lib/sandbox/pm-require.js index e4abf13e..5d2c55ee 100644 --- a/lib/sandbox/pm-require.js +++ b/lib/sandbox/pm-require.js @@ -7,7 +7,7 @@ const MODULE_KEY = '__module_obj', // why not use `module`? /** * Cache of all files that are available to be required. * - * @typedef {Record} FileCache + * @typedef {Object.} FileCache */ class PostmanRequireStore { From 1990a1be324cd089e5696045d09d5883d55ad3c6 Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Thu, 22 Feb 2024 12:36:06 +0530 Subject: [PATCH 08/11] Update types --- types/index.d.ts | 75 ++++++++++++++++++++++++++++++++++- types/sandbox/prerequest.d.ts | 11 ++++- types/sandbox/test.d.ts | 11 ++++- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 11ab88fb..6eb931e8 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,4 +1,4 @@ -// Type definitions for postman-sandbox 4.3.0 +// Type definitions for postman-sandbox 4.4.0 // Project: https://github.com/postmanlabs/postman-sandbox // Definitions by: PostmanLabs // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped @@ -86,17 +86,82 @@ declare type Return = { nextRequest: any; }; +/** + * Cache of all files that are available to be required. + */ +declare type FileCache = { + [key: string]: { data: string; } | { error: string; }; +}; + +/** + * @param fileCache - fileCache + */ +declare class PostmanRequireStore { + constructor(fileCache: FileCache); + /** + * Check if the file is available in the cache. + * @param path - path + */ + hasFile(path: string): boolean; + /** + * Get the file from the cache. + * @param path - path + * @returns - file + */ + getFile(path: string): any | undefined; + /** + * Get the resolved path for the file. + * @param path - path + * @returns - resolved path + */ + getResolvedPath(path: string): string | undefined; + /** + * Get the file data. + * @param path - path + */ + getFileData(path: string): string | undefined; + /** + * Check if the file has an error. + * @param path - path + */ + hasError(path: string): boolean; + /** + * Get the file error. + * @param path - path + */ + getFileError(path: string): string | undefined; +} + +/** + * @example + * const fileCache = { + * 'path/to/file.js': { + * data: 'module.exports = { foo: "bar" };' + * } + * }; + * + * const postmanRequire = createPostmanRequire(fileCache, scope); + * + * const module = postmanRequire('path/to/file.js'); + * console.log(module.foo); // bar + * @param fileCache - fileCache + * @param scope - scope + * @returns - postmanRequire + */ +declare function createPostmanRequire(fileCache: FileCache, scope: any): (...params: any[]) => any; + /** * @param execution - execution context * @param onRequest - callback to execute when pm.sendRequest() called * @param onSkipRequest - callback to execute when pm.execution.skipRequest() called * @param onAssertion - callback to execute when pm.expect() called * @param cookieStore - cookie store + * @param requireFn - requireFn * @param [options] - options * @param [options.disabledAPIs] - list of disabled APIs */ declare class Postman { - constructor(execution: Execution, onRequest: (...params: any[]) => any, onSkipRequest: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, options?: { + constructor(execution: Execution, onRequest: (...params: any[]) => any, onSkipRequest: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, requireFn: (...params: any[]) => any, options?: { disabledAPIs?: string[]; }); /** @@ -140,6 +205,12 @@ declare class Postman { * Exposes handlers to control or access execution state */ execution: Execution; + /** + * Imports a package in the script. + * @param name - name of the module + * @returns - exports from the module + */ + require(name: string): any; expect: Chai.ExpectStatic; } diff --git a/types/sandbox/prerequest.d.ts b/types/sandbox/prerequest.d.ts index af7bf282..d8946e5a 100644 --- a/types/sandbox/prerequest.d.ts +++ b/types/sandbox/prerequest.d.ts @@ -1,4 +1,4 @@ -// Type definitions for postman-sandbox 4.3.0 +// Type definitions for postman-sandbox 4.4.0 // Project: https://github.com/postmanlabs/postman-sandbox // Definitions by: PostmanLabs // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped @@ -20,11 +20,12 @@ declare interface PostmanLegacy { * @param onSkipRequest - callback to execute when pm.execution.skipRequest() called * @param onAssertion - callback to execute when pm.expect() called * @param cookieStore - cookie store + * @param requireFn - requireFn * @param [options] - options * @param [options.disabledAPIs] - list of disabled APIs */ declare class Postman { - constructor(execution: Execution, onRequest: (...params: any[]) => any, onSkipRequest: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, options?: { + constructor(execution: Execution, onRequest: (...params: any[]) => any, onSkipRequest: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, requireFn: (...params: any[]) => any, options?: { disabledAPIs?: string[]; }); /** @@ -63,6 +64,12 @@ declare class Postman { * Exposes handlers to control or access execution state */ execution: Execution; + /** + * Imports a package in the script. + * @param name - name of the module + * @returns - exports from the module + */ + require(name: string): any; expect: Chai.ExpectStatic; } diff --git a/types/sandbox/test.d.ts b/types/sandbox/test.d.ts index 42924417..16d8177e 100644 --- a/types/sandbox/test.d.ts +++ b/types/sandbox/test.d.ts @@ -1,4 +1,4 @@ -// Type definitions for postman-sandbox 4.3.0 +// Type definitions for postman-sandbox 4.4.0 // Project: https://github.com/postmanlabs/postman-sandbox // Definitions by: PostmanLabs // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped @@ -20,11 +20,12 @@ declare interface PostmanLegacy { * @param onSkipRequest - callback to execute when pm.execution.skipRequest() called * @param onAssertion - callback to execute when pm.expect() called * @param cookieStore - cookie store + * @param requireFn - requireFn * @param [options] - options * @param [options.disabledAPIs] - list of disabled APIs */ declare class Postman { - constructor(execution: Execution, onRequest: (...params: any[]) => any, onSkipRequest: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, options?: { + constructor(execution: Execution, onRequest: (...params: any[]) => any, onSkipRequest: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, requireFn: (...params: any[]) => any, options?: { disabledAPIs?: string[]; }); /** @@ -69,6 +70,12 @@ declare class Postman { * Exposes handlers to control or access execution state */ execution: Execution; + /** + * Imports a package in the script. + * @param name - name of the module + * @returns - exports from the module + */ + require(name: string): any; expect: Chai.ExpectStatic; } From bcd65e19e234521b652b0713f81e60c5be0aeaf5 Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Fri, 23 Feb 2024 11:43:41 +0530 Subject: [PATCH 09/11] Add a test for empty package file --- test/unit/sandbox-libraries/pm-require.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/unit/sandbox-libraries/pm-require.test.js b/test/unit/sandbox-libraries/pm-require.test.js index b470369c..164a248e 100644 --- a/test/unit/sandbox-libraries/pm-require.test.js +++ b/test/unit/sandbox-libraries/pm-require.test.js @@ -137,6 +137,21 @@ describe('sandbox library - pm.require api', function () { }, done); }); + it('should not throw error if file is empty', function (done) { + context.execute(` + var assert = require('assert'); + var mod = pm.require('mod1'); + assert.deepEqual(mod, {}); + `, { + context: sampleContextData, + resolvedPackages: { + mod1: { + data: '' + } + } + }, done); + }); + it('should allow required files to access globals', function (done) { context.execute(` var assert = require('assert'); From 7c31d889e5d4dbd510974e5632a023f62344467c Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Tue, 27 Feb 2024 17:41:39 +0530 Subject: [PATCH 10/11] Update CHANGELOG --- CHANGELOG.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.yaml b/CHANGELOG.yaml index a7533a66..bb28e8d4 100644 --- a/CHANGELOG.yaml +++ b/CHANGELOG.yaml @@ -1,3 +1,7 @@ +unreleased: + new features: + - GH-976 Add `pm.require` API to use packages inside scripts + 4.4.0: date: 2023-11-18 new features: From 552088e232023e3bcc766f10b47760702b388bc1 Mon Sep 17 00:00:00 2001 From: Utkarsh Maheshwari Date: Wed, 28 Feb 2024 14:53:13 +0530 Subject: [PATCH 11/11] Bump postman-collection and uvm --- package-lock.json | 11 ++++++----- package.json | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6971621e..d0cb854a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5167,9 +5167,9 @@ } }, "postman-collection": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.3.0.tgz", - "integrity": "sha512-QpmNOw1JhAVQTFWRz443/qpKs4/3T1MFrKqDZ84RS1akxOzhXXr15kD8+/+jeA877qyy9rfMsrFgLe2W7aCPjw==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.4.0.tgz", + "integrity": "sha512-2BGDFcUwlK08CqZFUlIC8kwRJueVzPjZnnokWPtJCd9f2J06HBQpGL7t2P1Ud1NEsK9NHq9wdipUhWLOPj5s/Q==", "requires": { "@faker-js/faker": "5.5.3", "file-type": "3.9.0", @@ -6310,8 +6310,9 @@ "dev": true }, "uniscope": { - "version": "github:postmanlabs/uniscope#17f4bb25e9b7694b5a0d36854694b2e7f8207d0e", - "from": "github:postmanlabs/uniscope#feat/options-reset-locals", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uniscope/-/uniscope-2.1.0.tgz", + "integrity": "sha512-nFkkwVnj/sLUgLxffkJXTHEOCY7uuVHItzmEyEibL9OdtTrXRhd8uHBAS6kgTUrmDPpKZlz/Ts/CdEgqj6o+Tg==", "dev": true }, "universalify": { diff --git a/package.json b/package.json index a100b927..da680202 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ }, "dependencies": { "lodash": "4.17.21", - "postman-collection": "4.3.0", + "postman-collection": "4.4.0", "teleport-javascript": "1.0.0", "uvm": "2.1.1" }, @@ -92,7 +92,7 @@ "terser": "^5.24.0", "tsd-jsdoc": "^2.5.0", "tv4": "1.3.0", - "uniscope": "github:postmanlabs/uniscope#feat/options-reset-locals", + "uniscope": "2.1.0", "watchify": "^4.0.0", "xml2js": "0.4.23" },