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

397 lines
15 KiB
JavaScript

var _ = require('lodash'),
fs = require('fs'),
async = require('async'),
Collection = require('postman-collection').Collection,
VariableScope = require('postman-collection').VariableScope,
CookieJar = require('tough-cookie').CookieJar,
transformer = require('postman-collection-transformer'),
liquidJSON = require('liquid-json'),
parseCsv = require('csv-parse'),
util = require('../util'),
config = require('../config'),
/**
* The message displayed when the specified collection file can't be loaded.
*
* @const
* @type {String}
*/
COLLECTION_LOAD_ERROR_MESSAGE = 'collection could not be loaded',
/**
* The message displayed when the specified iteration data file can't be loaded.
*
* @const
* @type {String}
*/
ITERATION_DATA_LOAD_ERROR_MESSAGE = 'iteration data could not be loaded',
/**
* The message displayed when the specified environment or globals file can't be loaded.
*
* @const
* @type {String}
*/
LOAD_ERROR_MESSAGE = 'could not load ',
/**
* The set of postman collection transformer options, to convert collection v1 to collection v2.
*
* @const
* @type {Object}
*/
COLLECTION_TRANSFORMER_OPTION = { inputVersion: '1.0.0', outputVersion: '2.1.0' },
/**
* Accepts an object, and extracts the property inside an object which is supposed to contain the required data.
* In case of variables, it also extracts them into plain JS objects.
*
* @param {Object} source - The source wrapper object that may or may not contain inner wrapped properties.
* @param {String} type - "environment" or "globals", etc.
* @returns {Object} - The object representation of the current extracted property.
*/
extractModel = function (source, type) {
source = source[type] || source; // extract object that holds variable. these usually come from cloud API
if (!_.isObject(source)) {
return undefined;
}
// ensure we un-box the JSON if it comes from cloud-api or similar sources
!source.values && _.isObject(source[type]) && (source = source[type]);
// we ensure that environment passed as array is converted to plain object. runtime does this too, but we do it
// here for consistency of options passed to reporters
return source;
},
/**
* Loads the given data of type from a specified external location
*
* @param {String} type - The type of data to load.
* @param {String} location - The location to load from (file path or URL).
* @param {Object} options - The set of wrapped options.
* @param {function} cb - The callback function whose invocation marks the end of the external load routine.
* @returns {*}
*/
externalLoader = function (type, location, options, cb) {
return _.isString(location) ? util.fetchJson(type, location, options, function (err, data) {
if (err) {
return cb(err);
}
return cb(null, extractModel(data, type));
}) : cb(null, extractModel(location, type));
},
/**
* A helper method to process a collection and convert it to a V2 equivalent if necessary, and return it.
*
* @todo Drop support for the v1 collection format in Newman v5.
* Reference: https://github.com/postmanlabs/newman/pull/1660
*
* @param {Object} collection The input collection, specified as a JSON object.
* @param {Function} callback A handler function that consumes an error object and the processed collection.
* @returns {*}
*/
processCollection = function (collection, callback) {
if (util.isV1Collection(collection)) {
// @todo: route this via print module to respect silent flags
console.warn('newman: Newman v4 deprecates support for the v1 collection format');
console.warn(' Use the Postman Native app to export collections in the v2 format\n');
return transformer.convert(collection, COLLECTION_TRANSFORMER_OPTION, callback);
}
callback(null, collection);
},
/**
* Helper function that manages the load of environments and globals
*
* @private
* @param {String} type - The type of resource to load: collection, environment, etc.
* @param {String|Object} value - The value derived from the CLI or run command.
* @param {Object} options - The set of wrapped options.
* @param {Function} callback - The function invoked when the scope has been loaded.
*/
loadScopes = function (type, value, options, callback) {
var done = function (err, scope) {
if (err) { return callback(new Error(LOAD_ERROR_MESSAGE + `${type}\n ${err.message || err}`)); }
if (!_.isObject(scope)) {
return done(new Error(LOAD_ERROR_MESSAGE + type));
}
callback(null, new VariableScope(VariableScope.isVariableScope(scope) ? scope.toJSON() : scope));
};
if (_.isObject(value)) {
return done(null, value);
}
externalLoader(type, value, options, done);
},
/**
* Custom method to auto parse CSV values
*
* @private
* @param {String} value - CSV field value
* @param {Object} context - Context of field value
* @param {Boolean} context.quoting - A boolean indicating if the field was surrounded by quotes.
* @returns {String|Number|Date}
*/
csvAutoParse = function (value, context) {
if (context.quoting) {
// avoid parsing quoted values
return value;
}
if (util.isInt(value)) {
return parseInt(value, 10);
}
if (util.isFloat(value)) {
return parseFloat(value);
}
return value;
},
/**
* Custom configuration loaders for the required configuration keys.
*
* @type {Object}
*/
configLoaders = {
/**
* The collection file load helper for the current run.
*
* @param {Object|String} value - The collection, specified as a JSON object, or the path to it's file.
* @param {Object} options - The set of wrapped options.
* @param {Function} callback - The callback function invoked to mark the end of the collection load routine.
* @returns {*}
*/
collection: function (value, options, callback) {
/**
* The post collection load handler.
*
* @param {?Error} err - An Error instance / null, passed from the collection loader.
* @param {Object} collection - The collection / raw JSON object, passed from the collection loader.
* @returns {*}
*/
var done = function (err, collection) {
if (err) {
return callback(err);
}
// ensure that the collection option is present before starting a run
if (!_.isObject(collection)) {
return callback(new Error(COLLECTION_LOAD_ERROR_MESSAGE));
}
// ensure that the collection reference is an SDK instance
// @todo - should this be handled by config loaders?
collection = new Collection(Collection.isCollection(collection) ?
// if the option contain an instance of collection, we simply clone it for future use
// create a collection in case it is not one. user can send v2 JSON as a source and that will be
// converted to a collection
collection.toJSON() : collection);
callback(null, collection);
};
// if the collection has been specified as an object, convert to V2 if necessary and return the result
if (_.isObject(value)) {
return processCollection(value, done);
}
externalLoader('collection', value, options, function (err, data) {
if (err) {
return done(new Error(COLLECTION_LOAD_ERROR_MESSAGE +
(err.help ? `\n ${err.help}` : '') +
`\n ${err.message || err}`));
}
if (!_.isObject(data)) {
return done(new Error(COLLECTION_LOAD_ERROR_MESSAGE));
}
return processCollection(data, done);
});
},
/**
* The environment configuration object, loaded for the current collection run.
*
* @type {Object}
*/
environment: loadScopes.bind(this, 'environment'),
/**
* The object of globals, loaded for the collection run.
*
* @type {Object}
*/
globals: loadScopes.bind(this, 'globals'),
/**
* Helper function to sanitize folder option.
*
* @param {String[]|String} value - The list of folders to execute
* @param {Object} options - The set of wrapped options.
* @param {Function} callback - The callback function invoked to mark the end of the folder load routine.
* @returns {*}
*/
folder: function (value, options, callback) {
if (!value.length) {
return callback(); // avoids empty string or array
}
if (Array.isArray(value) && value.length === 1) {
return callback(null, value[0]); // avoids using multipleIdOrName strategy for a single item array
}
callback(null, value);
},
/**
* The iterationData loader module, with support for JSON or CSV data files.
*
* @param {String|Object[]} location - The path to the iteration data file for the current collection run, or
* the array of iteration data objects.
* @param {Object} options - The set of wrapped options.
* @param {Function} callback - The function invoked to indicate the end of the iteration data loading routine.
* @returns {*}
*/
iterationData: function (location, options, callback) {
if (_.isArray(location)) { return callback(null, location); }
util.fetch(location, function (err, data) {
if (err) {
return callback(new Error(ITERATION_DATA_LOAD_ERROR_MESSAGE + `\n ${err.message || err}`));
}
// Try loading as a JSON, fall-back to CSV.
async.waterfall([
(cb) => {
try {
return cb(null, liquidJSON.parse(data.trim()));
}
catch (e) {
return cb(null, undefined); // e masked to avoid displaying JSON parse errors for CSV files
}
},
(json, cb) => {
if (json) {
return cb(null, json);
}
// Wasn't JSON
parseCsv(data, {
columns: true, // infer the columns names from the first row
escape: '"', // escape character
cast: csvAutoParse, // function to cast values of individual fields
trim: true, // ignore whitespace immediately around the delimiter
relax: true, // allow using quotes without escaping inside unquoted string
relax_column_count: true, // ignore inconsistent columns count
bom: true // strip the byte order mark (BOM) from the input string
}, cb);
}
], (err, parsed) => {
if (err) {
return callback(new Error(ITERATION_DATA_LOAD_ERROR_MESSAGE + `\n ${err.message || err}`));
}
callback(null, parsed);
});
});
},
sslClientCertList: function (location, options, callback) {
if (Array.isArray(location)) {
return callback(null, location);
}
if (typeof location !== 'string') {
return callback(new Error('path for ssl client certificates list file must be a string'));
}
fs.readFile(location, function (err, value) {
if (err) {
return callback(new Error(`unable to read the ssl client certificates file "${location}"`));
}
try {
value = liquidJSON.parse(value.toString(util.detectEncoding(value)).trim());
}
catch (e) {
return callback(new Error(`the file at ${location} does not contain valid JSON data.`));
}
// ensure that `sslClientCertList` is an array
if (!Array.isArray(value)) {
return callback(new Error('expected ssl client certificates list to be an array.'));
}
return callback(null, value);
});
},
cookieJar: function (location, options, callback) {
if (_.isObject(location) && location.constructor.name === 'CookieJar') {
return callback(null, location);
}
if (typeof location !== 'string') {
return callback(new Error('cookieJar must be a path to a JSON file or a CookieJar instance'));
}
fs.readFile(location, function (err, value) {
if (err) {
return callback(new Error(`unable to read the cookie jar file "${location}"`));
}
try {
value = CookieJar.fromJSON(value.toString());
}
catch (e) {
return callback(new Error(`the file at ${location} does not contain valid JSON data.`));
}
return callback(null, value);
});
}
};
/**
* The helper function to load all file based information for the current collection run.
*
* @param {Object} options - The set of generic collection run options.
* @param {Function} callback - The function called to mark the completion of the configuration load routine.
* @returns {*}
*/
module.exports = function (options, callback) {
// set newman version used for collection run
options.newmanVersion = util.version;
// set working directory if not provided
options.workingDir = options.workingDir || process.cwd();
// allow insecure file read by default
options.insecureFileRead = Boolean(_.get(options, 'insecureFileRead', true));
config.get(options, { loaders: configLoaders, command: 'run' }, function (err, result) {
if (err) { return callback(err); }
!_.isEmpty(options.globalVar) && _.forEach(options.globalVar, function (variable) {
variable && (result.globals.set(variable.key, variable.value));
});
!_.isEmpty(options.envVar) && _.forEach(options.envVar, function (variable) {
variable && (result.environment.set(variable.key, variable.value));
});
callback(null, result);
});
};