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

886 lines
32 KiB
JavaScript

/* eslint-disable object-shorthand */
var _ = require('lodash').noConflict(),
constants = require('../../constants'),
v1Common = require('../../common/v1'),
util = require('../../util'),
headersCommentPrefix = '//',
IS_SUBFOLDER = Symbol('_postman_isSubFolder'),
/**
* A constructor that is capable of being used for one-off conversions of requests, and folders.
*
* @param {Object} options - The set of options for builder construction.
* @class Builders
* @constructor
*/
Builders = function (options) {
this.options = options || {};
},
/**
* Parse formdata & urlencoded request data.
* - filter params missing property `key`
* - handle file type param value
* -cleanup `enabled` & `description`
*
* @param {Object[]} data - Data to parse.
* @param {?Boolean} [retainEmpty] - To retain empty values or not.
* @returns {Object[]} - Parsed data.
*/
parseFormData = function (data, retainEmpty) {
if (!(Array.isArray(data) && data.length)) { return []; }
var formdata = [],
i,
ii,
param;
for (i = 0, ii = data.length; i < ii; i++) {
// clone param to avoid mutating data.
// eslint-disable-next-line prefer-object-spread
param = Object.assign({}, data[i]);
// skip if param is missing property `key`,
// `key` is a required property, value can be null/undefined.
// because in a FormParam property lists, key is used for indexing.
if (_.isNil(param.key)) {
continue;
}
// for file `type`, set `value` to `src`
if (param.type === 'file' && !param.src && param.value) {
param.src = (_.isString(param.value) || _.isArray(param.value)) ? param.value : null;
delete param.value;
}
// `hasOwnProperty` check ensures that it don't delete undefined property: `enabled`
if (Object.prototype.hasOwnProperty.call(param, 'enabled')) {
// set `disabled` flag if `enabled` is false.
param.enabled === false && (param.disabled = true);
delete param.enabled; // cleanup
}
// Prevent empty descriptions from showing up in the converted results. This keeps collections clean.
util.cleanEmptyValue(param, 'description', retainEmpty);
formdata.push(param);
}
return formdata;
};
_.assign(Builders.prototype, {
/**
* Constructs a V2 compatible "info" object from a V1 Postman Collection
*
* @param {Object} collectionV1 - A v1 collection to derive collection metadata from.
* @returns {Object} - The resultant v2 info object.
*/
info: function (collectionV1) {
var info = {
_postman_id: collectionV1.id || util.uid(),
name: collectionV1.name
};
if (collectionV1.description) { info.description = collectionV1.description; }
else if (this.options.retainEmptyValues) { info.description = null; }
info.schema = constants.SCHEMA_V2_URL;
return info;
},
/**
* Facilitates sanitized variable transformations across all levels for v1 collection normalization.
*
* @param {Object} entity - The wrapper object containing variable definitions.
* @param {?Object} options - The set of options for the current variable transformation.
* @param {?Object} options.fallback - The set of fallback values to be applied when no variables exist.
* @param {?Boolean} options.noDefaults - When set to true, no defaults are applied.
* @param {?Boolean} options.retainIds - When set to true, ids are left as is.
* @returns {Object[]} - The set of sanitized variables.
*/
variable: function (entity, options) {
return util.handleVars(entity, options);
},
/**
* Constructs a V2 compatible URL object from a V1 request
*
* @param {Object} requestV1 - The source v1 request to extract the URL from.
* @returns {String|Object} - The resultant URL.
*/
url: function (requestV1) {
var queryParams = [],
pathVariables = [],
traversedVars = {},
queryParamAltered,
retainIds = this.options.retainIds,
parsed = util.urlparse(requestV1.url),
retainEmpty = this.options.retainEmptyValues;
// add query params
_.forEach(requestV1.queryParams, function (queryParam) {
(queryParam.enabled === false) && (queryParam.disabled = true);
delete queryParam.enabled;
util.cleanEmptyValue(queryParam, 'description', retainEmpty);
if (_.has(queryParam, 'equals')) {
if (queryParam.equals) {
(queryParam.value === null) && (queryParam.value = '');
}
else {
// = is not appended when the value is null. However,
// non empty value should be preserved
queryParam.value = queryParam.value || null;
}
queryParamAltered = true;
delete queryParam.equals;
}
queryParams.push(queryParam);
});
// only add query params from URL if not given explicitly
if (!_.size(queryParams)) {
// parsed query params are taken from the url, so no descriptions are available from them
queryParams = parsed.query;
}
// Merge path variables
_.forEach(requestV1.pathVariableData, function (pathVariable) {
pathVariable = _.clone(pathVariable);
util.cleanEmptyValue(pathVariable, 'description', retainEmpty);
if (!retainIds && pathVariable.id) {
delete pathVariable.id;
}
pathVariables.push(pathVariable);
traversedVars[pathVariable.key] = true;
});
// pathVariables in v1 are of the form {foo: bar}, so no descriptions can be obtained from them
_.forEach(requestV1.pathVariables, function (value, key) {
!traversedVars[key] && pathVariables.push({
value: value,
key: key
});
});
!_.isEmpty(queryParams) && (parsed.query = queryParams);
!_.isEmpty(pathVariables) && (parsed.variable = pathVariables);
// If the query params have been altered, update the raw stringified URL
queryParamAltered && (parsed.raw = util.urlunparse(parsed));
// return the objectified URL only if query param or path variable descriptions are present, string otherwise
return (parsed.query || parsed.variable) ? parsed : (parsed.raw || requestV1.url);
},
/**
* Extracts the HTTP Method from a V1 request
*
* @param {Object} requestV1 - The v1 request to extract the request method from.
* @returns {String} - The extracted request method.
*/
method: function (requestV1) {
return requestV1.method;
},
/**
* Constructs an array of Key-Values from a raw HTTP Header string.
*
* @param {Object} requestV1 - The v1 request to extract header information from.
* @returns {Object[]} - A list of header definition objects.
*/
header: function (requestV1) {
if (_.isArray(requestV1.headers)) {
return requestV1.headers;
}
var headers = [],
traversed = {},
headerData = requestV1.headerData || [],
retainEmpty = this.options.retainEmptyValues;
_.forEach(headerData, function (header) {
if (_.startsWith(header.key, headersCommentPrefix) || (header.enabled === false)) {
header.disabled = true;
header.key = header.key.replace(headersCommentPrefix, '').trim();
}
// prevent empty header descriptions from showing up in converted results. This keeps the collections clean
util.cleanEmptyValue(header, 'description', retainEmpty);
delete header.enabled;
headers.push(header); // @todo Improve this sequence to account for multi-valued headers
traversed[header.key] = true;
});
// requestV1.headers is a string, so no descriptions can be obtained from it
_.forEach(v1Common.parseHeaders(requestV1.headers), function (header) {
!traversed[header.key] && headers.push(header);
});
return headers;
},
/**
* Constructs a V2 Request compatible "body" object from a V1 Postman request
*
* @param {Object} requestV1 - The v1 request to extract the body from.
* @returns {{mode: *, content: (*|string)}}
*/
body: function (requestV1) {
var modes = {
binary: 'file',
graphql: 'graphql',
params: 'formdata',
raw: 'raw',
urlencoded: 'urlencoded'
},
data = {},
rawModeData,
graphqlModeData,
dataMode = modes[requestV1.dataMode],
retainEmpty = this.options.retainEmptyValues,
bodyOptions = {},
mode,
// flag indicating that all the props which holds request body data
// i.e, data (urlencoded, formdata), rawModeData (raw, file) and graphqlModeData (graphql)
// are empty.
emptyBody = _.isEmpty(requestV1.data) && _.isEmpty(requestV1.rawModeData) &&
_.isEmpty(requestV1.graphqlModeData);
// set body to null if:
// 1. emptyBody is true and dataMode is unset
// 2. dataMode is explicitly set to null
// @note the explicit null check is added to ensure that body is not set
// in case the dataMode was set to null by the app.
if ((!dataMode && emptyBody) || requestV1.dataMode === null) {
return retainEmpty ? null : undefined;
}
// set `rawModeData` if its a string
if (_.isString(requestV1.rawModeData)) {
rawModeData = requestV1.rawModeData;
}
// check if `rawModeData` is an array like: ['rawModeData']
else if (Array.isArray(requestV1.rawModeData) &&
requestV1.rawModeData.length === 1 &&
_.isString(requestV1.rawModeData[0])) {
rawModeData = requestV1.rawModeData[0];
}
// set graphqlModeData if its not empty
if (!_.isEmpty(requestV1.graphqlModeData)) {
graphqlModeData = requestV1.graphqlModeData;
}
// set data.mode.
// if dataMode is not set, infer from data or rawModeData or graphqlModeData
if (dataMode) {
data.mode = dataMode;
}
// at this point we are sure that the body is not empty so let's
// infer the data mode.
// @note its possible that multiple body types are set e.g, both
// rawModeData and graphqlModeData are set. So, the priority will be:
// raw -> formdata -> graphql (aligned with pre-graphql behaviour).
//
// set `formdata` if rawModeData is not set and data is an array
// `data` takes higher precedence over `rawModeData`.
else if (!rawModeData && Array.isArray(requestV1.data || requestV1.rawModeData)) {
data.mode = 'formdata';
}
// set `graphql` if graphqlModeData is set
else if (!rawModeData && graphqlModeData) {
data.mode = 'graphql';
}
// set `raw` mode as default
else {
data.mode = 'raw';
}
if (data.mode === 'raw') {
if (rawModeData) {
data[data.mode] = rawModeData;
}
else if (_.isString(requestV1.data)) {
data[data.mode] = requestV1.data;
}
else {
// empty string instead of retainEmpty check to have parity with other modes.
data[data.mode] = '';
}
}
else if (data.mode === 'graphql') {
data[data.mode] = graphqlModeData;
}
else if (data.mode === 'file') {
// rawModeData can be string or undefined.
data[data.mode] = { src: rawModeData };
}
else {
// parse data for formdata or urlencoded data modes.
// `rawModeData` is checked in case its of type `data`.
data[data.mode] = parseFormData(requestV1.data || requestV1.rawModeData, retainEmpty);
}
if (requestV1.dataOptions) {
// Convert v1 mode to v2 mode
for (mode in modes) {
if (!_.isEmpty(requestV1.dataOptions[mode])) {
bodyOptions[modes[mode]] = requestV1.dataOptions[mode];
}
}
!_.isEmpty(bodyOptions) && (data.options = bodyOptions);
}
if (requestV1.dataDisabled) { data.disabled = true; }
else if (retainEmpty) { data.disabled = false; }
return data;
},
/**
* Constructs a V2 "events" object from a V1 Postman Request
*
* @param {Object} entityV1 - The v1 entity to extract script information from.
* @returns {Object[]|*}
*/
event: function (entityV1) {
if (!entityV1) { return; }
const retainIds = this.options.retainIds;
// if prioritizeV2 is true, events is used as the source of truth
if ((util.notLegacy(entityV1, 'event') || this.options.prioritizeV2) && !_.isEmpty(entityV1.events)) {
// in v1, `events` is regarded as the source of truth if it exists, so handle that first and bail out.
// @todo: Improve this to order prerequest events before test events
_.forEach(entityV1.events, function (event) {
!event.listen && (event.listen = 'test');
// just delete the event.id if retainIds is not set
if (!retainIds && event.id) {
delete event.id;
}
if (event.script) {
// just delete the script.id if retainIds is not set
if (!retainIds && event.script.id) {
delete event.script.id;
}
!event.script.type && (event.script.type = 'text/javascript');
// @todo: Add support for src
_.isString(event.script.exec) && (event.script.exec = event.script.exec.split('\n'));
}
});
return entityV1.events;
}
var events = [];
// @todo: Extract both flows below into a common method
if (entityV1.tests) {
events.push({
listen: 'test',
script: {
type: 'text/javascript',
exec: _.isString(entityV1.tests) ?
entityV1.tests.split('\n') :
entityV1.tests
}
});
}
if (entityV1.preRequestScript) {
events.push({
listen: 'prerequest',
script: {
type: 'text/javascript',
exec: _.isString(entityV1.preRequestScript) ?
entityV1.preRequestScript.split('\n') :
entityV1.preRequestScript
}
});
}
return events.length ? events : undefined;
},
/**
* A number of auth parameter names have changed from V1 to V2. This function calls the appropriate
* mapper function, and creates the V2 auth parameter object.
*
* @param {Object} entityV1 - The v1 entity to derive auth information from.
* @param {?Object} options - The set of options for the current auth cleansing operation.
* @param {?Boolean} [options.includeNoauth=false] - When set to true, noauth is set to null.
* @returns {{type: *}}
*/
auth: function (entityV1, options) {
if (!entityV1) { return; }
if ((util.notLegacy(entityV1, 'auth') || this.options.prioritizeV2) && entityV1.auth) {
return util.authArrayToMap(entityV1, options);
}
if (!entityV1.currentHelper || (entityV1.currentHelper === null) || (entityV1.currentHelper === 'normal')) {
return;
}
var params,
type = v1Common.authMap[entityV1.currentHelper] || entityV1.currentHelper,
auth = {
type: type
};
// Some legacy versions of the App export Helper Attributes as a string.
if (_.isString(entityV1.helperAttributes)) {
try {
entityV1.helperAttributes = JSON.parse(entityV1.helperAttributes);
}
catch (e) {
return;
}
}
if (entityV1.helperAttributes && util.authMappersFromLegacy[entityV1.currentHelper]) {
params = util.authMappersFromLegacy[entityV1.currentHelper](entityV1.helperAttributes);
}
params && (auth[type] = params);
return auth;
},
/**
* Creates a V2 format request from a V1 Postman Collection Request
*
* @param {Object} requestV1 - The v1 request to be transformed.
* @returns {Object} - The converted v2 request.
*/
request: function (requestV1) {
var self = this,
request = {},
retainEmpty = self.options.retainEmptyValues,
units = ['auth', 'method', 'header', 'body', 'url'];
units.forEach(function (unit) {
request[unit] = self[unit](requestV1);
});
if (requestV1.description) { request.description = requestV1.description; }
else if (retainEmpty) { request.description = null; }
return request;
},
/**
* Converts a V1 cookie to a V2 cookie.
*
* @param {Object} cookieV1 - The v1 cookie object to convert.
* @returns {{expires: string, hostOnly: *, httpOnly: *, domain: *, path: *, secure: *, session: *, value: *}}
*/
cookie: function (cookieV1) {
return {
expires: (new Date(cookieV1.expirationDate * 1000)).toString(),
hostOnly: cookieV1.hostOnly,
httpOnly: cookieV1.httpOnly,
domain: cookieV1.domain,
path: cookieV1.path,
secure: cookieV1.secure,
session: cookieV1.session,
value: cookieV1.value,
key: cookieV1.name
};
},
/**
* Gets the saved request for the given response, and handles edge cases between Apps & Sync
*
* Handles a lot of edge cases, so the code is not very clean.
*
* The Flow followed here is:
*
* If responseV1.requestObject is present
* If it is a string
* Try parsing it as JSON
* If parsed,
* return it
* else
* It is a request ID
* If responseV1.request is present
* If it is a string
* Try parsing it as JSON
* If parsed,
* return it
* else
* It is a request ID
* Look up the collection for the request ID and return it, or return undefined.
*
* @param {Object} responseV1 - The v1 response to be converted.
* @returns {Object} - The converted saved request, in v2 format.
*/
savedRequest: function (responseV1) {
var self = this,
associatedRequestId;
if (responseV1.requestObject) {
if (_.isString(responseV1.requestObject)) {
try {
return JSON.parse(responseV1.requestObject);
}
catch (e) {
// if there was an error parsing it as JSON, it's probably an ID, so store it in the ID variable
associatedRequestId = responseV1.requestObject;
}
}
else {
return responseV1.requestObject;
}
}
if (responseV1.request) {
if (_.isString(responseV1.request)) {
try {
return JSON.parse(responseV1.request);
}
catch (e) {
// if there was an error parsing it as JSON, it's probably an ID, so store it in the ID variable
associatedRequestId = responseV1.request;
}
}
else {
return responseV1.request;
}
}
// we have a request ID
return associatedRequestId && _.get(self, ['cache', associatedRequestId]);
},
/**
* Since a V2 response contains the entire associated request that was sent, creating the response means it
* also must use the V1 request.
*
* @param {Object} responseV1 - The response object to convert from v1 to v2.
* @returns {Object} - The v2 response object.
*/
singleResponse: function (responseV1) {
var response = {},
self = this,
originalRequest;
originalRequest = self.savedRequest(responseV1);
// add ids to the v2 result only if both: the id and retainIds are truthy.
// this prevents successive exports to v2 from being overwhelmed by id diffs
self.options.retainIds && (response.id = responseV1.id || util.uid());
response.name = responseV1.name || 'response';
response.originalRequest = originalRequest ? self.request(originalRequest) : undefined;
response.status = responseV1.responseCode && responseV1.responseCode.name || undefined;
response.code = responseV1.responseCode && Number(responseV1.responseCode.code) || undefined;
response._postman_previewlanguage = responseV1.language;
response._postman_previewtype = responseV1.previewType;
response.header = responseV1.headers;
response.cookie = _.map(responseV1.cookies, function (cookie) {
return self.cookie(cookie);
});
response.responseTime = responseV1.time;
response.body = responseV1.text;
return response;
},
/**
* Constructs an array of "sample" responses (compatible with a V2 collection)
* from a Postman Collection V1 Request.
*
* If response ordering via `responses_order` field is present,
* ensure the ordering is respected while constructing responses array.
*
* @param {Object} requestV1 - The v1 request object to extract response information from.
* @returns {Object[]} - The list of v2 response definitions.
*/
response: function (requestV1) {
var self = this,
responses = _.get(requestV1, 'responses', []),
responsesCache = _.keyBy(responses, 'id'),
responses_order = _.get(requestV1, 'responses_order', []);
// If ordering of responses is not available
// Create a default ordering using the `responses` field
if (!(responses_order && responses_order.length)) {
responses_order = _.map(responses, 'id');
}
// Filter out any response id that is not available
responses_order = _.filter(responses_order, function (responseId) {
return _.has(responsesCache, responseId);
});
return _.map(responses_order, function (responseId) {
return self.singleResponse(responsesCache[responseId]);
});
},
/**
* Creates a V2 compatible ``item`` from a V1 Postman Collection Request
*
* @param {Object} requestV1 - Postman collection V1 request.
* @returns {Object} - The converted request object, in v2 format.
*/
singleItem: function (requestV1) {
if (!requestV1) { return; }
var self = this,
retainIds = self.options.retainIds,
units = ['request', 'response'],
variable = self.variable(requestV1, { retainIds: retainIds }),
item = {
name: requestV1.name || '', // Inline building to avoid additional function call
event: self.event(requestV1)
};
retainIds && (item.id = requestV1.id || util.uid());
// add protocolProfileBehavior property from requestV1 to the item
util.addProtocolProfileBehavior(requestV1, item);
units.forEach(function (unit) {
item[unit] = self[unit](requestV1);
});
variable && variable.length && (item.variable = variable);
return item;
},
/**
* Constructs an array of Items & ItemGroups compatible with the V2 format.
*
* @param {Object} collectionV1 - The v1 collection to derive folder information from.
* @returns {Object[]} - The list of item group definitions.
*/
itemGroups: function (collectionV1) {
var self = this,
items = [],
itemGroupCache = {},
retainEmpty = self.options.retainEmptyValues;
// Read all folder data, and prep it so that we can throw subfolders in the right places
itemGroupCache = _.reduce(collectionV1.folders, function (accumulator, folder) {
if (!folder) { return accumulator; }
var retainIds = self.options.retainIds,
auth = self.auth(folder),
event = self.event(folder),
variable = self.variable(folder, { retainIds: retainIds }),
result = {
name: folder.name,
item: []
};
retainIds && (result.id = folder.id || util.uid());
if (folder.description) { result.description = folder.description; }
else if (retainEmpty) { result.description = null; }
(auth || (auth === null)) && (result.auth = auth);
event && (result.event = event);
variable && variable.length && (result.variable = variable);
util.addProtocolProfileBehavior(folder, result);
accumulator[folder.id] = result;
return accumulator;
}, {});
// Populate each ItemGroup with subfolders
_.forEach(collectionV1.folders, function (folderV1) {
if (!folderV1) { return; }
var itemGroup = itemGroupCache[folderV1.id],
hasSubfolders = folderV1.folders_order && folderV1.folders_order.length,
hasRequests = folderV1.order && folderV1.order.length;
// Add subfolders
hasSubfolders && _.forEach(folderV1.folders_order, function (subFolderId) {
if (!itemGroupCache[subFolderId]) {
// todo: figure out what to do when a collection contains a subfolder ID,
// but the subfolder is not actually there.
return;
}
itemGroupCache[subFolderId][IS_SUBFOLDER] = true;
itemGroup.item.push(itemGroupCache[subFolderId]);
});
// Add items
hasRequests && _.forEach(folderV1.order, function (requestId) {
if (!self.cache[requestId]) {
// todo: what do we do here??
return;
}
itemGroup.item.push(self.singleItem(self.cache[requestId]));
});
});
// This compromises some self-healing, which was originally present, but the performance cost of
// doing self-healing the right way is high, so we directly rely on collectionV1.folders_order
// The self-healing way would be to iterate over itemGroupCache directly, but preserving the right order
// becomes a pain in that case.
_.forEach(_.uniq(collectionV1.folders_order || _.map(collectionV1.folders, 'id')), function (folderId) {
var itemGroup = itemGroupCache[folderId];
itemGroup && !_.get(itemGroup, IS_SUBFOLDER) && items.push(itemGroup);
});
// This is useful later
self.itemGroupCache = itemGroupCache;
return _.compact(items);
},
/**
* Creates a V2 compatible array of items from a V1 Postman Collection
*
* @param {Object} collectionV1 - A Postman Collection object in the V1 format.
* @returns {Object[]} - The list of item groups (folders) in v2 format.
*/
item: function (collectionV1) {
var self = this,
requestsCache = _.keyBy(collectionV1.requests, 'id'),
allRequests = _.map(collectionV1.requests, 'id'),
result;
self.cache = requestsCache;
result = self.itemGroups(collectionV1);
_.forEach(_.intersection(collectionV1.order, allRequests), function (requestId) {
var request = self.singleItem(requestsCache[requestId]);
request && (result.push(request));
});
return result;
}
});
module.exports = {
input: '1.0.0',
output: '2.0.0',
Builders: Builders,
/**
* Converts a single V1 request to a V2 item.
*
* @param {Object} request - The v1 request to convert to v2.
* @param {Object} options - The set of options for v1 -> v2 conversion.
* @param {Function} callback - The function to be invoked when the conversion has completed.
*/
convertSingle: function (request, options, callback) {
var builders = new Builders(options),
converted,
err;
try {
converted = builders.singleItem(_.cloneDeep(request));
}
catch (e) {
err = e;
}
if (callback) {
return callback(err, converted);
}
if (err) {
throw err;
}
return converted;
},
/**
* Converts a single V1 Response to a V2 Response.
*
* @param {Object} response - The v1 response to convert to v2.
* @param {Object} options - The set of options for v1 -> v2 conversion.
* @param {Function} callback - The function to be invoked when the conversion has completed.
*/
convertResponse: function (response, options, callback) {
var builders = new Builders(options),
converted,
err;
try {
converted = builders.singleResponse(_.cloneDeep(response));
}
catch (e) {
err = e;
}
if (callback) {
return callback(err, converted);
}
if (err) {
throw err;
}
return converted;
},
/**
* Converts a V1 collection to a V2 collection (performs ID replacement, etc as necessary).
*
* @param {Object} collection - The v1 collection to convert to v2.
* @param {Object} options - The set of options for v1 -> v2 conversion.
* @param {Function} callback - The function to be invoked when the conversion has completed.
*/
convert: function (collection, options, callback) {
collection = _.cloneDeep(collection);
var auth,
event,
variable,
newCollection = {},
units = ['info', 'item'],
builders = new Builders(options),
authOptions = { excludeNoauth: true },
varOpts = options && { fallback: options.env, retainIds: options.retainIds };
try {
units.forEach(function (unit) {
newCollection[unit] = builders[unit](collection);
});
(auth = builders.auth(collection, authOptions)) && (newCollection.auth = auth);
(event = builders.event(collection)) && (newCollection.event = event);
(variable = builders.variable(collection, varOpts)) && (newCollection.variable = variable);
util.addProtocolProfileBehavior(collection, newCollection);
}
catch (e) {
if (callback) {
return callback(e, null);
}
throw e;
}
if (callback) { return callback(null, newCollection); }
return newCollection;
}
};