Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support to skip request execution from script. #942

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
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
vedkribhu marked this conversation as resolved.
Show resolved Hide resolved
*/

/**
*
* @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