From 5d008550bf6dc9b267f09205ec3b8e366cc336a7 Mon Sep 17 00:00:00 2001 From: Vedanta Krishna Date: Thu, 7 Sep 2023 19:21:01 +0530 Subject: [PATCH 1/7] chore: add channel-beta postman collection dependency --- lib/sandbox/console.js | 7 +- lib/sandbox/cookie-store.js | 7 +- lib/sandbox/dispatch-event.js | 15 ++++ lib/sandbox/execute.js | 49 ++++++----- lib/sandbox/execution.js | 1 + lib/sandbox/pmapi.js | 7 +- test/unit/sandbox-libraries/pm.test.js | 116 +++++++++++++++++++++++++ 7 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 lib/sandbox/dispatch-event.js diff --git a/lib/sandbox/console.js b/lib/sandbox/console.js index f7c8a561..ee4243ad 100644 --- a/lib/sandbox/console.js +++ b/lib/sandbox/console.js @@ -1,3 +1,5 @@ +const dispatchEvent = require('./dispatch-event'); + var teleportJS = require('teleport-javascript'), arrayProtoSlice = Array.prototype.slice, @@ -45,7 +47,7 @@ function replacer (key, value) { return value; } -function PostmanConsole (emitter, cursor, originalConsole) { +function PostmanConsole (emitter, cursor, originalConsole, execution) { const dispatch = function (level) { // create a dispatch function that emits events const args = arrayProtoSlice.call(arguments, 1); @@ -54,7 +56,8 @@ function PostmanConsole (emitter, cursor, originalConsole) { originalConsole[level].apply(originalConsole, args); } - emitter.dispatch(CONSOLE_EVENT, cursor, level, teleportJS.stringify(args, replacer)); + + dispatchEvent(emitter, execution, CONSOLE_EVENT, cursor, level, teleportJS.stringify(args, replacer)); }; // setup variants of the logger based on log levels diff --git a/lib/sandbox/cookie-store.js b/lib/sandbox/cookie-store.js index e45281d1..8ac189d3 100644 --- a/lib/sandbox/cookie-store.js +++ b/lib/sandbox/cookie-store.js @@ -1,3 +1,5 @@ +const dispatchEvent = require('./dispatch-event'); + const _ = require('lodash'), Store = require('@postman/tough-cookie').Store, @@ -14,12 +16,13 @@ const _ = require('lodash'), arrayProtoSlice = Array.prototype.slice; class PostmanCookieStore extends Store { - constructor (id, emitter, timers) { + constructor (id, emitter, timers, execution) { super(); this.id = id; // execution identifier this.emitter = emitter; this.timers = timers; + this.execution = execution; } } @@ -77,7 +80,7 @@ STORE_METHODS.forEach(function (method) { // Refer: https://github.com/postmanlabs/postman-app-support/issues/11064 setTimeout(() => { // finally, dispatch event over the bridge - this.emitter.dispatch(eventName, eventId, EVENT_STORE_ACTION, method, args); + dispatchEvent(this.emitter, this.execution, eventName, eventId, EVENT_STORE_ACTION, method, args); }); }; }); diff --git a/lib/sandbox/dispatch-event.js b/lib/sandbox/dispatch-event.js new file mode 100644 index 00000000..2a4bdcf7 --- /dev/null +++ b/lib/sandbox/dispatch-event.js @@ -0,0 +1,15 @@ +/** + * + * @param {any} bridge - the bridge object + * @param {{ shouldSkipExecution: boolean }} execution - the execution object + * @param {string} event - the event name + * @param {...any} args - the arguments to be passed to the event + */ +module.exports = function dispatchEvent (bridge, execution, event, ...args) { + // if the execution is skipped, do not dispatch the event + if (execution && execution.shouldSkipExecution) { + return; + } + + bridge.dispatch(event, ...args); +}; diff --git a/lib/sandbox/execute.js b/lib/sandbox/execute.js index e2321ff5..894246c1 100644 --- a/lib/sandbox/execute.js +++ b/lib/sandbox/execute.js @@ -20,7 +20,8 @@ const _ = require('lodash'), EXECUTION_ASSERTION_EVENT = 'execution.assertion', EXECUTION_ASSERTION_EVENT_BASE = 'execution.assertion.', - executeContext = require('./execute-context'); + executeContext = require('./execute-context'), + dispatchEvent = require('./dispatch-event'); module.exports = function (bridge, glob) { // @note we use a common scope for all executions. this causes issues when scripts are run inside the sandbox @@ -49,7 +50,7 @@ module.exports = function (bridge, glob) { if (!template) { chai.use(require('chai-postman')(sdk, _, Ajv)); - return bridge.dispatch('initialize'); + return dispatchEvent(bridge, null, 'initialize'); } const _module = { exports: {} }, @@ -66,7 +67,7 @@ module.exports = function (bridge, glob) { scope.exec(template, (err) => { if (err) { - return bridge.dispatch('initialize', err); + return dispatchEvent(bridge, null, 'initialize', err); } const { chaiPlugin, initializeExecution: setupExecution } = (_module && _module.exports) || {}; @@ -79,7 +80,7 @@ module.exports = function (bridge, glob) { initializeExecution = setupExecution; } - bridge.dispatch('initialize'); + dispatchEvent(bridge, null, 'initialize'); }); }); @@ -97,7 +98,8 @@ module.exports = function (bridge, glob) { */ bridge.on('execute', function (id, event, context, options) { if (!(id && _.isString(id))) { - return bridge.dispatch('error', new Error('sandbox: execution identifier parameter(s) missing')); + return dispatchEvent(bridge, null, 'error', + new Error('sandbox: execution identifier parameter(s) missing')); } !options && (options = {}); @@ -114,9 +116,9 @@ module.exports = function (bridge, glob) { // extract the code from event. The event can be the code itself and we know that if the event is of type // string. - code = _.isFunction(event.script && event.script.toSource) && event.script.toSource(), + code = _.isFunction(event.script && event.script.toSource) && event.script.toSource(); // create the execution object - execution = new Execution(id, event, context, { ...options, initializeExecution }), + let execution = new Execution(id, event, context, { ...options, initializeExecution }), /** * Dispatch assertions from `pm.test` or legacy `test` API. @@ -136,11 +138,10 @@ module.exports = function (bridge, glob) { // For compatibility, dispatch the single assertion as an array. !Array.isArray(assertions) && (assertions = [assertions]); - bridge.dispatch(assertionEventName, options.cursor, assertions); - bridge.dispatch(EXECUTION_ASSERTION_EVENT, options.cursor, assertions); - }; - - let waiting, + dispatchEvent(bridge, execution, assertionEventName, options.cursor, assertions); + dispatchEvent(bridge, execution, EXECUTION_ASSERTION_EVENT, options.cursor, assertions); + }, + waiting, timers; execution.return.async = false; @@ -148,8 +149,8 @@ module.exports = function (bridge, glob) { // create the controlled timers timers = new PostmanTimers(null, function (err) { if (err) { // propagate the error out of sandbox - bridge.dispatch(errorEventName, options.cursor, err); - bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err); + dispatchEvent(bridge, execution, errorEventName, options.cursor, err); + dispatchEvent(bridge, execution, EXECUTION_ERROR_EVENT, options.cursor, err); } }, function () { execution.return.async = true; @@ -169,8 +170,8 @@ module.exports = function (bridge, glob) { bridge.off(cookiesEventName); if (err) { // fire extra execution error event - bridge.dispatch(errorEventName, options.cursor, err); - bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err); + dispatchEvent(bridge, execution, options.cursor, err); + dispatchEvent(bridge, execution, options.cursor, err); } // @note delete response from the execution object to avoid dispatching @@ -178,7 +179,8 @@ module.exports = function (bridge, glob) { execution.response && (delete execution.response); // fire the execution completion event - (dnd !== true) && bridge.dispatch(executionEventName, err || null, execution); + (dnd !== true) && dispatchEvent(bridge, { shouldSkipExecution: false }, + executionEventName, err || null, execution); }); // if a timeout is set, we must ensure that all pending timers are cleared and an execution timeout event is @@ -207,14 +209,21 @@ module.exports = function (bridge, glob) { executeContext(scope, code, execution, // if a console is sent, we use it. otherwise this also prevents erroneous referencing to any console // inside this closure. - (new PostmanConsole(bridge, options.cursor, options.debug && glob.console)), + (new PostmanConsole(bridge, options.cursor, options.debug && glob.console, execution)), timers, ( new PostmanAPI(execution, function (request, callback) { var eventId = timers.setEvent(callback); - bridge.dispatch(executionRequestEventName, options.cursor, id, eventId, request); - }, dispatchAssertions, new PostmanCookieStore(id, bridge, timers), { + dispatchEvent(bridge, execution, executionRequestEventName, options.cursor, id, eventId, request); + }, function () { + execution.shouldSkipExecution = true; + timers.terminate(null, true); + + // disable further edits on execution + execution = JSON.parse(JSON.stringify(execution)); + }, + dispatchAssertions, new PostmanCookieStore(id, bridge, timers, execution), { disabledAPIs: initializationOptions.disabledAPIs }) ), diff --git a/lib/sandbox/execution.js b/lib/sandbox/execution.js index ab521ac9..7f4c988f 100644 --- a/lib/sandbox/execution.js +++ b/lib/sandbox/execution.js @@ -27,6 +27,7 @@ class Execution { this.id = id; this.target = event.listen || PROPERTY.SCRIPT; this.legacy = options.legacy || {}; + this.shouldSkipExecution = false; this.cursor = _.isObject(options.cursor) ? options.cursor : {}; this.data = _.get(context, PROPERTY.DATA, {}); diff --git a/lib/sandbox/pmapi.js b/lib/sandbox/pmapi.js index 6e860f37..4d9cb681 100644 --- a/lib/sandbox/pmapi.js +++ b/lib/sandbox/pmapi.js @@ -44,12 +44,13 @@ const _ = require('lodash'), * * @param {Execution} execution - * @param {Function} onRequest - + * @param {Function} onSkipRequest - callback to execute when pm.skipRequest called * @param {Function} onAssertion - * @param {Object} cookieStore - * @param {Object} [options] - * @param {Array.} [options.disabledAPIs] - */ -function Postman (execution, onRequest, onAssertion, cookieStore, options = {}) { +function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, options = {}) { // @todo - ensure runtime passes data in a scope format let iterationData = new VariableScope(); @@ -273,6 +274,10 @@ function Postman (execution, onRequest, onAssertion, cookieStore, options = {}) return chai.expect(this).to; } }); + + Object.defineProperty(this.request, 'stopExecution', { + value: onSkipRequest + }); } iterationData = null; // precautionary diff --git a/test/unit/sandbox-libraries/pm.test.js b/test/unit/sandbox-libraries/pm.test.js index eeea411e..dd49ae72 100644 --- a/test/unit/sandbox-libraries/pm.test.js +++ b/test/unit/sandbox-libraries/pm.test.js @@ -278,6 +278,122 @@ describe('sandbox library - pm api', function () { }, done); }); + it('should not execute any line after pm.request.stopExecution in pre-request script', function (done) { + context.on('console', function (level, ...args) { + expect(args[1]).to.equal('pre-request log 1'); + }); + context.execute(` + preRequestScript: { + console.log('pre-request log 1'); + pm.request.stopExecution(); + console.log('pre-request log 2'); + } + `, { + timeout: 200, + context: { + request: 'https://postman-echo.com/get?foo=bar' + } + }, function (err, execution) { + if (err) { return done(err); } + expect(execution).to.include({ shouldSkipExecution: true }); + + return done(); + }); + }); + + it(`should not execute any line after pm.request.stopExecution in pre-request script, + even if the pm.request.stopExecution invoked inside a try catch block`, function (done) { + context.on('console', function (level, ...args) { + // console.log(args); + expect(args[1]).to.equal('pre-request log 1'); + }); + context.execute(` + preRequestScript: { + console.log('pre-request log 1'); + try { + pm.request.stopExecution(); + } catch (err) { + // ignore + } + console.log('pre-request log 2'); + } + `, { + timeout: 200, + context: { + request: 'https://postman-echo.com/get?foo=bar' + } + }, function (err, execution) { + if (err) { return done(err); } + expect(execution).to.include({ shouldSkipExecution: true }); + + return done(); + }); + }); + + it(`should not execute any line after pm.request.stopExecution in pre-request script, + even if the pm.request.stopExecution invoked inside an async function`, function (done) { + context.on('console', function (level, ...args) { + expect(args[1]).to.equal('pre-request log 1'); + }); + context.execute(` + preRequestScript: { + console.log('pre-request log 1'); + async function myAsyncFunction() { + pm.request.stopExecution(); + } + + myAsyncFunction(); + console.log('pre-request log 2'); + } + `, { + timeout: 200, + context: { + request: 'https://postman-echo.com/get?foo=bar' + } + }, function (err, execution) { + if (err) { return done(err); } + expect(execution).to.include({ shouldSkipExecution: true }); + + return done(); + }); + }); + + it('should not reflect any variable change line after pm.request.stopExecution in pre-request script', + function (done) { + context.on('console', function (level, ...args) { + expect(args[1]).to.equal('pre-request log 1'); + }); + context.execute(` + preRequestScript: { + async function myFun () { + console.log('pre-request log 1'); + + pm.variables.set('foo', 'bar'); + pm.request.stopExecution(); + new Promise((res) => setTimeout(res, 100)) + pm.variables.set('foo', 'nobar'); + console.log('pre-request log 2'); + } + + myFun(); + + } + `, { + timeout: 200, + context: { + request: 'https://postman-echo.com/get?foo=bar' + } + }, function (err, execution) { + if (err) { return done(err); } + expect(execution).to.include({ shouldSkipExecution: true }); + expect(execution).to.deep.nested.include({ '_variables.values': [ + { value: 'bar', key: 'foo', type: 'any' } + ] }); + + return done(); + }); + }); + it('when serialized should not have assertion helpers added by sandbox', function (done) { context.execute(` var assert = require('assert'), From 323de696cf23759522ac6a147870d11e53b20f93 Mon Sep 17 00:00:00 2001 From: Vedanta Krishna Date: Tue, 12 Sep 2023 10:12:35 +0530 Subject: [PATCH 2/7] chore: add jsdoc to new function --- lib/sandbox/pmapi.js | 17 ++++++++++- types/sandbox/prerequest.d.ts | 54 ++++++++++++++++++++++++++++++++--- types/sandbox/test.d.ts | 54 ++++++++++++++++++++++++++++++++--- 3 files changed, 116 insertions(+), 9 deletions(-) diff --git a/lib/sandbox/pmapi.js b/lib/sandbox/pmapi.js index 4d9cb681..2be21a75 100644 --- a/lib/sandbox/pmapi.js +++ b/lib/sandbox/pmapi.js @@ -150,12 +150,16 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, */ iterationData: iterationData, + /** + * @Interface IRequest + * @extends import("postman-collection").Request + */ /** * The request object inside pm is a representation of the request for which this script is being run. * For a pre-request script, this is the request that is about to be sent and when in a test script, * this is the representation of the request that was sent. * - * @type {Request} + * @type {IRequest} */ request: execution.request, @@ -275,6 +279,17 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, } }); + /** + * @property {Function} stopExecution - stops the execution of current request. + * NOTE: If called from a pre-request script, the request will not be sent. + * + * @example + * if (pm.environment.get("token")) { + * pm.request.stopExecution(); + * } + * + * @name IRequest#stopExecution + */ Object.defineProperty(this.request, 'stopExecution', { value: onSkipRequest }); diff --git a/types/sandbox/prerequest.d.ts b/types/sandbox/prerequest.d.ts index e12225cd..a596953c 100644 --- a/types/sandbox/prerequest.d.ts +++ b/types/sandbox/prerequest.d.ts @@ -1,4 +1,4 @@ -// Type definitions for postman-sandbox 3.5.7 +// Type definitions for postman-sandbox 4.2.7 // Project: https://github.com/postmanlabs/postman-sandbox // Definitions by: PostmanLabs // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped @@ -14,8 +14,19 @@ declare interface PostmanLegacy { setNextRequest(requestName: string): void; } +/** + * @param execution - - + * @param onRequest - - + * @param onSkipRequest - callback to execute when pm.skipRequest called + * @param onAssertion - - + * @param cookieStore - - + * @param [options] - - + * @param [options.disabledAPIs] - - + */ declare class Postman { - constructor(bridge: EventEmitter, execution: Execution, onRequest: (...params: any[]) => any, cookieStore: any); + constructor(execution: Execution, onRequest: (...params: any[]) => any, onSkipRequest: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, options?: { + disabledAPIs?: string[]; + }); /** * The pm.info object contains information pertaining to the script being executed. * Useful information such as the request name, request Id, and iteration count are @@ -35,7 +46,7 @@ declare class Postman { * For a pre-request script, this is the request that is about to be sent and when in a test script, * this is the representation of the request that was sent. */ - request: import("postman-collection").Request; + request: IRequest; /** * The cookies object contains a list of cookies that are associated with the domain * to which the request was made. @@ -44,6 +55,8 @@ declare class Postman { visualizer: Visualizer; /** * Allows one to send request from script asynchronously. + * @param req - - + * @param callback - - */ sendRequest(req: import("postman-collection").Request | string, callback: (...params: any[]) => any): void; expect: Chai.ExpectStatic; @@ -75,6 +88,20 @@ declare interface Info { requestId: string; } +declare interface IRequest extends import("postman-collection").Request { + /** + * @example + * if (pm.environment.get("token")) { + * pm.request.stopExecution(); + * } + * @property stopExecution - stops the execution of current request. + * NOTE: If called from a pre-request script, the request will not be sent. + */ + stopExecution: { + stopExecution: (...params: any[]) => any; + }; +} + declare interface Visualizer { /** * Set visualizer template and its options @@ -96,25 +123,44 @@ declare interface Visualizer { */ declare var pm: Postman; -declare interface PostmanCookieJar { +/** + * @param cookieStore - - + */ +declare class PostmanCookieJar { + constructor(cookieStore: any); /** * Get the cookie value with the given name. + * @param url - - + * @param name - - + * @param callback - - */ get(url: string, name: string, callback: (...params: any[]) => any): void; /** * Get all the cookies for the given URL. + * @param url - - + * @param [options] - - + * @param callback - - */ getAll(url: string, options?: any, callback: (...params: any[]) => any): void; /** * Set or update a cookie. + * @param url - - + * @param name - - + * @param [value] - - + * @param [callback] - - */ set(url: string, name: string | any, value?: string | ((...params: any[]) => any), callback?: (...params: any[]) => any): void; /** * Remove single cookie with the given name. + * @param url - - + * @param name - - + * @param [callback] - - */ unset(url: string, name: string, callback?: (...params: any[]) => any): void; /** * Remove all the cookies for the given URL. + * @param url - - + * @param [callback] - - */ clear(url: string, callback?: (...params: any[]) => any): void; } diff --git a/types/sandbox/test.d.ts b/types/sandbox/test.d.ts index f9fff448..8dec431e 100644 --- a/types/sandbox/test.d.ts +++ b/types/sandbox/test.d.ts @@ -1,4 +1,4 @@ -// Type definitions for postman-sandbox 3.5.7 +// Type definitions for postman-sandbox 4.2.7 // Project: https://github.com/postmanlabs/postman-sandbox // Definitions by: PostmanLabs // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped @@ -14,8 +14,19 @@ declare interface PostmanLegacy { setNextRequest(requestName: string): void; } +/** + * @param execution - - + * @param onRequest - - + * @param onSkipRequest - callback to execute when pm.skipRequest called + * @param onAssertion - - + * @param cookieStore - - + * @param [options] - - + * @param [options.disabledAPIs] - - + */ declare class Postman { - constructor(bridge: EventEmitter, execution: Execution, onRequest: (...params: any[]) => any, cookieStore: any); + constructor(execution: Execution, onRequest: (...params: any[]) => any, onSkipRequest: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, options?: { + disabledAPIs?: string[]; + }); /** * The pm.info object contains information pertaining to the script being executed. * Useful information such as the request name, request Id, and iteration count are @@ -35,7 +46,7 @@ declare class Postman { * For a pre-request script, this is the request that is about to be sent and when in a test script, * this is the representation of the request that was sent. */ - request: import("postman-collection").Request; + request: IRequest; /** * Inside the test scripts, the pm.response object contains all information pertaining * to the response that was received. @@ -49,6 +60,8 @@ declare class Postman { visualizer: Visualizer; /** * Allows one to send request from script asynchronously. + * @param req - - + * @param callback - - */ sendRequest(req: import("postman-collection").Request | string, callback: (...params: any[]) => any): void; expect: Chai.ExpectStatic; @@ -80,6 +93,20 @@ declare interface Info { requestId: string; } +declare interface IRequest extends import("postman-collection").Request { + /** + * @example + * if (pm.environment.get("token")) { + * pm.request.stopExecution(); + * } + * @property stopExecution - stops the execution of current request. + * NOTE: If called from a pre-request script, the request will not be sent. + */ + stopExecution: { + stopExecution: (...params: any[]) => any; + }; +} + declare interface Visualizer { /** * Set visualizer template and its options @@ -101,25 +128,44 @@ declare interface Visualizer { */ declare var pm: Postman; -declare interface PostmanCookieJar { +/** + * @param cookieStore - - + */ +declare class PostmanCookieJar { + constructor(cookieStore: any); /** * Get the cookie value with the given name. + * @param url - - + * @param name - - + * @param callback - - */ get(url: string, name: string, callback: (...params: any[]) => any): void; /** * Get all the cookies for the given URL. + * @param url - - + * @param [options] - - + * @param callback - - */ getAll(url: string, options?: any, callback: (...params: any[]) => any): void; /** * Set or update a cookie. + * @param url - - + * @param name - - + * @param [value] - - + * @param [callback] - - */ set(url: string, name: string | any, value?: string | ((...params: any[]) => any), callback?: (...params: any[]) => any): void; /** * Remove single cookie with the given name. + * @param url - - + * @param name - - + * @param [callback] - - */ unset(url: string, name: string, callback?: (...params: any[]) => any): void; /** * Remove all the cookies for the given URL. + * @param url - - + * @param [callback] - - */ clear(url: string, callback?: (...params: any[]) => any): void; } From 1c1c2c418fb30a5cb414a632bb02fe9f7f3bdbed Mon Sep 17 00:00:00 2001 From: Vedanta Krishna Date: Wed, 13 Sep 2023 19:03:27 +0530 Subject: [PATCH 3/7] fix(request-flow): don't pass dnd as true when stopExecution happens --- lib/sandbox/execute.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/sandbox/execute.js b/lib/sandbox/execute.js index 894246c1..d25a37cb 100644 --- a/lib/sandbox/execute.js +++ b/lib/sandbox/execute.js @@ -218,10 +218,7 @@ module.exports = function (bridge, glob) { dispatchEvent(bridge, execution, executionRequestEventName, options.cursor, id, eventId, request); }, function () { execution.shouldSkipExecution = true; - timers.terminate(null, true); - - // disable further edits on execution - execution = JSON.parse(JSON.stringify(execution)); + timers.terminate(null); }, dispatchAssertions, new PostmanCookieStore(id, bridge, timers, execution), { disabledAPIs: initializationOptions.disabledAPIs From 50b3d00f648198296d5a10729c62ca0f35e4b37c Mon Sep 17 00:00:00 2001 From: Vedanta Krishna Date: Thu, 14 Sep 2023 09:46:45 +0530 Subject: [PATCH 4/7] fix(request-flow): fix the type of request object in jsdocs --- lib/sandbox/pmapi.js | 3 +-- npm/build-sandbox-types.js | 14 ++++++++++++++ types/sandbox/prerequest.d.ts | 4 ++-- types/sandbox/test.d.ts | 4 ++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/sandbox/pmapi.js b/lib/sandbox/pmapi.js index 2be21a75..18409d65 100644 --- a/lib/sandbox/pmapi.js +++ b/lib/sandbox/pmapi.js @@ -152,14 +152,13 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, /** * @Interface IRequest - * @extends import("postman-collection").Request */ /** * The request object inside pm is a representation of the request for which this script is being run. * For a pre-request script, this is the request that is about to be sent and when in a test script, * this is the representation of the request that was sent. * - * @type {IRequest} + * @type {Request|IRequest} */ request: execution.request, diff --git a/npm/build-sandbox-types.js b/npm/build-sandbox-types.js index 33ce1103..1e8be765 100644 --- a/npm/build-sandbox-types.js +++ b/npm/build-sandbox-types.js @@ -129,6 +129,20 @@ module.exports = function (exit) { c.type.typeName.escapedText = `import("postman-collection").${currentType}`; } } + + // For properties referencing sdk and some more properties, eg. request: Request|IRequest + if (c.type && c.type.types && c.type.types.length) { + c.type.types.forEach((t) => { + if (t.typeName) { + let currentType = t.typeName.escapedText; + + if (collectionSDKTypes.includes(currentType)) { + t.typeName.escapedText = `import("postman-collection").${currentType}`; + } + } + }); + } + // takes care of functions with parameters referencing CollectionSDK types else if (c.parameters && c.parameters.length > 0) { c.parameters.forEach((p) => { diff --git a/types/sandbox/prerequest.d.ts b/types/sandbox/prerequest.d.ts index a596953c..da7fea66 100644 --- a/types/sandbox/prerequest.d.ts +++ b/types/sandbox/prerequest.d.ts @@ -46,7 +46,7 @@ declare class Postman { * For a pre-request script, this is the request that is about to be sent and when in a test script, * this is the representation of the request that was sent. */ - request: IRequest; + request: import("postman-collection").Request | IRequest; /** * The cookies object contains a list of cookies that are associated with the domain * to which the request was made. @@ -88,7 +88,7 @@ declare interface Info { requestId: string; } -declare interface IRequest extends import("postman-collection").Request { +declare interface IRequest { /** * @example * if (pm.environment.get("token")) { diff --git a/types/sandbox/test.d.ts b/types/sandbox/test.d.ts index 8dec431e..f43a58a2 100644 --- a/types/sandbox/test.d.ts +++ b/types/sandbox/test.d.ts @@ -46,7 +46,7 @@ declare class Postman { * For a pre-request script, this is the request that is about to be sent and when in a test script, * this is the representation of the request that was sent. */ - request: IRequest; + request: import("postman-collection").Request | IRequest; /** * Inside the test scripts, the pm.response object contains all information pertaining * to the response that was received. @@ -93,7 +93,7 @@ declare interface Info { requestId: string; } -declare interface IRequest extends import("postman-collection").Request { +declare interface IRequest { /** * @example * if (pm.environment.get("token")) { From d442bcfe2069c206cee30e10c2caf5fd6a211c9e Mon Sep 17 00:00:00 2001 From: Vedanta Krishna Date: Thu, 14 Sep 2023 10:53:47 +0530 Subject: [PATCH 5/7] chore(request-flow): minor refactoring. - fix the type of request object in jsdocs - add comments --- lib/sandbox/dispatch-event.js | 3 +++ lib/sandbox/execute.js | 16 +++++++++++----- lib/sandbox/execution.js | 8 ++++++++ lib/sandbox/pmapi.js | 11 ++++++----- test/unit/sandbox-libraries/pm.test.js | 1 - types/sandbox/prerequest.d.ts | 12 +++++------- types/sandbox/test.d.ts | 12 +++++------- 7 files changed, 38 insertions(+), 25 deletions(-) diff --git a/lib/sandbox/dispatch-event.js b/lib/sandbox/dispatch-event.js index 2a4bdcf7..4583a548 100644 --- a/lib/sandbox/dispatch-event.js +++ b/lib/sandbox/dispatch-event.js @@ -1,4 +1,7 @@ /** + * Why Do we need this wrapper? + * Because when user executes pm.request.stopExecution(), we need to stop the execution of the current request. + * But, we don't stop the execution of the script. We just stop sending events to the bridge. * * @param {any} bridge - the bridge object * @param {{ shouldSkipExecution: boolean }} execution - the execution object diff --git a/lib/sandbox/execute.js b/lib/sandbox/execute.js index d25a37cb..8f15d6e4 100644 --- a/lib/sandbox/execute.js +++ b/lib/sandbox/execute.js @@ -116,9 +116,9 @@ module.exports = function (bridge, glob) { // extract the code from event. The event can be the code itself and we know that if the event is of type // string. - code = _.isFunction(event.script && event.script.toSource) && event.script.toSource(); + code = _.isFunction(event.script && event.script.toSource) && event.script.toSource(), // create the execution object - let execution = new Execution(id, event, context, { ...options, initializeExecution }), + execution = new Execution(id, event, context, { ...options, initializeExecution }), /** * Dispatch assertions from `pm.test` or legacy `test` API. @@ -140,8 +140,9 @@ module.exports = function (bridge, glob) { dispatchEvent(bridge, execution, assertionEventName, options.cursor, assertions); dispatchEvent(bridge, execution, EXECUTION_ASSERTION_EVENT, options.cursor, assertions); - }, - waiting, + }; + + let waiting, timers; execution.return.async = false; @@ -179,6 +180,11 @@ module.exports = function (bridge, glob) { execution.response && (delete execution.response); // fire the execution completion event + + // note: We are sending shouldSkipExecution: false to dispatchEvent function + // because this event should be fired even if shouldSkipExecution is true as this event is + // used to complete the execution in the sandbox. All other events are fired only if + // shouldSkipExecution is false. (dnd !== true) && dispatchEvent(bridge, { shouldSkipExecution: false }, executionEventName, err || null, execution); }); @@ -216,7 +222,7 @@ module.exports = function (bridge, glob) { var eventId = timers.setEvent(callback); dispatchEvent(bridge, execution, executionRequestEventName, options.cursor, id, eventId, request); - }, function () { + }, /* onStopExecution = */ function () { execution.shouldSkipExecution = true; timers.terminate(null); }, diff --git a/lib/sandbox/execution.js b/lib/sandbox/execution.js index 7f4c988f..caacdd08 100644 --- a/lib/sandbox/execution.js +++ b/lib/sandbox/execution.js @@ -27,6 +27,14 @@ class Execution { this.id = id; this.target = event.listen || PROPERTY.SCRIPT; this.legacy = options.legacy || {}; + + /** + * This property is set to true if user has called pm.request.stopExecution() in the script. + * This is used to stop the execution of the current request. + * We stop sending events to the bridge if this is set to true. + * + * @type {Boolean} + */ this.shouldSkipExecution = false; this.cursor = _.isObject(options.cursor) ? options.cursor : {}; diff --git a/lib/sandbox/pmapi.js b/lib/sandbox/pmapi.js index 18409d65..bdd56e40 100644 --- a/lib/sandbox/pmapi.js +++ b/lib/sandbox/pmapi.js @@ -44,13 +44,13 @@ const _ = require('lodash'), * * @param {Execution} execution - * @param {Function} onRequest - - * @param {Function} onSkipRequest - callback to execute when pm.skipRequest called + * @param {Function} onStopExecution - callback to execute when pm.request.stopExecution() called * @param {Function} onAssertion - * @param {Object} cookieStore - * @param {Object} [options] - * @param {Array.} [options.disabledAPIs] - */ -function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, options = {}) { +function Postman (execution, onRequest, onStopExecution, onAssertion, cookieStore, options = {}) { // @todo - ensure runtime passes data in a scope format let iterationData = new VariableScope(); @@ -279,9 +279,10 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, }); /** - * @property {Function} stopExecution - stops the execution of current request. - * NOTE: If called from a pre-request script, the request will not be sent. + * Halts the execution of current request. No line after this will be executed and + * if invoked from a pre-request script, the request will not be sent. * + * @type {Function} stopExecution * @example * if (pm.environment.get("token")) { * pm.request.stopExecution(); @@ -290,7 +291,7 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, * @name IRequest#stopExecution */ Object.defineProperty(this.request, 'stopExecution', { - value: onSkipRequest + value: onStopExecution }); } diff --git a/test/unit/sandbox-libraries/pm.test.js b/test/unit/sandbox-libraries/pm.test.js index dd49ae72..7b1833a0 100644 --- a/test/unit/sandbox-libraries/pm.test.js +++ b/test/unit/sandbox-libraries/pm.test.js @@ -304,7 +304,6 @@ describe('sandbox library - pm api', function () { it(`should not execute any line after pm.request.stopExecution in pre-request script, even if the pm.request.stopExecution invoked inside a try catch block`, function (done) { context.on('console', function (level, ...args) { - // console.log(args); expect(args[1]).to.equal('pre-request log 1'); }); context.execute(` diff --git a/types/sandbox/prerequest.d.ts b/types/sandbox/prerequest.d.ts index da7fea66..277c3c41 100644 --- a/types/sandbox/prerequest.d.ts +++ b/types/sandbox/prerequest.d.ts @@ -17,14 +17,14 @@ declare interface PostmanLegacy { /** * @param execution - - * @param onRequest - - - * @param onSkipRequest - callback to execute when pm.skipRequest called + * @param onStopExecution - callback to execute when pm.request.stopExecution() called * @param onAssertion - - * @param cookieStore - - * @param [options] - - * @param [options.disabledAPIs] - - */ 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, onStopExecution: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, options?: { disabledAPIs?: string[]; }); /** @@ -90,16 +90,14 @@ declare interface Info { declare interface IRequest { /** + * Halts the execution of current request. No line after this will be executed and + * if invoked from a pre-request script, the request will not be sent. * @example * if (pm.environment.get("token")) { * pm.request.stopExecution(); * } - * @property stopExecution - stops the execution of current request. - * NOTE: If called from a pre-request script, the request will not be sent. */ - stopExecution: { - stopExecution: (...params: any[]) => any; - }; + stopExecution: (...params: any[]) => any; } declare interface Visualizer { diff --git a/types/sandbox/test.d.ts b/types/sandbox/test.d.ts index f43a58a2..ad49a6a6 100644 --- a/types/sandbox/test.d.ts +++ b/types/sandbox/test.d.ts @@ -17,14 +17,14 @@ declare interface PostmanLegacy { /** * @param execution - - * @param onRequest - - - * @param onSkipRequest - callback to execute when pm.skipRequest called + * @param onStopExecution - callback to execute when pm.request.stopExecution() called * @param onAssertion - - * @param cookieStore - - * @param [options] - - * @param [options.disabledAPIs] - - */ 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, onStopExecution: (...params: any[]) => any, onAssertion: (...params: any[]) => any, cookieStore: any, options?: { disabledAPIs?: string[]; }); /** @@ -95,16 +95,14 @@ declare interface Info { declare interface IRequest { /** + * Halts the execution of current request. No line after this will be executed and + * if invoked from a pre-request script, the request will not be sent. * @example * if (pm.environment.get("token")) { * pm.request.stopExecution(); * } - * @property stopExecution - stops the execution of current request. - * NOTE: If called from a pre-request script, the request will not be sent. */ - stopExecution: { - stopExecution: (...params: any[]) => any; - }; + stopExecution: (...params: any[]) => any; } declare interface Visualizer { From ebb23424308a260c14ced1adaed54f413f666828 Mon Sep 17 00:00:00 2001 From: Vedanta Krishna Date: Fri, 15 Sep 2023 13:27:19 +0530 Subject: [PATCH 6/7] test(request-flow): fix the issue where event name was missed in the wrapper over events --- lib/sandbox/execute.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sandbox/execute.js b/lib/sandbox/execute.js index 8f15d6e4..6036b0e2 100644 --- a/lib/sandbox/execute.js +++ b/lib/sandbox/execute.js @@ -171,8 +171,8 @@ module.exports = function (bridge, glob) { bridge.off(cookiesEventName); if (err) { // fire extra execution error event - dispatchEvent(bridge, execution, options.cursor, err); - dispatchEvent(bridge, execution, options.cursor, err); + dispatchEvent(bridge, execution, errorEventName, options.cursor, err); + dispatchEvent(bridge, execution, EXECUTION_ERROR_EVENT, options.cursor, err); } // @note delete response from the execution object to avoid dispatching From d0a926f2e7e2e52890ccb7649565424c90504e74 Mon Sep 17 00:00:00 2001 From: Vedanta Krishna Date: Tue, 3 Oct 2023 14:02:16 +0530 Subject: [PATCH 7/7] feat(skip-execution): add support to display system logs System logs will be displayed on postman app console with special highlight, they should be used when you want to inform user that some action was taken from script. --- lib/sandbox/console.js | 17 +++- lib/sandbox/execute.js | 12 ++- test/unit/sandbox-libraries/pm.test.js | 103 +++++++++++++++++++------ 3 files changed, 105 insertions(+), 27 deletions(-) diff --git a/lib/sandbox/console.js b/lib/sandbox/console.js index ee4243ad..70fd58a8 100644 --- a/lib/sandbox/console.js +++ b/lib/sandbox/console.js @@ -10,6 +10,8 @@ var teleportJS = require('teleport-javascript'), */ CONSOLE_EVENT = 'execution.console', + SYSTEM_MESSAGE = 'system_message', + /** * List of functions that we expect and create for console * @@ -66,4 +68,17 @@ function PostmanConsole (emitter, cursor, originalConsole, execution) { }); } -module.exports = PostmanConsole; +/** + * + * @param {{log: Function}} console - postman console instance + * @param {'skip_request'} type - this is used to identify the message type in postman-app + * @param {...any} args - any other information like request name to be used to generate console message + */ +function dispatchSystemMessage (console, type, ...args) { + return console.log(SYSTEM_MESSAGE, type, ...args); +} + +module.exports = { + default: PostmanConsole, + dispatchSystemMessage: dispatchSystemMessage +}; diff --git a/lib/sandbox/execute.js b/lib/sandbox/execute.js index 6036b0e2..688a203c 100644 --- a/lib/sandbox/execute.js +++ b/lib/sandbox/execute.js @@ -5,7 +5,7 @@ const _ = require('lodash'), sdk = require('postman-collection'), PostmanEvent = sdk.Event, Execution = require('./execution'), - PostmanConsole = require('./console'), + { default: PostmanConsole, dispatchSystemMessage } = require('./console'), PostmanTimers = require('./timers'), PostmanAPI = require('./pmapi'), PostmanCookieStore = require('./cookie-store'), @@ -143,7 +143,8 @@ module.exports = function (bridge, glob) { }; let waiting, - timers; + timers, + postmanConsole; execution.return.async = false; @@ -211,11 +212,13 @@ module.exports = function (bridge, glob) { timers.clearEvent(id, err, res); }); + postmanConsole = new PostmanConsole(bridge, options.cursor, options.debug && glob.console, execution); + // 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 // inside this closure. - (new PostmanConsole(bridge, options.cursor, options.debug && glob.console, execution)), + postmanConsole, timers, ( new PostmanAPI(execution, function (request, callback) { @@ -223,6 +226,9 @@ module.exports = function (bridge, glob) { dispatchEvent(bridge, execution, executionRequestEventName, options.cursor, id, eventId, request); }, /* onStopExecution = */ function () { + // Dispatch event to display system message on console informing user that the request + // execution was skipped from script + dispatchSystemMessage(postmanConsole, 'skip_request', _.get(execution, 'legacy._itemName')); execution.shouldSkipExecution = true; timers.terminate(null); }, diff --git a/test/unit/sandbox-libraries/pm.test.js b/test/unit/sandbox-libraries/pm.test.js index 7b1833a0..aa58703c 100644 --- a/test/unit/sandbox-libraries/pm.test.js +++ b/test/unit/sandbox-libraries/pm.test.js @@ -279,9 +279,9 @@ describe('sandbox library - pm api', function () { }); it('should not execute any line after pm.request.stopExecution in pre-request script', function (done) { - context.on('console', function (level, ...args) { - expect(args[1]).to.equal('pre-request log 1'); - }); + const consoleSpy = sinon.spy(); + + context.on('console', consoleSpy); context.execute(` preRequestScript: { console.log('pre-request log 1'); @@ -292,20 +292,38 @@ describe('sandbox library - pm api', function () { timeout: 200, context: { request: 'https://postman-echo.com/get?foo=bar' + }, + legacy: { + _itemName: 'request-name', + _itemId: 'request-id', + _itemPath: 'col1/fol1/request-name' } }, function (err, execution) { if (err) { return done(err); } - expect(execution).to.include({ shouldSkipExecution: true }); - return done(); + try { + expect(execution).to.include({ shouldSkipExecution: true }); + expect(consoleSpy).to.have.been.calledTwice; + expect(consoleSpy.getCall(0).args[1]).to.equal('log'); + expect(consoleSpy.getCall(0).args[2]).to.equal('pre-request log 1'); + expect(consoleSpy.getCall(1).args[1]).to.equal('log'); + expect(consoleSpy.getCall(1).args[2]).to.equal('system_message'); + expect(consoleSpy.getCall(1).args[3]).to.equal('skip_request'); + expect(consoleSpy.getCall(1).args[4]).to.equal('request-name'); + + return done(); + } + catch (err) { + return done(err); + } }); }); it(`should not execute any line after pm.request.stopExecution in pre-request script, even if the pm.request.stopExecution invoked inside a try catch block`, function (done) { - context.on('console', function (level, ...args) { - expect(args[1]).to.equal('pre-request log 1'); - }); + const consoleSpy = sinon.spy(); + + context.on('console', consoleSpy); context.execute(` preRequestScript: { console.log('pre-request log 1'); @@ -323,17 +341,30 @@ describe('sandbox library - pm api', function () { } }, function (err, execution) { if (err) { return done(err); } - expect(execution).to.include({ shouldSkipExecution: true }); - return done(); + try { + expect(execution).to.include({ shouldSkipExecution: true }); + expect(consoleSpy).to.have.been.calledTwice; + expect(consoleSpy.getCall(0).args[1]).to.equal('log'); + expect(consoleSpy.getCall(0).args[2]).to.equal('pre-request log 1'); + expect(consoleSpy.getCall(1).args[1]).to.equal('log'); + expect(consoleSpy.getCall(1).args[2]).to.equal('system_message'); + expect(consoleSpy.getCall(1).args[3]).to.equal('skip_request'); + expect(consoleSpy.getCall(1).args[4]).to.equal(undefined); + + return done(); + } + catch (err) { + return done(err); + } }); }); it(`should not execute any line after pm.request.stopExecution in pre-request script, even if the pm.request.stopExecution invoked inside an async function`, function (done) { - context.on('console', function (level, ...args) { - expect(args[1]).to.equal('pre-request log 1'); - }); + const consoleSpy = sinon.spy(); + + context.on('console', consoleSpy); context.execute(` preRequestScript: { console.log('pre-request log 1'); @@ -351,17 +382,30 @@ describe('sandbox library - pm api', function () { } }, function (err, execution) { if (err) { return done(err); } - expect(execution).to.include({ shouldSkipExecution: true }); - return done(); + try { + expect(execution).to.include({ shouldSkipExecution: true }); + expect(consoleSpy).to.have.been.calledTwice; + expect(consoleSpy.getCall(0).args[1]).to.equal('log'); + expect(consoleSpy.getCall(0).args[2]).to.equal('pre-request log 1'); + expect(consoleSpy.getCall(1).args[1]).to.equal('log'); + expect(consoleSpy.getCall(1).args[2]).to.equal('system_message'); + expect(consoleSpy.getCall(1).args[3]).to.equal('skip_request'); + expect(consoleSpy.getCall(1).args[4]).to.equal(undefined); + + return done(); + } + catch (err) { + return done(err); + } }); }); it('should not reflect any variable change line after pm.request.stopExecution in pre-request script', function (done) { - context.on('console', function (level, ...args) { - expect(args[1]).to.equal('pre-request log 1'); - }); + const consoleSpy = sinon.spy(); + + context.on('console', consoleSpy); context.execute(` preRequestScript: { async function myFun () { @@ -384,12 +428,25 @@ describe('sandbox library - pm api', function () { } }, function (err, execution) { if (err) { return done(err); } - expect(execution).to.include({ shouldSkipExecution: true }); - expect(execution).to.deep.nested.include({ '_variables.values': [ - { value: 'bar', key: 'foo', type: 'any' } - ] }); - return done(); + try { + expect(execution).to.include({ shouldSkipExecution: true }); + expect(execution).to.deep.nested.include({ '_variables.values': [ + { value: 'bar', key: 'foo', type: 'any' } + ] }); + expect(consoleSpy).to.have.been.calledTwice; + expect(consoleSpy.getCall(0).args[1]).to.equal('log'); + expect(consoleSpy.getCall(0).args[2]).to.equal('pre-request log 1'); + expect(consoleSpy.getCall(1).args[1]).to.equal('log'); + expect(consoleSpy.getCall(1).args[2]).to.equal('system_message'); + expect(consoleSpy.getCall(1).args[3]).to.equal('skip_request'); + expect(consoleSpy.getCall(1).args[4]).to.equal(undefined); + + return done(); + } + catch (err) { + return done(err); + } }); });