-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
index.js
441 lines (389 loc) · 19.8 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
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;
};