Simon Priet e69a613a37 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.
2021-09-08 14:01:19 +02:00

495 lines
18 KiB
JavaScript

var _ = require('lodash'),
core = require('./core'),
Emitter = require('events'),
inherits = require('inherits'),
now = require('performance-now'),
sdk = require('postman-collection'),
requests = require('./request-wrapper'),
dryRun = require('./dry-run'),
RESPONSE_START_EVENT_BASE = 'response.start.',
RESPONSE_END_EVENT_BASE = 'response.end.',
RESPONSE_START = 'responseStart',
RESPONSE_END = 'response',
ERROR_RESTRICTED_ADDRESS = 'NETERR: getaddrinfo ENOTFOUND ',
/**
* Headers which get overwritten by the requester.
*
* @private
* @const
* @type {Object}
*/
OVERWRITTEN_HEADERS = {
cookie: true, // cookies get appended with `;`
'content-length': true
},
/**
* Creates a sdk compatible cookie from a tough-cookie compatible cookie.
*
* @param cookie
* @returns {Object}
*/
toPostmanCookie = function (cookie) {
var expires = cookie.expiryTime();
cookie.toJSON && (cookie = cookie.toJSON());
return new sdk.Cookie({
name: cookie.key,
value: cookie.value,
expires: Number.isFinite(expires) ? new Date(expires) : null,
maxAge: cookie.maxAge,
domain: cookie.domain,
path: cookie.path,
secure: cookie.secure,
httpOnly: cookie.httpOnly,
hostOnly: cookie.hostOnly,
extensions: cookie.extensions
});
},
/**
* This method is used in conjunction with _.transform method to convert multi-value headers to multiple single
* value headers
*
* @param {Array} acc
* @param {Array|String} val
* @param {String} key
* @return {Object}
*/
transformMultiValueHeaders = function (acc, val, key) {
var i, ii;
if (Array.isArray(val)) {
for (i = 0, ii = val.length; i < ii; i++) {
acc.push({
key: key,
value: val[i]
});
}
}
else {
acc.push({
key: key,
value: val
});
}
},
/**
* Calculate request timings offset by adding runtime overhead which
* helps to determine request prepare and process time taken.
*
* @param {Number} runtimeTimer - Runtime request start HR time
* @param {Number} requestTimer - Request start HR time
* @param {Object} timings - Request timings offset
* @returns {Object}
*/
calcTimingsOffset = function (runtimeTimer, requestTimer, timings) {
if (!(runtimeTimer && requestTimer && timings)) { return; }
// runtime + postman-request initialization time
var initTime = requestTimer - runtimeTimer,
offset = {
request: initTime
};
// add initialization overhead to request offsets
_.forOwn(timings, function (value, key) {
offset[key] = value + initTime;
});
// total time taken by runtime to get the response
// @note if offset.end is missing, that means request is not complete.
// this is used to calculate timings on responseStart.
if (offset.end) {
offset.done = now() - runtimeTimer;
}
return offset;
},
Requester;
/**
* Creates a new Requester, which is used to make HTTP(s) requests.
*
* @param trace
* @param options
* @param {Boolean} [options.keepAlive=true] Optimizes HTTP connections by keeping them alive, so that new requests
* to the same host are made over the same underlying TCP connection.
* @param {CookieJar} [options.cookieJar] A cookie jar to use with Node requests.
* @param {Boolean} [options.strictSSL]
* @param {Boolean} [options.followRedirects=true] If false, returns a 301/302 as the response code
* instead of following the redirect
* @note `options.keepAlive` is only supported in Node.
* @note `options.cookieJar` is only supported in Node.
*
* @constructor
*/
inherits(Requester = function (trace, options) {
this.options = options || {};
// protect the timeout value from being non-numeric or infinite
if (!_.isFinite(this.options.timeout)) {
this.options.timeout = undefined;
}
this.trace = trace;
Requester.super_.call(this);
}, Emitter);
_.assign(Requester.prototype, /** @lends Requester.prototype */ {
/**
* Perform an HTTP request.
*
* @param {String} id
* @param {Request} request
* @param {Object} protocolProfileBehavior
* @param {Function} callback
*/
request: function (id, request, protocolProfileBehavior, callback) {
var self = this,
hostname,
cookieJar,
requestOptions,
networkOptions = self.options.network || {},
startTime = Date.now(),
startTimer = now(), // high-resolution time
cookies = [],
responseHeaders = [],
responseJSON = {},
// keep track of `responseStart` and `response` triggers
_responseStarted = false,
_responseEnded = false,
_responseData = {},
// Refer: https://github.com/postmanlabs/postman-runtime/blob/v7.14.0/docs/history.md
getExecutionHistory = function (debugInfo) {
var history = {
execution: {
verbose: Boolean(requestOptions.verbose),
sessions: {},
data: []
}
},
executionData = [],
requestSessions = {};
if (!Array.isArray(debugInfo)) {
return history;
}
// prepare history from request debug data
debugInfo.forEach(function (debugData) {
if (!debugData) { return; }
// @todo cache connection sessions and fetch reused session
// from the requester pool.
if (debugData.session && !requestSessions[debugData.session.id]) {
requestSessions[debugData.session.id] = debugData.session.data;
}
executionData.push({
request: debugData.request,
response: debugData.response,
timings: debugData.timings && {
// runtime start time
start: startTime,
// request start time
requestStart: debugData.timingStart,
// offsets calculated are relative to runtime start time
offset: calcTimingsOffset(startTimer, debugData.timingStartTimer, debugData.timings)
},
session: debugData.session && {
id: debugData.session.id,
// is connection socket reused
reused: debugData.session.reused
}
});
});
// update history object
history.execution.data = executionData;
history.execution.sessions = requestSessions;
return history;
},
/**
* Add the missing/system headers in the request object
*
* @param {Object[]} headers
*/
addMissingRequestHeaders = function (headers) {
_.forEach(headers, function (header) {
var lowerCasedKey = header.key.toLowerCase();
// update headers which gets overwritten by the requester
if (OVERWRITTEN_HEADERS[lowerCasedKey]) {
if (Array.isArray(_.get(request.headers, ['reference', lowerCasedKey]))) {
request.headers.remove(header.key);
}
request.headers.upsert({
key: header.key,
value: header.value,
system: true
});
}
});
},
/**
* Helper function to trigger `callback` and complete the request function
*
* @param {Error} error - error while requesting
* @param {Response} response - SDK Response instance
* @param {Object} history - Request-Response History
*/
onEnd = function (error, response, history) {
self.emit(RESPONSE_END_EVENT_BASE + id, error, self.trace.cursor,
self.trace, response, request, cookies, history);
return callback(error, response, request, cookies, history);
},
/**
* Helper function to keep track of `responseStart` and `response`
* triggers to make they are emitted in correct order.
*
* @todo fix requester control flow to remove this hack!
* this is required because CookieJar.getCookies is async method
* and by that time postman-request ends the request, which affects
* request post-send helpers because `response.start` event is not
* emitted on time and shared variables `cookies`, `responseJSON`,
* and, `responseHeaders` are initialized in onStart function.
*
* @param {String} trigger - trigger name
* @param {Response} response - SDK Response instance
* @param {Object} history - Request-Response History
*/
onComplete = function (trigger, response, history) {
if (trigger === RESPONSE_START) {
// set flag for responseStart callback
_responseStarted = true;
// if response is ended, end the response using cached data
if (_responseEnded) {
onEnd(null, _responseData.response, _responseData.history);
}
// bail out and wait for response end if not ended already
return;
}
// if response started, don't wait and end the response
if (_responseStarted) {
onEnd(null, response, history);
return;
}
// wait for responseStart and cache response callback data
_responseEnded = true;
_responseData = {
response: response,
history: history
};
},
/**
* Helper function to trigger `responseStart` callback and
* - transform postman-request response instance to SDK Response
* - filter cookies
* - filter response headers
* - add missing request headers
*
* @param {Object} response - Postman-Request response instance
*/
onStart = function (response) {
var responseStartEventName = RESPONSE_START_EVENT_BASE + id,
executionData,
initialRequest,
finalRequest,
sdkResponse,
history,
done = function () {
// emit the response.start event which eventually
// triggers responseStart callback
self.emit(responseStartEventName, null, sdkResponse, request, cookies, history);
// trigger completion of responseStart
onComplete(RESPONSE_START);
};
// @todo get rid of jsonifyResponse
responseJSON = core.jsonifyResponse(response, requestOptions);
// transform response headers to SDK compatible HeaderList
responseHeaders = _.transform(responseJSON.headers, transformMultiValueHeaders, []);
// initialize SDK Response instance
sdkResponse = new sdk.Response({
status: response && response.statusMessage,
code: responseJSON.statusCode,
header: responseHeaders
});
// prepare history from request debug data
history = getExecutionHistory(_.get(response, 'request._debug'));
// get the initial and final (on redirect) request from history
executionData = _.get(history, 'execution.data') || [];
initialRequest = _.get(executionData, '[0].request') || {};
finalRequest = executionData.length > 1 ?
// get final redirect
_.get(executionData, [executionData.length - 1, 'request']) :
// no redirects
initialRequest;
// add missing request headers so that they get bubbled up into the UI
addMissingRequestHeaders(initialRequest.headers);
// pull out cookies from the cookie jar, and make them chrome compatible.
if (cookieJar && _.isFunction(cookieJar.getCookies)) {
// get cookies set for the final request URL
cookieJar.getCookies(finalRequest.href, function (err, cookiesFromJar) {
if (err) {
return done();
}
cookies = _.transform(cookiesFromJar, function (acc, cookie) {
acc.push(toPostmanCookie(cookie));
}, []);
cookies = new sdk.CookieList(null, cookies);
done();
});
}
else {
cookies = new sdk.CookieList(null, []);
done();
}
};
// at this point the request could have come from collection, auth or sandbox
// we can't trust the integrity of this request
// bail out if request url is empty
if (!(request && request.url && request.url.toString && request.url.toString())) {
return onEnd(new Error('runtime:extensions~request: request url is empty'));
}
cookieJar = self.options.cookieJar;
requestOptions = core.getRequestOptions(request, self.options, protocolProfileBehavior);
// update url with the final encoded url
// @note this mutates the request object which will be passed in request
// and response callbacks
request.url.update(requestOptions.url.href);
hostname = request.url.getHost();
// check if host is on the `restrictedAddresses`
if (networkOptions.restrictedAddresses && core.isAddressRestricted(hostname, networkOptions)) {
return onEnd(new Error(ERROR_RESTRICTED_ADDRESS + hostname));
}
return requests(request, requestOptions, onStart, function (err, res, resBody, debug) {
// prepare history from request debug data
var history = getExecutionHistory(debug),
responseTime,
response;
if (err) {
// bubble up http errors
// @todo - Should we send an empty sdk Response here?
//
// Sending `history` object even in case of error
return onEnd(err, undefined, history);
}
// Calculate the time taken for us to get the response.
responseTime = Date.now() - startTime;
if (res && res.timings) {
// update response time to actual response end time
// of the final request in the redirect chain.
responseTime = Math.ceil(res.timings.end);
}
if (resBody && resBody instanceof ArrayBuffer) {
resBody = Buffer.from(resBody);
}
// Response in the SDK format
// @todo reuse same response instance used for responseStart callback
response = new sdk.Response({
code: responseJSON.statusCode,
status: res && res.statusMessage,
header: responseHeaders,
stream: resBody,
responseTime: responseTime
});
onComplete(RESPONSE_END, response, history);
});
},
/**
* Removes all current event listeners on the requester, and makes it ready for garbage collection :).
*
* @param {Function=} cb - Optional callback to be called on disposal
*
* @todo - In the future, when the requester manages its own connections etc, close them all here.
*/
dispose: function (cb) {
// This is safe for us, because we do not use wait on events. (i.e, no part of Runtime ever waits on
// any event to occur). We rely on callbacks for that, only choosing to use events as a way of streaming
// information outside runtime.
this.removeAllListeners();
_.isFunction(cb) && cb();
}
});
_.assign(Requester, /** @lends Requester */ {
/**
* Asynchronously create a new requester.
*
* @param trace
* @param trace.type - type of requester to return (for now, just http)
* @param trace.source - information about who needs this requester, e.g Auth, etc.
* @param trace.cursor - the cursor
* @param options
* @param callback
* @returns {*}
*/
create: function (trace, options, callback) {
return callback(null, new Requester(trace, options));
},
/**
* A helper method to dry run the given request instance.
* It returns the cloned request instance with the system added properties.
*
* @param {Request} request
* @param {Object} options
* @param {Object} options.cookieJar
* @param {Object} options.protocolProfileBehavior
* @param {Object} options.implicitCacheControl
* @param {Object} options.implicitTraceHeader
* @param {Function} done
*/
dryRun
});
module.exports.Requester = Requester;