Skip to content

Commit abcb20c

Browse files
committed
feat: add ability to skip request execution from script
1 parent 5e890c2 commit abcb20c

File tree

10 files changed

+296
-30
lines changed

10 files changed

+296
-30
lines changed

CHANGELOG.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
master:
2+
new features:
3+
- GH-942 Added support for pm.execution.skipRequest
14
4.2.7:
25
date: 2023-08-03
36
chores:

lib/sandbox/console.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function replacer (key, value) {
4545
return value;
4646
}
4747

48-
function PostmanConsole (emitter, cursor, originalConsole) {
48+
function PostmanConsole (emitter, cursor, originalConsole, execution) {
4949
const dispatch = function (level) { // create a dispatch function that emits events
5050
const args = arrayProtoSlice.call(arguments, 1);
5151

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

57-
emitter.dispatch(CONSOLE_EVENT, cursor, level, teleportJS.stringify(args, replacer));
57+
58+
emitter.dispatch(execution, CONSOLE_EVENT, cursor, level, teleportJS.stringify(args, replacer));
5859
};
5960

6061
// setup variants of the logger based on log levels

lib/sandbox/cookie-store.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ const _ = require('lodash'),
1414
arrayProtoSlice = Array.prototype.slice;
1515

1616
class PostmanCookieStore extends Store {
17-
constructor (id, emitter, timers) {
17+
constructor (id, emitter, timers, execution) {
1818
super();
1919

2020
this.id = id; // execution identifier
2121
this.emitter = emitter;
2222
this.timers = timers;
23+
this.execution = execution;
2324
}
2425
}
2526

@@ -77,7 +78,7 @@ STORE_METHODS.forEach(function (method) {
7778
// Refer: https://github.com/postmanlabs/postman-app-support/issues/11064
7879
setTimeout(() => {
7980
// finally, dispatch event over the bridge
80-
this.emitter.dispatch(eventName, eventId, EVENT_STORE_ACTION, method, args);
81+
this.emitter.dispatch(this.execution, eventName, eventId, EVENT_STORE_ACTION, method, args);
8182
});
8283
};
8384
});

lib/sandbox/execute.js

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,23 @@ module.exports = function (bridge, glob) {
2626
// @note we use a common scope for all executions. this causes issues when scripts are run inside the sandbox
2727
// in parallel, but we still use this way for the legacy "persistent" behaviour needed in environment
2828
const scope = Scope.create({
29-
eval: true,
30-
ignore: ['require'],
31-
block: ['bridge']
32-
});
29+
eval: true,
30+
ignore: ['require'],
31+
block: ['bridge']
32+
}),
33+
originalBridgeDispatch = bridge.dispatch;
34+
35+
bridge.dispatch = function (execution, ...args) {
36+
// What is the purpose of overriding the dispatch method here?
37+
// When the user invokes pm.execution.skipRequest(), our goal is to halt the current request's execution.
38+
// Since we lack a foolproof method to completely halt the script's execution, our approach is to
39+
// cease sending events to the bridge, creating the appearance that the script ahead never ran.
40+
if (execution && execution.shouldSkipExecution) {
41+
return;
42+
}
43+
44+
return originalBridgeDispatch.call(bridge, ...args);
45+
};
3346

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

52-
return bridge.dispatch('initialize');
65+
return bridge.dispatch(null, 'initialize');
5366
}
5467

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

6780
scope.exec(template, (err) => {
6881
if (err) {
69-
return bridge.dispatch('initialize', err);
82+
return bridge.dispatch(null, 'initialize', err);
7083
}
7184

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

82-
bridge.dispatch('initialize');
95+
bridge.dispatch(null, 'initialize');
8396
});
8497
});
8598

@@ -97,7 +110,8 @@ module.exports = function (bridge, glob) {
97110
*/
98111
bridge.on('execute', function (id, event, context, options) {
99112
if (!(id && _.isString(id))) {
100-
return bridge.dispatch('error', new Error('sandbox: execution identifier parameter(s) missing'));
113+
return bridge.dispatch(null, 'error',
114+
new Error('sandbox: execution identifier parameter(s) missing'));
101115
}
102116

103117
!options && (options = {});
@@ -136,8 +150,8 @@ module.exports = function (bridge, glob) {
136150
// For compatibility, dispatch the single assertion as an array.
137151
!Array.isArray(assertions) && (assertions = [assertions]);
138152

139-
bridge.dispatch(assertionEventName, options.cursor, assertions);
140-
bridge.dispatch(EXECUTION_ASSERTION_EVENT, options.cursor, assertions);
153+
bridge.dispatch(execution, assertionEventName, options.cursor, assertions);
154+
bridge.dispatch(execution, EXECUTION_ASSERTION_EVENT, options.cursor, assertions);
141155
};
142156

143157
let waiting,
@@ -148,8 +162,8 @@ module.exports = function (bridge, glob) {
148162
// create the controlled timers
149163
timers = new PostmanTimers(null, function (err) {
150164
if (err) { // propagate the error out of sandbox
151-
bridge.dispatch(errorEventName, options.cursor, err);
152-
bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err);
165+
bridge.dispatch(execution, errorEventName, options.cursor, err);
166+
bridge.dispatch(execution, EXECUTION_ERROR_EVENT, options.cursor, err);
153167
}
154168
}, function () {
155169
execution.return.async = true;
@@ -169,16 +183,22 @@ module.exports = function (bridge, glob) {
169183
bridge.off(cookiesEventName);
170184

171185
if (err) { // fire extra execution error event
172-
bridge.dispatch(errorEventName, options.cursor, err);
173-
bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err);
186+
bridge.dispatch(null, errorEventName, options.cursor, err);
187+
bridge.dispatch(null, EXECUTION_ERROR_EVENT, options.cursor, err);
174188
}
175189

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

180194
// fire the execution completion event
181-
(dnd !== true) && bridge.dispatch(executionEventName, err || null, execution);
195+
196+
// Note: We are sending null to dispatchEvent function
197+
// because this event should be fired even if shouldSkipExecution is true as this event is
198+
// used to complete the execution in the sandbox. All other events are fired only if
199+
// shouldSkipExecution is false.
200+
(dnd !== true) && bridge.dispatch(null,
201+
executionEventName, err || null, execution);
182202
});
183203

184204
// if a timeout is set, we must ensure that all pending timers are cleared and an execution timeout event is
@@ -207,14 +227,19 @@ module.exports = function (bridge, glob) {
207227
executeContext(scope, code, execution,
208228
// if a console is sent, we use it. otherwise this also prevents erroneous referencing to any console
209229
// inside this closure.
210-
(new PostmanConsole(bridge, options.cursor, options.debug && glob.console)),
230+
(new PostmanConsole(bridge, options.cursor, options.debug && glob.console, execution)),
211231
timers,
212232
(
213233
new PostmanAPI(execution, function (request, callback) {
214234
var eventId = timers.setEvent(callback);
215235

216-
bridge.dispatch(executionRequestEventName, options.cursor, id, eventId, request);
217-
}, dispatchAssertions, new PostmanCookieStore(id, bridge, timers), {
236+
bridge.dispatch(execution, executionRequestEventName, options.cursor, id, eventId, request);
237+
},
238+
/* onSkipRequest = */ () => {
239+
execution.shouldSkipExecution = true;
240+
timers.terminate(null);
241+
},
242+
dispatchAssertions, new PostmanCookieStore(id, bridge, timers, execution), {
218243
disabledAPIs: initializationOptions.disabledAPIs
219244
})
220245
),

lib/sandbox/execution.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ class Execution {
2727
this.id = id;
2828
this.target = event.listen || PROPERTY.SCRIPT;
2929
this.legacy = options.legacy || {};
30+
31+
/**
32+
* This property is set to true if user has called pm.execution.skipRequest() in the script.
33+
* This is used to stop the execution of the current request.
34+
* We stop sending events to the bridge if this is set to true.
35+
*
36+
* @type {Boolean}
37+
*/
38+
this.shouldSkipExecution = false;
3039
this.cursor = _.isObject(options.cursor) ? options.cursor : {};
3140

3241
this.data = _.get(context, PROPERTY.DATA, {});

lib/sandbox/ping.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
listener (pong) {
33
return function (payload) {
4-
this.dispatch(pong, payload);
4+
this.dispatch(null, pong, payload);
55
};
66
}
77
};

lib/sandbox/pmapi.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ const _ = require('lodash'),
4444
*
4545
* @param {Execution} execution -
4646
* @param {Function} onRequest -
47+
* @param {Function} onSkipRequest - callback to execute when pm.execution.skipRequest() called
4748
* @param {Function} onAssertion -
4849
* @param {Object} cookieStore -
4950
* @param {Object} [options] -
5051
* @param {Array.<String>} [options.disabledAPIs] -
5152
*/
52-
function Postman (execution, onRequest, onAssertion, cookieStore, options = {}) {
53+
function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, options = {}) {
5354
// @todo - ensure runtime passes data in a scope format
5455
let iterationData = new VariableScope();
5556

@@ -253,6 +254,29 @@ function Postman (execution, onRequest, onAssertion, cookieStore, options = {})
253254
}
254255
}, options.disabledAPIs);
255256

257+
_assignDefinedReadonly(this, /** @lends Postman.prototype */ {
258+
/**
259+
* Exposes handlers to control execution state
260+
*
261+
* @interface Execution
262+
*/
263+
264+
/**
265+
*
266+
* @type {Execution}
267+
*/
268+
execution: _assignDefinedReadonly({}, /** @lends Execution */ {
269+
/**
270+
* Stops the execution of current request. No line after this will be executed and
271+
* if invoked from a pre-request script, the request will not be sent.
272+
*
273+
* @type {Function} skipRequest
274+
* @instance
275+
*/
276+
skipRequest: onSkipRequest
277+
})
278+
});
279+
256280
// extend pm api with test runner abilities
257281
setupTestRunner(this, onAssertion);
258282

test/unit/sandbox-libraries/pm.test.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,121 @@ describe('sandbox library - pm api', function () {
278278
}, done);
279279
});
280280

281+
it('should not execute any line after pm.execution.skipRequest in pre-request script', function (done) {
282+
context.on('console', function (level, ...args) {
283+
expect(args[1]).to.equal('pre-request log 1');
284+
});
285+
context.execute(`
286+
preRequestScript: {
287+
console.log('pre-request log 1');
288+
pm.execution.skipRequest();
289+
console.log('pre-request log 2');
290+
}
291+
`, {
292+
timeout: 200,
293+
context: {
294+
request: 'https://postman-echo.com/get?foo=bar'
295+
}
296+
}, function (err, execution) {
297+
if (err) { return done(err); }
298+
expect(execution).to.include({ shouldSkipExecution: true });
299+
300+
return done();
301+
});
302+
});
303+
304+
it(`should not execute any line after pm.execution.skipRequest in pre-request script,
305+
even if the pm.execution.skipRequest invoked inside a try catch block`, function (done) {
306+
context.on('console', function (level, ...args) {
307+
expect(args[1]).to.equal('pre-request log 1');
308+
});
309+
context.execute(`
310+
preRequestScript: {
311+
console.log('pre-request log 1');
312+
try {
313+
pm.execution.skipRequest();
314+
} catch (err) {
315+
// ignore
316+
}
317+
console.log('pre-request log 2');
318+
}
319+
`, {
320+
timeout: 200,
321+
context: {
322+
request: 'https://postman-echo.com/get?foo=bar'
323+
}
324+
}, function (err, execution) {
325+
if (err) { return done(err); }
326+
expect(execution).to.include({ shouldSkipExecution: true });
327+
328+
return done();
329+
});
330+
});
331+
332+
it(`should not execute any line after pm.execution.skipRequest in pre-request script,
333+
even if the pm.execution.skipRequest invoked inside an async function`, function (done) {
334+
context.on('console', function (level, ...args) {
335+
expect(args[1]).to.equal('pre-request log 1');
336+
});
337+
context.execute(`
338+
preRequestScript: {
339+
console.log('pre-request log 1');
340+
async function myAsyncFunction() {
341+
pm.execution.skipRequest();
342+
}
343+
344+
myAsyncFunction();
345+
console.log('pre-request log 2');
346+
}
347+
`, {
348+
timeout: 200,
349+
context: {
350+
request: 'https://postman-echo.com/get?foo=bar'
351+
}
352+
}, function (err, execution) {
353+
if (err) { return done(err); }
354+
expect(execution).to.include({ shouldSkipExecution: true });
355+
356+
return done();
357+
});
358+
});
359+
360+
it('should not reflect any variable change line after pm.execution.skipRequest in pre-request script',
361+
function (done) {
362+
context.on('console', function (level, ...args) {
363+
expect(args[1]).to.equal('pre-request log 1');
364+
});
365+
context.execute(`
366+
preRequestScript: {
367+
async function myFun () {
368+
console.log('pre-request log 1');
369+
370+
pm.variables.set('foo', 'bar');
371+
pm.execution.skipRequest();
372+
new Promise((res) => setTimeout(res, 100))
373+
pm.variables.set('foo', 'nobar');
374+
console.log('pre-request log 2');
375+
}
376+
377+
myFun();
378+
379+
}
380+
`, {
381+
timeout: 200,
382+
context: {
383+
request: 'https://postman-echo.com/get?foo=bar'
384+
}
385+
}, function (err, execution) {
386+
if (err) { return done(err); }
387+
expect(execution).to.include({ shouldSkipExecution: true });
388+
expect(execution).to.deep.nested.include({ '_variables.values': [
389+
{ value: 'bar', key: 'foo', type: 'any' }
390+
] });
391+
392+
return done();
393+
});
394+
});
395+
281396
it('when serialized should not have assertion helpers added by sandbox', function (done) {
282397
context.execute(`
283398
var assert = require('assert'),

0 commit comments

Comments
 (0)