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

278 lines
12 KiB
JavaScript

var _ = require('lodash'),
uuid = require('uuid'),
Response = require('postman-collection').Response,
visualizer = require('../../visualizer'),
/**
* List of request properties which can be mutated via pre-request
*
* @private
* @const
* @type {String[]}
*/
ALLOWED_REQUEST_MUTATIONS = ['url', 'method', 'headers', 'body'],
extractVisualizerData,
getResponseJSON;
/**
* Returns visualizer data from the latest execution result.
*
* @param {Array} prereqExecutions - pre-script executions results
* @param {Array} testExecutions - test-script executions results
* @returns {Object|undefined} - visualizer data
*/
extractVisualizerData = function (prereqExecutions, testExecutions) {
var visualizerData,
i;
if (_.isArray(testExecutions)) {
// loop through the test executions in reverse order to return data from latest execution
for (i = testExecutions.length - 1; i >= 0; i--) {
visualizerData = _.get(testExecutions[i], 'result.return.visualizer');
if (visualizerData) {
return visualizerData;
}
}
}
if (_.isArray(prereqExecutions)) {
// extract visualizer data from pre-request script results if it is not found earlier
for (i = prereqExecutions.length - 1; i >= 0; i--) {
visualizerData = _.get(prereqExecutions[i], 'result.return.visualizer');
if (visualizerData) {
return visualizerData;
}
}
}
};
/**
* Convert response into a JSON serializable object.
* The stream property is converted to base64 string for performance reasons.
*
* @param {Object} response - SDK Response instance
* @returns {Object}
*/
getResponseJSON = function (response) {
if (!Response.isResponse(response)) {
return;
}
return {
id: response.id,
code: response.code,
status: response.status,
header: response.headers && response.headers.toJSON(),
stream: response.stream && {
type: 'Base64',
data: response.stream.toString('base64')
},
responseTime: response.responseTime
};
};
/**
* Add options
* stopOnError:Boolean
* @type {Object}
*/
module.exports = {
init: function (done) {
// @todo - code item global timeout and delay here
done();
},
triggers: ['beforeItem', 'item', 'beforePrerequest', 'prerequest', 'beforeTest', 'test'],
process: {
/**
* @param {Function=} callback
* @param {Object} payload
* @param {Function} next
* @todo validate payload
*/
item: function (callback, payload, next) {
// adjust for polymorphic instructions
if (!next && _.isFunction(payload) && !_.isFunction(callback)) {
next = payload;
payload = callback;
callback = null;
}
var item = payload.item,
originalRequest = item.request.clone(),
coords = payload.coords,
data = _.isObject(payload.data) ? payload.data : {},
environment = _.isObject(payload.environment) ? payload.environment : {},
globals = _.isObject(payload.globals) ? payload.globals : {},
collectionVariables = _.isObject(payload.collectionVariables) ? payload.collectionVariables : {},
_variables = _.isObject(payload._variables) ? payload._variables : {},
stopOnError = _.has(payload, 'stopOnError') ? payload.stopOnError : this.options.stopOnError,
// @todo: this is mostly coded in event extension and we are
// still not sure whether that is the right place for it to be.
abortOnFailure = this.options.abortOnFailure,
stopOnFailure = this.options.stopOnFailure,
delay = _.get(this.options, 'delay.item'),
ctxTemplate;
// validate minimum parameters required for the command to work
if (!(item && coords)) {
return next(new Error('runtime: item execution is missing required parameters'));
}
// store a common uuid in the coords
coords.ref = uuid.v4();
// here we code to queue prerequest script, then make a request and then execute test script
this.triggers.beforeItem(null, coords, item);
this.queueDelay(function () {
// create the context object for scripts to run
ctxTemplate = {
collectionVariables: collectionVariables,
_variables: _variables,
globals: globals,
environment: environment,
data: data,
request: item.request
};
// @todo make it less nested by coding Instruction.thenQueue
this.queue('event', {
name: 'prerequest',
item: item,
coords: coords,
context: ctxTemplate,
trackContext: ['globals', 'environment', 'collectionVariables'],
stopOnScriptError: stopOnError,
stopOnFailure: stopOnFailure
}).done(function (prereqExecutions, prereqExecutionError) {
// if stop on error is marked and script executions had an error,
// do not proceed with more commands, instead we bail out
if ((stopOnError || stopOnFailure) && prereqExecutionError) {
this.triggers.item(null, coords, item); // @todo - should this trigger receive error?
return callback && callback.call(this, prereqExecutionError, {
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.
ALLOWED_REQUEST_MUTATIONS.forEach(function (property) {
if (_.has(ctxTemplate, ['request', property])) {
item.request[property] = ctxTemplate.request[property];
}
// update property's parent reference
if (item.request[property] && typeof item.request[property].setParent === 'function') {
item.request[property].setParent(item.request);
}
});
this.queue('request', {
item: item,
globals: ctxTemplate.globals,
environment: ctxTemplate.environment,
collectionVariables: ctxTemplate.collectionVariables,
_variables: ctxTemplate._variables,
data: ctxTemplate.data,
coords: coords,
source: 'collection'
}).done(function (result, requestError) {
!result && (result = {});
var request = result.request,
response = result.response,
cookies = result.cookies;
if ((stopOnError || stopOnFailure) && requestError) {
this.triggers.item(null, coords, item); // @todo - should this trigger receive error?
return callback && callback.call(this, requestError, {
request: request
});
}
// also the test object requires the updated request object (since auth helpers may modify it)
request && (ctxTemplate.request = request);
// @note convert response instance to plain object.
// we want to avoid calling Response.toJSON() which triggers toJSON on Response.stream buffer.
// Because that increases the size of stringified object by 3 times.
// Also, that increases the total number of tokens (buffer.data) whereas Buffer.toString
// generates a single string that is easier to stringify and sent over the UVM bridge.
response && (ctxTemplate.response = getResponseJSON(response));
// set cookies for this transaction
cookies && (ctxTemplate.cookies = cookies);
// the context template also has a test object to store assertions
ctxTemplate.tests = {}; // @todo remove
this.queue('event', {
name: 'test',
item: item,
coords: coords,
context: ctxTemplate,
trackContext: ['tests', 'globals', 'environment', 'collectionVariables'],
stopOnScriptError: stopOnError,
abortOnFailure: abortOnFailure,
stopOnFailure: stopOnFailure
}).done(function (testExecutions, testExecutionError) {
var visualizerData = extractVisualizerData(prereqExecutions, testExecutions),
visualizerResult;
if (visualizerData) {
visualizer.processTemplate(visualizerData.template,
visualizerData.data,
visualizerData.options,
function (err, processedTemplate) {
visualizerResult = {
// bubble up the errors while processing template through visualizer result
error: err,
// add processed template and data to visualizer result
processedTemplate: processedTemplate,
data: visualizerData.data
};
// trigger an event saying that item has been processed
this.triggers.item(null, coords, item, visualizerResult);
}.bind(this));
}
else {
// trigger an event saying that item has been processed
// @todo - should this trigger receive error?
this.triggers.item(null, coords, item, null);
}
// reset mutated request with original request instance
// @note request mutations are not persisted across iterations
item.request = originalRequest;
callback && callback.call(this, ((stopOnError || stopOnFailure) && testExecutionError) ?
testExecutionError : null, {
prerequest: prereqExecutions,
request: request,
response: response,
test: testExecutions
});
});
});
});
}.bind(this), {
time: delay,
source: 'item',
cursor: coords
}, next);
}
}
};