feat: ✨ Created a mini nodeJS server with NewMan for testing without PostMan GUI.
This will mimic a run in a CD/CI environment or docker container.
This commit is contained in:
530
node_modules/postman-runtime/lib/runner/extensions/event.command.js
generated
vendored
Normal file
530
node_modules/postman-runtime/lib/runner/extensions/event.command.js
generated
vendored
Normal file
@@ -0,0 +1,530 @@
|
||||
var _ = require('lodash'),
|
||||
uuid = require('uuid'),
|
||||
async = require('async'),
|
||||
|
||||
util = require('../util'),
|
||||
sdk = require('postman-collection'),
|
||||
sandbox = require('postman-sandbox'),
|
||||
serialisedError = require('serialised-error'),
|
||||
ToughCookie = require('tough-cookie').Cookie,
|
||||
|
||||
createItemContext = require('../create-item-context'),
|
||||
|
||||
ASSERTION_FAILURE = 'AssertionFailure',
|
||||
SAFE_CONTEXT_VARIABLES = ['_variables', 'environment', 'globals', 'collectionVariables', 'cookies', 'data',
|
||||
'request', 'response'],
|
||||
|
||||
EXECUTION_REQUEST_EVENT_BASE = 'execution.request.',
|
||||
EXECUTION_RESPONSE_EVENT_BASE = 'execution.response.',
|
||||
EXECUTION_ASSERTION_EVENT_BASE = 'execution.assertion.',
|
||||
EXECUTION_ERROR_EVENT_BASE = 'execution.error.',
|
||||
EXECUTION_COOKIES_EVENT_BASE = 'execution.cookies.',
|
||||
|
||||
COOKIES_EVENT_STORE_ACTION = 'store',
|
||||
COOKIE_STORE_PUT_METHOD = 'putCookie',
|
||||
COOKIE_STORE_UPDATE_METHOD = 'updateCookie',
|
||||
|
||||
FILE = 'file',
|
||||
|
||||
REQUEST_BODY_MODE_FILE = 'file',
|
||||
REQUEST_BODY_MODE_FORMDATA = 'formdata',
|
||||
|
||||
getCookieDomain, // fn
|
||||
postProcessContext, // fn
|
||||
sanitizeFiles; // fn
|
||||
|
||||
postProcessContext = function (execution, failures) { // function determines whether the event needs to abort
|
||||
var error;
|
||||
|
||||
if (failures && failures.length) {
|
||||
error = new Error(failures.join(', '));
|
||||
error.name = ASSERTION_FAILURE;
|
||||
}
|
||||
|
||||
return error ? serialisedError(error, true) : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes files in Request body if any.
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @param {Request~definition} request Request JSON representation to be sanitized
|
||||
* @param {Function} callback function invoked with error, request and sanitisedFiles.
|
||||
* sanitisedFiles is the list of files removed from request.
|
||||
*
|
||||
* @note this function mutates the request
|
||||
* @todo remove files path from request.certificate
|
||||
*/
|
||||
sanitizeFiles = function (request, callback) {
|
||||
if (!request) {
|
||||
return callback(new Error('Could not complete pm.sendRequest. Request is empty.'));
|
||||
}
|
||||
|
||||
var sanitisedFiles = [];
|
||||
|
||||
// do nothing if request body is empty
|
||||
if (!request.body) {
|
||||
// send request as such
|
||||
return callback(null, request, sanitisedFiles);
|
||||
}
|
||||
|
||||
// in case of request body mode is file, we strip it out
|
||||
if (request.body.mode === REQUEST_BODY_MODE_FILE) {
|
||||
sanitisedFiles.push(_.get(request, 'body.file.src'));
|
||||
request.body = null; // mutate the request for body
|
||||
}
|
||||
|
||||
// if body is form-data then we deep dive into the data items and remove the entries that have file data
|
||||
else if (request.body.mode === REQUEST_BODY_MODE_FORMDATA) {
|
||||
// eslint-disable-next-line lodash/prefer-immutable-method
|
||||
_.remove(request.body.formdata, function (param) {
|
||||
// blank param and non-file param is removed
|
||||
if (!param || param.type !== FILE) { return false; }
|
||||
|
||||
// at this point the param needs to be removed
|
||||
sanitisedFiles.push(param.src);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null, request, sanitisedFiles);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch domain name from CookieStore event arguments.
|
||||
*
|
||||
* @private
|
||||
* @param {String} fnName - CookieStore method name
|
||||
* @param {Array} args - CookieStore method arguments
|
||||
* @returns {String|Undefined} - Domain name
|
||||
*/
|
||||
getCookieDomain = function (fnName, args) {
|
||||
if (!(fnName && args)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var domain;
|
||||
|
||||
switch (fnName) {
|
||||
case 'findCookie':
|
||||
case 'findCookies':
|
||||
case 'removeCookie':
|
||||
case 'removeCookies':
|
||||
domain = args[0];
|
||||
break;
|
||||
case 'putCookie':
|
||||
case 'updateCookie':
|
||||
domain = args[0] && args[0].domain;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
return domain;
|
||||
};
|
||||
|
||||
/**
|
||||
* Script execution extension of the runner.
|
||||
* This module exposes processors for executing scripts before and after requests. Essentially, the processors are
|
||||
* itself not aware of other processors and simply allow running of a script and then queue a procesor as defined in
|
||||
* payload.
|
||||
*
|
||||
* Adds options
|
||||
* - stopOnScriptError:Boolean [false]
|
||||
* - host:Object [undefined]
|
||||
*/
|
||||
module.exports = {
|
||||
init: function (done) {
|
||||
var run = this;
|
||||
|
||||
// if this run object already has a host, we do not need to create one.
|
||||
if (run.host) {
|
||||
return done();
|
||||
}
|
||||
|
||||
// @todo - remove this when chrome app and electron host creation is offloaded to runner
|
||||
// @todo - can this be removed now in runtime v4?
|
||||
if (run.options.host && run.options.host.external === true) {
|
||||
run.host = run.options.host.instance;
|
||||
|
||||
return done();
|
||||
}
|
||||
|
||||
sandbox.createContext(_.merge({
|
||||
timeout: _(run.options.timeout).pick(['script', 'global']).values().min()
|
||||
// debug: true
|
||||
}, run.options.host), function (err, context) {
|
||||
if (err) { return done(err); }
|
||||
// store the host in run object for future use and move on
|
||||
run.host = context;
|
||||
|
||||
context.on('console', function () {
|
||||
run.triggers.console.apply(run.triggers, arguments);
|
||||
});
|
||||
|
||||
context.on('error', function () {
|
||||
run.triggers.error.apply(run.triggers, arguments);
|
||||
});
|
||||
|
||||
context.on('execution.error', function () {
|
||||
run.triggers.exception.apply(run.triggers, arguments);
|
||||
});
|
||||
|
||||
context.on('execution.assertion', function () {
|
||||
run.triggers.assertion.apply(run.triggers, arguments);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* This lists the name of the events that the script processors are likely to trigger
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
triggers: ['beforeScript', 'script', 'assertion', 'exception', 'console'],
|
||||
|
||||
process: {
|
||||
/**
|
||||
* This processors job is to do the following:
|
||||
* - trigger event by its name
|
||||
* - execute all scripts that the event listens to and return execution results
|
||||
*
|
||||
* @param {Object} payload
|
||||
* @param {String} payload.name
|
||||
* @param {Item} payload.item
|
||||
* @param {Object} [payload.context]
|
||||
* @param {Cursor} [payload.coords]
|
||||
* @param {Number} [payload.scriptTimeout] - The millisecond timeout for the current running script.
|
||||
* @param {Array.<String>} [payload.trackContext]
|
||||
* @param {Boolean} [payload.stopOnScriptError] - if set to true, then a synchronous error encountered during
|
||||
* execution of a script will stop executing any further scripts
|
||||
* @param {Boolean} [payload.abortOnFailure]
|
||||
* @param {Boolean} [payload.stopOnFailure]
|
||||
* @param {Function} next
|
||||
*
|
||||
* @note - in order to raise trigger for the entire event, ensure your extension has registered the triggers
|
||||
*/
|
||||
event: function (payload, next) {
|
||||
var item = payload.item,
|
||||
eventName = payload.name,
|
||||
cursor = payload.coords,
|
||||
// the payload can have a list of variables to track from the context post execution, ensure that
|
||||
// those are accurately set
|
||||
track = _.isArray(payload.trackContext) && _.isObject(payload.context) &&
|
||||
// ensure that only those variables that are defined in the context are synced
|
||||
payload.trackContext.filter(function (variable) {
|
||||
return _.isObject(payload.context[variable]);
|
||||
}),
|
||||
stopOnScriptError = (_.has(payload, 'stopOnScriptError') ? payload.stopOnScriptError :
|
||||
this.options.stopOnScriptError),
|
||||
abortOnError = (_.has(payload, 'abortOnError') ? payload.abortOnError : this.options.abortOnError),
|
||||
|
||||
// @todo: find a better home for this option processing
|
||||
abortOnFailure = payload.abortOnFailure,
|
||||
stopOnFailure = payload.stopOnFailure,
|
||||
|
||||
events;
|
||||
|
||||
// @todo: find a better place to code this so that event is not aware of such options
|
||||
if (abortOnFailure) {
|
||||
abortOnError = true;
|
||||
}
|
||||
|
||||
// validate the payload
|
||||
if (!eventName) {
|
||||
return next(new Error('runner.extension~events: event payload is missing the event name.'));
|
||||
}
|
||||
if (!item) {
|
||||
return next(new Error('runner.extension~events: event payload is missing the triggered item.'));
|
||||
}
|
||||
|
||||
// get the list of events to be executed
|
||||
// includes events in parent as well
|
||||
events = item.events.listeners(eventName, {excludeDisabled: true});
|
||||
|
||||
// call the "before" event trigger by its event name.
|
||||
// at this point, the one who queued this event, must ensure that the trigger for it is defined in its
|
||||
// 'trigger' interface
|
||||
this.triggers[_.camelCase('before-' + eventName)](null, cursor, events, item);
|
||||
|
||||
// with all the event listeners in place, we now iterate on them and execute its scripts. post execution,
|
||||
// we accumulate the results in order to be passed on to the event callback trigger.
|
||||
async.mapSeries(events, function (event, next) {
|
||||
// in case the event has no script we bail out early
|
||||
if (!event.script) {
|
||||
return next(null, {event: event});
|
||||
}
|
||||
|
||||
// get access to the script from the event.
|
||||
var script = event.script,
|
||||
executionId = uuid(),
|
||||
assertionFailed = [],
|
||||
asyncScriptError,
|
||||
|
||||
// create copy of cursor so we don't leak script ids outside `event.command`
|
||||
// and across scripts
|
||||
scriptCursor = _.clone(cursor);
|
||||
|
||||
// store the execution id in script
|
||||
script._lastExecutionId = executionId; // please don't use it anywhere else!
|
||||
|
||||
// if we can find an id on script or event we add them to the cursor
|
||||
// so logs and errors can be traced back to the script they came from
|
||||
event.id && (scriptCursor.eventId = event.id);
|
||||
event.script.id && (scriptCursor.scriptId = event.script.id);
|
||||
|
||||
// trigger the "beforeScript" callback
|
||||
this.triggers.beforeScript(null, scriptCursor, script, event, item);
|
||||
|
||||
// add event listener to trap all assertion events, but only if needed. to avoid needlessly accumulate
|
||||
// stuff in memory.
|
||||
(abortOnFailure || stopOnFailure) &&
|
||||
this.host.on(EXECUTION_ASSERTION_EVENT_BASE + executionId, function (scriptCursor, assertions) {
|
||||
_.forEach(assertions, function (assertion) {
|
||||
assertion && !assertion.passed && assertionFailed.push(assertion.name);
|
||||
});
|
||||
});
|
||||
|
||||
// To store error event, but only if needed. Because error in callback of host.execute()
|
||||
// don't show execution errors for async scripts
|
||||
(abortOnError || stopOnScriptError) &&
|
||||
// only store first async error in case of multiple errors
|
||||
this.host.once(EXECUTION_ERROR_EVENT_BASE + executionId, function (scriptCursor, error) {
|
||||
if (error && !(error instanceof Error)) {
|
||||
error = new Error(error.message || error);
|
||||
}
|
||||
|
||||
asyncScriptError = error;
|
||||
|
||||
// @todo: Figure out a way to abort the script execution here as soon as we get an error.
|
||||
// We can send `execution.abort.` event to sandbox for this, but currently it silently
|
||||
// terminates the script execution without triggering the callback.
|
||||
});
|
||||
|
||||
this.host.on(EXECUTION_COOKIES_EVENT_BASE + executionId,
|
||||
function (eventId, action, fnName, args) {
|
||||
// only store action is supported, might need to support
|
||||
// more cookie actions in next 2 years ¯\_(ツ)_/¯
|
||||
if (action !== COOKIES_EVENT_STORE_ACTION) { return; }
|
||||
|
||||
var self = this,
|
||||
dispatchEvent = EXECUTION_COOKIES_EVENT_BASE + executionId,
|
||||
cookieJar = _.get(self, 'requester.options.cookieJar'),
|
||||
cookieStore = cookieJar && cookieJar.store,
|
||||
cookieDomain;
|
||||
|
||||
if (!cookieStore) {
|
||||
return self.host.dispatch(dispatchEvent, eventId, 'CookieStore: no store found');
|
||||
}
|
||||
|
||||
if (typeof cookieStore[fnName] !== 'function') {
|
||||
return self.host.dispatch(dispatchEvent, eventId,
|
||||
`CookieStore: invalid method name '${fnName}'`);
|
||||
}
|
||||
|
||||
!Array.isArray(args) && (args = []);
|
||||
|
||||
// set expected args length to make sure callback is always called
|
||||
args.length = cookieStore[fnName].length - 1;
|
||||
|
||||
// there's no way cookie store can identify the difference
|
||||
// between regular and programmatic access. So, for now
|
||||
// we check for programmatic access using the cookieJar
|
||||
// helper method and emit the default empty value for that
|
||||
// method.
|
||||
// @note we don't emit access denied error here because
|
||||
// that might blocks users use-case while accessing
|
||||
// cookies for a sub-domain.
|
||||
cookieDomain = getCookieDomain(fnName, args);
|
||||
if (cookieJar && typeof cookieJar.allowProgrammaticAccess === 'function' &&
|
||||
!cookieJar.allowProgrammaticAccess(cookieDomain)) {
|
||||
return self.host.dispatch(dispatchEvent, eventId,
|
||||
`CookieStore: programmatic access to "${cookieDomain}" is denied`);
|
||||
}
|
||||
|
||||
// serialize cookie object
|
||||
if (fnName === COOKIE_STORE_PUT_METHOD && args[0]) {
|
||||
args[0] = ToughCookie.fromJSON(args[0]);
|
||||
}
|
||||
|
||||
if (fnName === COOKIE_STORE_UPDATE_METHOD && args[0] && args[1]) {
|
||||
args[0] = ToughCookie.fromJSON(args[0]);
|
||||
args[1] = ToughCookie.fromJSON(args[1]);
|
||||
}
|
||||
|
||||
// add store method's callback argument
|
||||
args.push(function (err, res) {
|
||||
// serialize error message
|
||||
if (err && err instanceof Error) {
|
||||
err = err.message || String(err);
|
||||
}
|
||||
|
||||
self.host.dispatch(dispatchEvent, eventId, err, res);
|
||||
});
|
||||
|
||||
try {
|
||||
cookieStore[fnName].apply(cookieStore, args);
|
||||
}
|
||||
catch (error) {
|
||||
self.host.dispatch(dispatchEvent, eventId,
|
||||
`runtime~CookieStore: error executing "${fnName}"`);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.host.on(EXECUTION_REQUEST_EVENT_BASE + executionId,
|
||||
function (scriptCursor, id, requestId, request) {
|
||||
// remove files in request body if any
|
||||
sanitizeFiles(request, function (err, request, sanitisedFiles) {
|
||||
if (err) {
|
||||
return this.host.dispatch(EXECUTION_RESPONSE_EVENT_BASE + id, requestId, err);
|
||||
}
|
||||
|
||||
var nextPayload;
|
||||
|
||||
// if request is sanitized send a warning
|
||||
if (!_.isEmpty(sanitisedFiles)) {
|
||||
this.triggers.console(scriptCursor, 'warn',
|
||||
'uploading files from scripts is not allowed');
|
||||
}
|
||||
|
||||
nextPayload = {
|
||||
item: new sdk.Item({request: request}),
|
||||
coords: scriptCursor,
|
||||
// @todo - get script type from the sandbox
|
||||
source: 'script',
|
||||
// abortOnError makes sure request command bubbles errors
|
||||
// so we can pass it on to the callback
|
||||
abortOnError: true
|
||||
};
|
||||
|
||||
// create context for executing this request
|
||||
nextPayload.context = createItemContext(nextPayload);
|
||||
|
||||
this.immediate('httprequest', nextPayload).done(function (result) {
|
||||
this.host.dispatch(
|
||||
EXECUTION_RESPONSE_EVENT_BASE + id,
|
||||
requestId,
|
||||
null,
|
||||
result && result.response,
|
||||
|
||||
// @todo get cookies from result.history or pass PostmanHistory
|
||||
// instance once it is fully supported
|
||||
result && {cookies: result.cookies}
|
||||
);
|
||||
}).catch(function (err) {
|
||||
this.host.dispatch(EXECUTION_RESPONSE_EVENT_BASE + id, requestId, err);
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
|
||||
// finally execute the script
|
||||
this.host.execute(event, {
|
||||
id: executionId,
|
||||
// debug: true,
|
||||
timeout: payload.scriptTimeout, // @todo: Expose this as a property in Collection SDK's Script
|
||||
cursor: scriptCursor,
|
||||
context: _.pick(payload.context, SAFE_CONTEXT_VARIABLES),
|
||||
serializeLogs: _.get(this, 'options.script.serializeLogs'),
|
||||
|
||||
// legacy options
|
||||
legacy: {
|
||||
_itemId: item.id,
|
||||
_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);
|
||||
|
||||
// Handle async errors as well.
|
||||
// If there was an error running the script itself, that takes precedence
|
||||
if (!err && asyncScriptError) {
|
||||
err = asyncScriptError;
|
||||
}
|
||||
|
||||
// electron IPC does not bubble errors to the browser process, so we serialize it here.
|
||||
err && (err = serialisedError(err, true));
|
||||
|
||||
// if it is defined that certain variables are to be synced back to result, we do the same
|
||||
track && result && track.forEach(function (variable) {
|
||||
if (!(_.isObject(result[variable]) && payload.context[variable])) { return; }
|
||||
|
||||
var contextVariable = payload.context[variable],
|
||||
mutations = result[variable].mutations;
|
||||
|
||||
// bail out if there are no mutations
|
||||
if (!mutations) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure that variable scope is treated accordingly
|
||||
if (_.isFunction(contextVariable.applyMutation)) {
|
||||
mutations = new sdk.MutationTracker(result[variable].mutations);
|
||||
|
||||
mutations.applyOn(contextVariable);
|
||||
}
|
||||
|
||||
// @todo: unify the non variable scope flows and consume diff always
|
||||
// and drop sending the full variable scope from sandbox
|
||||
else {
|
||||
util.syncObject(contextVariable, result[variable]);
|
||||
}
|
||||
});
|
||||
|
||||
// Get the failures. If there was an error running the script itself, that takes precedence
|
||||
if (!err && (abortOnFailure || stopOnFailure)) {
|
||||
err = postProcessContext(result, assertionFailed); // also use async assertions
|
||||
}
|
||||
|
||||
// Ensure that we have SDK instances, not serialized plain objects.
|
||||
// @todo - should this be handled by the sandbox?
|
||||
result && result._variables && (result._variables = new sdk.VariableScope(result._variables));
|
||||
result && result.environment && (result.environment = new sdk.VariableScope(result.environment));
|
||||
result && result.globals && (result.globals = new sdk.VariableScope(result.globals));
|
||||
result && result.collectionVariables &&
|
||||
(result.collectionVariables = new sdk.VariableScope(result.collectionVariables));
|
||||
result && result.request && (result.request = new sdk.Request(result.request));
|
||||
|
||||
// @note Since postman-sandbox@3.5.2, response object is not included in the execution result.
|
||||
// Refer: https://github.com/postmanlabs/postman-sandbox/pull/512
|
||||
// Adding back here to avoid breaking change in `script` callback.
|
||||
// @todo revisit script callback args in runtime v8.
|
||||
result && payload.context && payload.context.response &&
|
||||
(result.response = new sdk.Response(payload.context.response));
|
||||
|
||||
// persist the pm.variables for the next script
|
||||
result && result._variables &&
|
||||
(payload.context._variables = new sdk.VariableScope(result._variables));
|
||||
|
||||
// persist the pm.variables for the next request
|
||||
result && result._variables && (this.state._variables = new sdk.VariableScope(result._variables));
|
||||
|
||||
// persist the mutated request in payload context,
|
||||
// @note this will be used for the next prerequest script or
|
||||
// upcoming commands(request, httprequest).
|
||||
result && result.request && (payload.context.request = result.request);
|
||||
|
||||
// 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);
|
||||
|
||||
// move to next script and pass on the results for accumulation
|
||||
next(((stopOnScriptError || abortOnError || stopOnFailure) && err) ? err : null, _.assign({
|
||||
event: event,
|
||||
script: script,
|
||||
result: result
|
||||
}, err && {error: err})); // we use assign here to avoid needless error property
|
||||
}.bind(this));
|
||||
}.bind(this), function (err, results) {
|
||||
// trigger the event completion callback
|
||||
this.triggers[eventName](null, cursor, results, item);
|
||||
next((abortOnError && err) ? err : null, results, err);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user