diff --git a/lib/runner/extensions/event.command.js b/lib/runner/extensions/event.command.js index 81194108b..c650aa251 100644 --- a/lib/runner/extensions/event.command.js +++ b/lib/runner/extensions/event.command.js @@ -19,6 +19,7 @@ var _ = require('lodash'), EXECUTION_ASSERTION_EVENT_BASE = 'execution.assertion.', EXECUTION_ERROR_EVENT_BASE = 'execution.error.', EXECUTION_COOKIES_EVENT_BASE = 'execution.cookies.', + EXECUTION_SKIP_EVENT_BASE = 'execution.skip.', COOKIES_EVENT_STORE_ACTION = 'store', COOKIE_STORE_PUT_METHOD = 'putCookie', @@ -125,6 +126,11 @@ getCookieDomain = function (fnName, args) { return domain; }; +const skippedExecutions = new Set(), + isExecutionSkipped = (executionId) => { + return skippedExecutions.has(executionId); + }; + /** * Script execution extension of the runner. * This module exposes processors for executing scripts before and after requests. Essentially, the processors are @@ -162,18 +168,34 @@ module.exports = { run.host = context; context.on('console', function () { + const cursor = arguments[0]; + + if (cursor && cursor.execution && isExecutionSkipped(cursor.execution)) { return; } + run.triggers.console(...arguments); }); context.on('error', function () { + const cursor = arguments[0]; + + if (cursor && cursor.execution && isExecutionSkipped(cursor.execution)) { return; } + run.triggers.error(...arguments); }); context.on('execution.error', function () { + const cursor = arguments[0]; + + if (cursor && cursor.execution && isExecutionSkipped(cursor.execution)) { return; } + run.triggers.exception(...arguments); }); context.on('execution.assertion', function () { + const cursor = arguments[0]; + + if (cursor && cursor.execution && isExecutionSkipped(cursor.execution)) { return; } + run.triggers.assertion(...arguments); }); @@ -268,7 +290,16 @@ module.exports = { // create copy of cursor so we don't leak script ids outside `event.command` // and across scripts - scriptCursor = _.clone(cursor); + scriptCursor = _.clone(cursor), + + disposeListeners = function () { + this.host.removeAllListeners(EXECUTION_REQUEST_EVENT_BASE + executionId); + this.host.removeAllListeners(EXECUTION_ASSERTION_EVENT_BASE + executionId); + this.host.removeAllListeners(EXECUTION_RESPONSE_EVENT_BASE + executionId); + this.host.removeAllListeners(EXECUTION_COOKIES_EVENT_BASE + executionId); + this.host.removeAllListeners(EXECUTION_ERROR_EVENT_BASE + executionId); + this.host.removeAllListeners(EXECUTION_SKIP_EVENT_BASE + executionId); + }.bind(this); // store the execution id in script script._lastExecutionId = executionId; // please don't use it anywhere else! @@ -425,6 +456,18 @@ module.exports = { }.bind(this)); }.bind(this)); + this.host.on(EXECUTION_SKIP_EVENT_BASE + executionId, function () { + // execution can not be skipped for test event + if (eventName === 'test') { + return; + } + + // we dispose all listeners to avoid any more events being triggered + // and mark the execution as skipped + disposeListeners(); + skippedExecutions.add(executionId); + }); + // finally execute the script this.host.execute(event, { id: executionId, @@ -439,11 +482,7 @@ module.exports = { _itemName: item.name } }, function (err, result) { - this.host.removeAllListeners(EXECUTION_REQUEST_EVENT_BASE + executionId); - this.host.removeAllListeners(EXECUTION_ASSERTION_EVENT_BASE + executionId); - this.host.removeAllListeners(EXECUTION_RESPONSE_EVENT_BASE + executionId); - this.host.removeAllListeners(EXECUTION_COOKIES_EVENT_BASE + executionId); - this.host.removeAllListeners(EXECUTION_ERROR_EVENT_BASE + executionId); + disposeListeners(); // Handle async errors as well. // If there was an error running the script itself, that takes precedence @@ -513,6 +552,8 @@ module.exports = { // upcoming commands(request, httprequest). result && result.request && (payload.context.request = result.request); + result && (result.shouldSkipExecution = skippedExecutions.has(executionId)); + // now that this script is done executing, we trigger the event and move to the next script this.triggers.script(err || null, scriptCursor, result, script, event, item); diff --git a/lib/runner/extensions/item.command.js b/lib/runner/extensions/item.command.js index aee503f61..0b90c6448 100644 --- a/lib/runner/extensions/item.command.js +++ b/lib/runner/extensions/item.command.js @@ -15,6 +15,20 @@ var _ = require('lodash'), extractVisualizerData, getResponseJSON; +function shouldSkipExecution (executions) { + if (!executions) { + return false; + } + + if (!Array.isArray(executions)) { + executions = [executions]; + } + + return executions.some((execution) => { + return execution && execution.result && execution.result.shouldSkipExecution; + }); +} + /** * Returns visualizer data from the latest execution result. * @@ -163,6 +177,14 @@ module.exports = { }); } + if (shouldSkipExecution(prereqExecutions)) { + this.triggers.item(prereqExecutionError, coords, item); + + return callback && callback.call(this, null, { + prerequest: prereqExecutions + }); + } + // update allowed request mutation properties with the mutated context // @note from this point forward, make sure this mutated // request instance is used for upcoming commands. diff --git a/test/integration/request-flow/skip-execution-from-sandbox.test.js b/test/integration/request-flow/skip-execution-from-sandbox.test.js new file mode 100644 index 000000000..110926d22 --- /dev/null +++ b/test/integration/request-flow/skip-execution-from-sandbox.test.js @@ -0,0 +1,269 @@ +var expect = require('chai').expect, + _ = require('lodash'); + + +describe('pm.execution.skipRequest: ', function () { + describe('when single request is run', function () { + var testrun, + cookieUrl = 'https://postman-echo.com/cookies'; + + before(function (done) { + this.run({ + collection: { + item: { + event: [{ + listen: 'prerequest', + script: { + exec: ` + pm.execution.skipRequest(); + ` + } + }, { + listen: 'test', + script: { + exec: ` + console.log('Test'); + ` + } + }], + request: { + url: cookieUrl, + header: [{ key: 'Cookie', value: 'foo=bar;' }] + } + } + } + }, function (err, results) { + testrun = results; + done(err); + }); + }); + + it('should not send the request if invoked skipRequest in pre-request script', function () { + expect(testrun).to.be.ok; + expect(testrun.done.getCall(0).args[0]).to.be.null; + expect(testrun).to.nested.include({ + 'done.calledOnce': true, + 'start.calledOnce': true, + 'io.calledOnce': false, + 'request.calledOnce': false + }); + }); + + it('should not send run test script if invoked skipRequest in pre-request script', function () { + expect(testrun).to.be.ok; + expect(testrun).to.nested.include({ + 'test.calledOnce': false, + 'prerequest.calledOnce': true + }); + }); + }); + + + describe('when running multiple requests in a collection run', function () { + var testRun, + runOptions = { + data: [{ + 'key-4': 'data-value-4' + }], + globals: { + values: [ + { key: 'key-1', value: 'global-value-1', name: 'key-1', enabled: true }, + { key: 'key-2', value: 'global-value-2', name: 'key-2', enabled: true } + ] + }, + environment: { + values: [ + { key: 'key-3', value: 'env-value-3', name: 'key-3', enabled: true }, + { key: 'key-4', value: 'env-value-4', name: 'key-4', enabled: true } + ] + } + }; + + before(function (done) { + var clonedRunOptions = _.merge({}, runOptions, { + collection: { + variable: [ + { key: 'key-2', value: 'coll-value-2', name: 'key-2', enabled: true }, + { key: 'key-3', value: 'coll-value-3', name: 'key-3', enabled: true } + ], + event: [ + { + listen: 'prerequest', + script: { + exec: ` + console.log('collection pre', pm.variables.toObject()) + ` } + }, + { + listen: 'test', + script: { + exec: ` + ` } + } + ], + item: [{ + name: 'Sample Request 1', + event: [ + { + listen: 'prerequest', + script: { + exec: ` + pm.variables.set('key-1', 'modified-1'); + pm.execution.skipRequest(); + postman.setNextRequest('Sample Request 2'); + pm.sendRequest('https://postman-echo.com/GET'); + pm.variables.set('key-2', 'modified-2'); + console.log('item 1 pre', pm.variables.toObject()) + ` } + }, + { + listen: 'test', + script: { + exec: ` + pm.variables.set('key-3', 'modified-3'); + console.log('item 1 test', pm.variables.toObject()) + ` } + } + ], + request: { + url: 'https://postman-echo.com/get?param={{key-1}}:{{key-2}}:{{key-3}}:{{key-4}}', + auth: { + type: 'bearer', + bearer: { + token: '{{key-1}}:{{key-2}}:{{key-3}}:{{key-4}}' + } + } + } + }, { + name: 'Sample Request 2', + event: [ + { + listen: 'prerequest', + script: { + exec: ` + pm.variables.set('key-4', 'modified-4'); + console.log('item 2 pre', pm.variables.toObject()) + ` } + }, + { + listen: 'test', + script: { + exec: ` + console.log('item 2 test', pm.variables.toObject()) + pm.variables.unset('key-1') + pm.variables.unset('key-2') + pm.execution.skipRequest(); + + pm.variables.unset('key-3') + pm.variables.unset('key-4') + console.log('item 2 test after unsetting', pm.variables.toObject()) + ` } + } + ], + request: { + url: 'https://postman-echo.com/get?param={{key-1}}:{{key-2}}:{{key-3}}:{{key-4}}', + auth: { + type: 'bearer', + bearer: { + token: '{{key-1}}:{{key-2}}:{{key-3}}:{{key-4}}' + } + } + } + }] + } + }); + + this.run(clonedRunOptions, function (err, results) { + testRun = results; + + done(err); + }); + }); + + it('should not reflect any variable change line after pm.execution.skipRequest', function () { + expect(testRun.console.callCount).to.equal(5); + + var collPreConsole = testRun.console.getCall(0).args.slice(2), + collPreConsole2 = testRun.console.getCall(1).args.slice(2), + item2PreConsole = testRun.console.getCall(2).args.slice(2), + item2TestConsole = testRun.console.getCall(3).args.slice(2), + item2TestConsoleAfterUnsetting = testRun.console.getCall(4).args.slice(2); + + expect(testRun).to.be.ok; + expect(testRun).to.nested.include({ + 'done.calledOnce': true, + 'start.calledOnce': true, + 'io.calledOnce': true, + 'request.calledOnce': true + }); + + expect(collPreConsole).to.deep.include.ordered.members([ + 'collection pre', + { + 'key-1': 'global-value-1', + 'key-2': 'coll-value-2', + 'key-3': 'env-value-3', + 'key-4': 'data-value-4' + } + ]); + + expect(collPreConsole2).to.deep.include.ordered.members([ + 'collection pre', + { + 'key-1': 'modified-1', + 'key-2': 'coll-value-2', + 'key-3': 'env-value-3', + 'key-4': 'data-value-4' + } + ]); + + expect(item2PreConsole).to.deep.include.ordered.members([ + 'item 2 pre', + { + 'key-1': 'modified-1', + 'key-2': 'coll-value-2', + 'key-3': 'env-value-3', + 'key-4': 'modified-4' + } + ]); + + expect(item2TestConsole).to.deep.include.ordered.members([ + 'item 2 test', + { + 'key-1': 'modified-1', + 'key-2': 'coll-value-2', + 'key-3': 'env-value-3', + 'key-4': 'modified-4' + } + ]); + + expect(item2TestConsoleAfterUnsetting).to.deep.include.ordered.members([ + 'item 2 test after unsetting', + { + 'key-1': 'global-value-1', + 'key-2': 'coll-value-2', + 'key-3': 'env-value-3', + 'key-4': 'data-value-4' + } + ]); + }); + + it('should not resolve variables values mutated after skipRequest', function () { + var url1 = testRun.request.getCall(0).args[3].url.toString(), + expectedToken = 'modified-1:coll-value-2:env-value-3:modified-4'; + + expect(url1).to.equal('https://postman-echo.com/get?param=' + expectedToken); + }); + + it('should not have setNextRequest property set if called after skipRequest', function () { + const prerequest = testRun.script.getCall(1).args[2]; + + expect(prerequest.return).to.be.ok; + expect(prerequest).to.not.have.nested.property('return.nextRequest', 'Sample Request 2'); + }); + + it('should not invoke sendRequest if called after skipRequest', function () { + expect(testRun.request.callCount).to.equal(1); + }); + }); +});