Skip to content

Commit

Permalink
feat: add ability to skip request execution from script
Browse files Browse the repository at this point in the history
  • Loading branch information
vedkribhu committed Oct 16, 2023
1 parent 5e890c2 commit abcb20c
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 30 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
master:
new features:
- GH-942 Added support for pm.execution.skipRequest
4.2.7:
date: 2023-08-03
chores:
Expand Down
5 changes: 3 additions & 2 deletions lib/sandbox/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,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);

Expand All @@ -54,7 +54,8 @@ function PostmanConsole (emitter, cursor, originalConsole) {
originalConsole[level].apply(originalConsole, args);
}

emitter.dispatch(CONSOLE_EVENT, cursor, level, teleportJS.stringify(args, replacer));

emitter.dispatch(execution, CONSOLE_EVENT, cursor, level, teleportJS.stringify(args, replacer));
};

// setup variants of the logger based on log levels
Expand Down
5 changes: 3 additions & 2 deletions lib/sandbox/cookie-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,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;
}
}

Expand Down Expand Up @@ -77,7 +78,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);
this.emitter.dispatch(this.execution, eventName, eventId, EVENT_STORE_ACTION, method, args);
});
};
});
Expand Down
61 changes: 43 additions & 18 deletions lib/sandbox/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,23 @@ module.exports = function (bridge, glob) {
// @note we use a common scope for all executions. this causes issues when scripts are run inside the sandbox
// in parallel, but we still use this way for the legacy "persistent" behaviour needed in environment
const scope = Scope.create({
eval: true,
ignore: ['require'],
block: ['bridge']
});
eval: true,
ignore: ['require'],
block: ['bridge']
}),
originalBridgeDispatch = bridge.dispatch;

bridge.dispatch = function (execution, ...args) {
// What is the purpose of overriding the dispatch method here?
// When the user invokes pm.execution.skipRequest(), our goal is to halt the current request's execution.
// Since we lack a foolproof method to completely halt the script's execution, our approach is to
// cease sending events to the bridge, creating the appearance that the script ahead never ran.
if (execution && execution.shouldSkipExecution) {
return;
}

return originalBridgeDispatch.call(bridge, ...args);
};

// For caching required information provided during
// initialization which will be used during execution
Expand All @@ -49,7 +62,7 @@ module.exports = function (bridge, glob) {
if (!template) {
chai.use(require('chai-postman')(sdk, _, Ajv));

return bridge.dispatch('initialize');
return bridge.dispatch(null, 'initialize');
}

const _module = { exports: {} },
Expand All @@ -66,7 +79,7 @@ module.exports = function (bridge, glob) {

scope.exec(template, (err) => {
if (err) {
return bridge.dispatch('initialize', err);
return bridge.dispatch(null, 'initialize', err);
}

const { chaiPlugin, initializeExecution: setupExecution } = (_module && _module.exports) || {};
Expand All @@ -79,7 +92,7 @@ module.exports = function (bridge, glob) {
initializeExecution = setupExecution;
}

bridge.dispatch('initialize');
bridge.dispatch(null, 'initialize');
});
});

Expand All @@ -97,7 +110,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 bridge.dispatch(null, 'error',
new Error('sandbox: execution identifier parameter(s) missing'));
}

!options && (options = {});
Expand Down Expand Up @@ -136,8 +150,8 @@ 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);
bridge.dispatch(execution, assertionEventName, options.cursor, assertions);
bridge.dispatch(execution, EXECUTION_ASSERTION_EVENT, options.cursor, assertions);
};

let waiting,
Expand All @@ -148,8 +162,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);
bridge.dispatch(execution, errorEventName, options.cursor, err);
bridge.dispatch(execution, EXECUTION_ERROR_EVENT, options.cursor, err);
}
}, function () {
execution.return.async = true;
Expand All @@ -169,16 +183,22 @@ 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);
bridge.dispatch(null, errorEventName, options.cursor, err);
bridge.dispatch(null, EXECUTION_ERROR_EVENT, options.cursor, err);
}

// @note delete response from the execution object to avoid dispatching
// the large response payload back due to performance reasons.
execution.response && (delete execution.response);

// fire the execution completion event
(dnd !== true) && bridge.dispatch(executionEventName, err || null, execution);

// Note: We are sending null 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) && bridge.dispatch(null,
executionEventName, err || null, execution);
});

// if a timeout is set, we must ensure that all pending timers are cleared and an execution timeout event is
Expand Down Expand Up @@ -207,14 +227,19 @@ 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), {
bridge.dispatch(execution, executionRequestEventName, options.cursor, id, eventId, request);
},
/* onSkipRequest = */ () => {
execution.shouldSkipExecution = true;
timers.terminate(null);
},
dispatchAssertions, new PostmanCookieStore(id, bridge, timers, execution), {
disabledAPIs: initializationOptions.disabledAPIs
})
),
Expand Down
9 changes: 9 additions & 0 deletions lib/sandbox/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ 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.execution.skipRequest() 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 : {};

this.data = _.get(context, PROPERTY.DATA, {});
Expand Down
2 changes: 1 addition & 1 deletion lib/sandbox/ping.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
listener (pong) {
return function (payload) {
this.dispatch(pong, payload);
this.dispatch(null, pong, payload);
};
}
};
Expand Down
26 changes: 25 additions & 1 deletion lib/sandbox/pmapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ const _ = require('lodash'),
*
* @param {Execution} execution -
* @param {Function} onRequest -
* @param {Function} onSkipRequest - callback to execute when pm.execution.skipRequest() called
* @param {Function} onAssertion -
* @param {Object} cookieStore -
* @param {Object} [options] -
* @param {Array.<String>} [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();

Expand Down Expand Up @@ -253,6 +254,29 @@ function Postman (execution, onRequest, onAssertion, cookieStore, options = {})
}
}, options.disabledAPIs);

_assignDefinedReadonly(this, /** @lends Postman.prototype */ {
/**
* Exposes handlers to control execution state
*
* @interface Execution
*/

/**
*
* @type {Execution}
*/
execution: _assignDefinedReadonly({}, /** @lends Execution */ {
/**
* Stops 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} skipRequest
* @instance
*/
skipRequest: onSkipRequest
})
});

// extend pm api with test runner abilities
setupTestRunner(this, onAssertion);

Expand Down
115 changes: 115 additions & 0 deletions test/unit/sandbox-libraries/pm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,121 @@ describe('sandbox library - pm api', function () {
}, done);
});

it('should not execute any line after pm.execution.skipRequest 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.execution.skipRequest();
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.execution.skipRequest in pre-request script,
even if the pm.execution.skipRequest invoked inside a try catch block`, 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');
try {
pm.execution.skipRequest();
} 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.execution.skipRequest in pre-request script,
even if the pm.execution.skipRequest 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.execution.skipRequest();
}
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.execution.skipRequest 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.execution.skipRequest();
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'),
Expand Down

0 comments on commit abcb20c

Please sign in to comment.