var _ = require('lodash'), asyncEach = require('async/each'), sdk = require('postman-collection'), runtime = require('postman-runtime'), request = require('postman-request'), EventEmitter = require('eventemitter3'), SecureFS = require('./secure-fs'), RunSummary = require('./summary'), getOptions = require('./options'), exportFile = require('./export-file'), util = require('../util'), /** * This object describes the various events raised by Newman, and what each event argument contains. * Error and cursor are present in all events. * * @type {Object} */ runtimeEvents = { beforeIteration: [], beforeItem: ['item'], beforePrerequest: ['events', 'item'], prerequest: ['executions', 'item'], beforeRequest: ['request', 'item'], request: ['response', 'request', 'item', 'cookies', 'history'], beforeTest: ['events', 'item'], test: ['executions', 'item'], item: ['item'], iteration: [], beforeScript: ['script', 'event', 'item'], script: ['execution', 'script', 'event', 'item'] }, /** * load all the default reporters here. if you have new reporter, add it to this list * we know someone, who does not like dynamic requires * * @type {Object} */ defaultReporters = { cli: require('../reporters/cli'), json: require('../reporters/json'), junit: require('../reporters/junit'), progress: require('../reporters/progress'), emojitrain: require('../reporters/emojitrain') }, /** * The object of known reporters and their install instruction in case the reporter is not loaded. * Pad message with two spaces since its a follow-up message for reporter warning. * * @private * @type {Object} */ knownReporterErrorMessages = { html: ' run `npm install newman-reporter-html`\n', teamcity: ' run `npm install newman-reporter-teamcity`\n' }, /** * Multiple ids or names entrypoint lookup strategy. * * @private * @type {String} */ MULTIENTRY_LOOKUP_STRATEGY = 'multipleIdOrName'; /** * Runs the collection, with all the provided options, returning an EventEmitter. * * @param {Object} options - The set of wrapped options, passed by the CLI parser. * @param {Collection|Object|String} options.collection - A JSON / Collection / String representing the collection. * @param {Object|String} options.environment - An environment JSON / file path for the current collection run. * @param {Object|String} options.globals - A globals JSON / file path for the current collection run. * @param {String} options.workingDir - Path of working directory that contains files needed for the collection run. * @param {String} options.insecureFileRead - If true, allow reading files outside of working directory. * @param {Object|String} options.iterationData - An iterationData JSON / file path for the current collection run. * @param {Object|String} options.reporters - A set of reporter names and their associated options for the current run. * @param {Object|String} options.cookieJar - A tough-cookie cookieJar / file path for the current collection run. * @param {String} options.exportGlobals - The relative path to export the globals file from the current run to. * @param {String} options.exportEnvironment - The relative path to export the environment file from the current run to. * @param {String} options.exportCollection - The relative path to export the collection from the current run to. * @param {String} options.exportCookieJar - The relative path to export the cookie jar from the current run to. * @param {Function} callback - The callback function invoked to mark the end of the collection run. * @returns {EventEmitter} - An EventEmitter instance with done and error event attachments. */ module.exports = function (options, callback) { // validate all options. it is to be noted that `options` parameter is option and is polymorphic (!callback && _.isFunction(options)) && ( (callback = options), (options = {}) ); !_.isFunction(callback) && (callback = _.noop); var emitter = new EventEmitter(), // @todo: create a new inherited constructor runner = new runtime.Runner(), stopOnFailure, entrypoint; // get the configuration from various sources getOptions(options, function (err, options) { if (err) { return callback(err); } // ensure that the collection option is present before starting a run if (!_.isObject(options.collection)) { return callback(new Error('expecting a collection to run')); } // use client certificate list to allow different ssl certificates for // different URLs var sslClientCertList = options.sslClientCertList || [], // allow providing custom cookieJar cookieJar = options.cookieJar || request.jar(); // if sslClientCert option is set, put it at the end of the list to // match all URLs that didn't match in the list if (options.sslClientCert) { sslClientCertList.push({ name: 'client-cert', matches: [sdk.UrlMatchPattern.MATCH_ALL_URLS], key: { src: options.sslClientKey }, cert: { src: options.sslClientCert }, passphrase: options.sslClientPassphrase }); } // iterates over the bail array and sets each item as an obj key with a value of boolean true // [item1, item2] => {item1: true, item2: true} if (_.isArray(options.bail)) { options.bail = _.transform(options.bail, function (result, value) { result[value] = true; }, {}); } // sets entrypoint to execute if options.folder is specified. if (options.folder) { entrypoint = { execute: options.folder }; // uses `multipleIdOrName` lookupStrategy in case of multiple folders. _.isArray(entrypoint.execute) && (entrypoint.lookupStrategy = MULTIENTRY_LOOKUP_STRATEGY); } // sets stopOnFailure to true in case bail is used without any modifiers or with failure // --bail => stopOnFailure = true // --bail failure => stopOnFailure = true (typeof options.bail !== 'undefined' && (options.bail === true || (_.isObject(options.bail) && options.bail.failure))) ? stopOnFailure = true : stopOnFailure = false; // store summary object and other relevant information inside the emitter emitter.summary = new RunSummary(emitter, options); // to store the exported content from reporters emitter.exports = []; // expose the runner object for reporter and programmatic use emitter.runner = runner; // now start the run! runner.run(options.collection, { stopOnFailure: stopOnFailure, // LOL, you just got trolled ¯\_(ツ)_/¯ abortOnFailure: options.abortOnFailure, // used in integration tests, to be considered for a future release abortOnError: _.get(options, 'bail.folder'), iterationCount: options.iterationCount, environment: options.environment, globals: options.globals, entrypoint: entrypoint, data: options.iterationData, delay: { item: options.delayRequest }, timeout: { global: options.timeout || 0, request: options.timeoutRequest || 0, script: options.timeoutScript || 0 }, fileResolver: new SecureFS(options.workingDir, options.insecureFileRead), requester: { useWhatWGUrlParser: true, cookieJar: cookieJar, followRedirects: _.has(options, 'ignoreRedirects') ? !options.ignoreRedirects : undefined, strictSSL: _.has(options, 'insecure') ? !options.insecure : undefined, timings: Boolean(options.verbose), extendedRootCA: options.sslExtraCaCerts, agents: _.isObject(options.requestAgents) ? options.requestAgents : undefined }, certificates: sslClientCertList.length && new sdk.CertificateList({}, sslClientCertList) }, function (err, run) { if (err) { return callback(err); } var callbacks = {}, // ensure that the reporter option type polymorphism is handled reporters = _.isString(options.reporters) ? [options.reporters] : options.reporters, // keep a track of start assertion indices of legacy assertions legacyAssertionIndices = {}; // emit events for all the callbacks triggered by the runtime _.forEach(runtimeEvents, function (definition, eventName) { // intercept each runtime.* callback and expose a global object based event callbacks[eventName] = function (err, cursor) { var args = arguments, obj = { cursor }; // convert the arguments into an object by taking the key name reference from the definition // object _.forEach(definition, function (key, index) { obj[key] = args[index + 2]; // first two are err, cursor }); args = [eventName, err, obj]; emitter.emit.apply(emitter, args); // eslint-disable-line prefer-spread }; }); // add non generic callback handling _.assignIn(callbacks, { /** * Emits event for start of the run. It injects/exposes additional objects useful for * programmatic usage and reporters * * @param {?Error} err - An Error instance / null object. * @param {Object} cursor - The run cursor instance. * @returns {*} */ start (err, cursor) { emitter.emit('start', err, { cursor, run }); }, /** * Bubbles up console messages. * * @param {Object} cursor - The run cursor instance. * @param {String} level - The level of console logging [error, silent, etc]. * @returns {*} */ console (cursor, level) { emitter.emit('console', null, { cursor: cursor, level: level, messages: _.slice(arguments, 2) }); }, /** * The exception handler for the current run instance. * * @todo Fix bug of arg order in runtime. * @param {Object} cursor - The run cursor. * @param {?Error} err - An Error instance / null object. * @returns {*} */ exception (cursor, err) { emitter.emit('exception', null, { cursor: cursor, error: err }); }, assertion (cursor, assertions) { _.forEach(assertions, function (assertion) { var errorName = _.get(assertion, 'error.name', 'AssertionError'); !assertion && (assertion = {}); // store the legacy assertion index assertion.index && (legacyAssertionIndices[cursor.ref] = assertion.index); emitter.emit('assertion', (assertion.passed ? null : { name: errorName, index: assertion.index, test: assertion.name, message: _.get(assertion, 'error.message', assertion.name || ''), stack: errorName + ': ' + _.get(assertion, 'error.message', '') + '\n' + ' at Object.eval sandbox-script.js:' + (assertion.index + 1) + ':' + ((cursor && cursor.position || 0) + 1) + ')' }), { cursor: cursor, assertion: assertion.name, skipped: assertion.skipped, error: assertion.error, item: run.resolveCursor(cursor) }); }); }, /** * Custom callback to override the `done` event to fire the end callback. * * @todo Do some memory cleanup here? * @param {?Error} err - An error instance / null passed from the done event handler. * @param {Object} cursor - The run instance cursor. * @returns {*} */ done (err, cursor) { // in case runtime faced an error during run, we do not process any other event and emit `done`. // we do it this way since, an error in `done` callback would have anyway skipped any intermediate // events or callbacks if (err) { emitter.emit('done', err, emitter.summary); callback(err, emitter.summary); return; } // we emit a `beforeDone` event so that reporters and other such addons can do computation before // the run is marked as done emitter.emit('beforeDone', null, { cursor: cursor, summary: emitter.summary }); _.forEach(['environment', 'globals', 'collection', 'cookie-jar'], function (item) { // fetch the path name from options if one is provided var path = _.get(options, _.camelCase(`export-${item}`)); // if the options have an export path, then add the item to export queue path && emitter.exports.push({ name: item, default: `newman-${item}.json`, path: path, content: item === 'cookie-jar' ? cookieJar.toJSON() : _(emitter.summary[item].toJSON()) .defaults({ name: item }) .merge({ _postman_variable_scope: item, _postman_exported_at: (new Date()).toISOString(), _postman_exported_using: util.userAgent }) .value() }); }); asyncEach(emitter.exports, exportFile, function (err) { // we now trigger actual done event which we had overridden emitter.emit('done', err, emitter.summary); callback(err, emitter.summary); }); } }); emitter.on('script', function (err, o) { // bubble special script name based events o && o.event && emitter.emit(o.event.listen + 'Script', err, o); }); emitter.on('beforeScript', function (err, o) { // bubble special script name based events o && o.event && emitter.emit(_.camelCase('before-' + o.event.listen + 'Script'), err, o); }); // initialise all the reporters !emitter.reporters && (emitter.reporters = {}); _.isArray(reporters) && _.forEach(reporters, function (reporterName) { // disallow duplicate reporter initialisation if (_.has(emitter.reporters, reporterName)) { return; } var Reporter; try { // check if the reporter is an external reporter Reporter = require((function (name) { // ensure scoped packages are loaded var prefix = '', scope = (name.charAt(0) === '@') && name.substr(0, name.indexOf('/') + 1); if (scope) { prefix = scope; name = name.substr(scope.length); } return prefix + 'newman-reporter-' + name; }(reporterName))); } // @todo - maybe have a debug mode and log error there catch (error) { if (!defaultReporters[reporterName]) { // @todo: route this via print module to respect silent flags console.warn(`newman: could not find "${reporterName}" reporter`); console.warn(' ensure that the reporter is installed in the same directory as newman'); // print install instruction in case a known reporter is missing if (knownReporterErrorMessages[reporterName]) { console.warn(knownReporterErrorMessages[reporterName]); } else { console.warn(' please install reporter using npm\n'); } } } // load local reporter if its not an external reporter !Reporter && (Reporter = defaultReporters[reporterName]); try { // we could have checked _.isFunction(Reporter), here, but we do not do that so that the nature of // reporter error can be bubbled up Reporter && (emitter.reporters[reporterName] = new Reporter(emitter, _.get(options, ['reporter', reporterName], {}), options)); } catch (error) { // if the reporter errored out during initialisation, we should not stop the run simply log // the error stack trace for debugging console.warn(`newman: could not load "${reporterName}" reporter`); if (!defaultReporters[reporterName]) { // @todo: route this via print module to respect silent flags console.warn(` this seems to be a problem in the "${reporterName}" reporter.\n`); } console.warn(error); } }); // raise warning when more than one dominant reporters are used (function (reporters) { // find all reporters whose `dominant` key is set to true var conflicts = _.keys(_.transform(reporters, function (conflicts, reporter, name) { reporter.dominant && (conflicts[name] = true); })); (conflicts.length > 1) && // if more than one dominant, raise a warning console.warn(`newman: ${conflicts.join(', ')} reporters might not work well together.`); }(emitter.reporters)); // we ensure that everything is async to comply with event paradigm and start the run setImmediate(function () { run.start(callbacks); }); }); }); return emitter; };