UNPKG

postman-runtime

Version:

Underlying library of executing Postman Collections

524 lines (453 loc) 20.1 kB
var { Url, UrlMatchPatternList, VariableList } = require('postman-collection'), VariableScope = require('postman-collection').VariableScope, sdk = require('postman-collection'), _ = require('lodash'), /** * @const * @type {string} */ FUNCTION = 'function', /** * @const * @type {string} */ STRING = 'string', createReadStream, // function extractSNR, // function prepareLookupHash; // function /** * Create readable stream for given file as well as detect possible file * read issues. * * @param {Object} resolver - External file resolver module * @param {String} fileSrc - File path * @param {Function} callback - Final callback * * @note This function is defined in the file's root because there is a need to * trap it within closure in order to append the stream clone functionalities. * This ensures lesser footprint in case we have a memory leak. */ createReadStream = function (resolver, fileSrc, callback) { var readStream; // check for the existence of the file before creating read stream. // eslint-disable-next-line security/detect-non-literal-fs-filename resolver.stat(fileSrc, function (err, stats) { if (err) { // overwrite `ENOENT: no such file or directory` error message. Most likely the case. err.code === 'ENOENT' && (err.message = `"${fileSrc}", no such file`); return callback(err); } // check for a valid file. if (stats && typeof stats.isFile === FUNCTION && !stats.isFile()) { return callback(new Error(`"${fileSrc}", is not a file`)); } // check read permissions for user. // octal `400` signifies 'user permissions'. [4 0 0] -> [u g o] // `4` signifies 'read permission'. [4] -> [1 0 0] -> [r w x] if (stats && !(stats.mode & 0o400)) { return callback(new Error(`"${fileSrc}", read permission denied`)); } // @note Handle all the errors before `createReadStream` to avoid listening on stream error event. // listening on error requires listening on end event as well. which will make this sync. // @note In form-data mode stream error will be handled in postman-request but bails out ongoing request. // eslint-disable-next-line security/detect-non-literal-fs-filename readStream = resolver.createReadStream(fileSrc); // We might have to read the file before making the actual request // e.g, while calculating body hash during AWS auth or redirecting form-data params // So, this method wraps the `createReadStream` function with fixed arguments. // This makes sure that we don't have to pass `fileResolver` to // internal modules (like auth plugins) for security reasons. readStream.cloneReadStream = function (callback) { // eslint-disable-next-line security/detect-non-literal-fs-filename return createReadStream(resolver, fileSrc, callback); }; callback(null, readStream); }); }; /** * Extract set next request from the execution. * * @function getIterationData * @param {Array} executions - The prerequests or the tests of an item's execution. * @param {Object} previous - If extracting the tests request then prerequest's snr. * @return {Any} - The Set Next Request */ extractSNR = function (executions, previous) { var snr = previous || {}; _.isArray(executions) && executions.forEach(function (execution) { _.has(_.get(execution, 'result.return'), 'nextRequest') && ( (snr.defined = true), (snr.value = execution.result.return.nextRequest) ); }); return snr; }; /** * Returns a hash of IDs and Names of items in an array * * @param {Array} items - * @returns {Object} */ prepareLookupHash = function (items) { var hash = { ids: {}, names: {}, obj: {} }; _.forEach(items, function (item, index) { if (item) { item.id && (hash.ids[item.id] = index); item.name && (hash.names[item.name] = index); } }); return hash; }; /** * Resolve URL string from an item and payload by substituting all variables (including vault). * Returns intermediate products needed by callers that do further resolution. * * @private * @param {Item} item - The request item * @param {Object} payload - Payload containing variable scopes * @returns {{ urlString: string, variableDefinitions: Array, vaultVariables: * }} */ function resolveUrl (item, payload) { if (!(item && item.request && item.request.url)) { return { urlString: '', variableDefinitions: [], vaultVariables: null }; } // @todo - resolve variables in a more graceful way var variableDefinitions = [ // extract the variable list from variable scopes // @note: this is the order of precedence for variable resolution - don't change it _.get(payload, '_variables.values', []), _.get(payload, 'data', []), _.get(payload, 'environment.values', []), _.get(payload, 'collectionVariables.values', []), _.get(payload, 'globals.values', []) // @note vault variables are added later ], vaultValues = _.get(payload, 'vaultSecrets.values'), hasVaultSecrets = vaultValues ? vaultValues.count() > 0 : false, urlObj = item.request.url, urlString = urlObj.toString(), unresolvedUrlString = urlString, vaultVariables = null, vaultUrl; if (hasVaultSecrets) { // get the vault variables that match the unresolved URL string vaultUrl = urlObj.protocol ? urlString : 'http://' + urlString; // force protocol vaultVariables = payload.vaultSecrets.__getMatchingVariables(vaultUrl); // resolve variables in URL string with initial vault variables urlString = sdk.Property.replaceSubstitutions(urlString, [...variableDefinitions, vaultVariables]); if (urlString !== unresolvedUrlString) { // get the final list of vault variables that match the resolved URL string vaultUrl = new sdk.Url(urlString).toString(true); vaultVariables = payload.vaultSecrets.__getMatchingVariables(vaultUrl); // resolve vault variables in URL string // @note other variable scopes are skipped as they are already resolved urlString = sdk.Property.replaceSubstitutions(urlString, [vaultVariables]); } } else if (urlString) { urlString = sdk.Property.replaceSubstitutions(urlString, variableDefinitions); } return { urlString, variableDefinitions, vaultVariables }; } /** * Utility functions that are required to be re-used throughout the runner * * @module Runner~util * @private * * @note Do not put module logic or business logic related functions here. * The functions here are purely decoupled and low-level functions. */ module.exports = { /** * This function allows one to call another function by wrapping it within a try-catch block. * The first parameter is the function itself, followed by the scope in which this function is to be executed. * The third parameter onwards are blindly forwarded to the function being called * * @param {Function} fn - * @param {*} ctx - * * @returns {Error} If there was an error executing the function, the error is returned. * Note that if the function called here is asynchronous, it's errors will not be returned (for obvious reasons!) */ safeCall (fn, ctx) { // extract the arguments that are to be forwarded to the function to be called var args = Array.prototype.slice.call(arguments, 2); try { (typeof fn === FUNCTION) && fn.apply(ctx || global, args); } catch (err) { return err; } }, /** * Copies attributes from source object to destination object. * * @param {Object} dest - * @param {Object} src - * * @return {Object} */ syncObject (dest, src) { var prop; // update or add values from src for (prop in src) { if (Object.hasOwn(src, prop)) { dest[prop] = src[prop]; } } // remove values that no longer exist for (prop in dest) { if (Object.hasOwn(dest, prop) && !Object.hasOwn(src, prop)) { delete dest[prop]; } } return dest; }, /** * Create readable stream for given file as well as detect possible file * read issues. The resolver also attaches a clone function to the stream * so that the stream can be restarted any time. * * @param {Object} resolver - External file resolver module * @param {Function} resolver.stat - Resolver method to check for existence and permissions of file * @param {Function} resolver.createReadStream - Resolver method for creating read stream * @param {String} fileSrc - File path * @param {Function} callback - * */ createReadStream (resolver, fileSrc, callback) { // bail out if resolver not found. if (!resolver) { return callback(new Error('file resolver not supported')); } // bail out if resolver is not supported. if (typeof resolver.stat !== FUNCTION || typeof resolver.createReadStream !== FUNCTION) { return callback(new Error('file resolver interface mismatch')); } // bail out if file source is invalid or empty string. if (!fileSrc || typeof fileSrc !== STRING) { return callback(new Error('invalid or missing file source')); } // now that things are sanitized and validated, we transfer it to the // stream reading utility function that does the heavy lifting of // calling there resolver to return the stream return createReadStream(resolver, fileSrc, callback); }, /** * Mutates the given variable scope to be a vault variable scope by * converting the domains to UrlMatchPattern and adding a helper function * to get the matching variables for a given URL string. * * @note vault variables have a meta property called `domains` which is an * array of URL match pattern strings. * * @private * @param {PostmanVariableScope} scope - Postman variable scope instance */ prepareVaultVariableScope (scope) { // bail out if the given scope is already a vault variable scope if (scope.__vaultVariableScope) { return scope; } // traverse all the variables and convert the domains to UrlMatchPattern scope.values.each((variable) => { const domains = variable && variable._ && variable._.domains; if (!(Array.isArray(domains) && domains.length)) { return; } // mark the scope as having domain patterns scope.__hasDomainPatterns = true; // convert the domains to UrlMatchPattern variable._.domainPatterns = new UrlMatchPatternList(null, domains.map((domain) => { const url = new Url(domain); // @note URL path is ignored return `${url.protocol || 'https'}://${url.getRemote()}:*/*`; })); }); /** * Returns a list of variables that match the given URL string against * the domain patterns. * * @private * @param {String} urlString - URL string to match against * @returns {PostmanVariableList} */ scope.__getMatchingVariables = function (urlString) { // return all the variables if there are no domain patterns if (!scope.__hasDomainPatterns) { return scope.values; } const variables = scope.values.filter((variable) => { const domainPatterns = variable && variable._ && variable._.domainPatterns; if (!domainPatterns) { return true; } return domainPatterns.test(urlString); }); return new VariableList(null, variables.map((variable) => { return variable.toJSON(); // clone the variable })); }; // mark the scope as a vault variable scope scope.__vaultVariableScope = true; }, /** * ensure that the environment, globals and collectionVariables are in VariableScope instance format * @param {*} state application state object. */ prepareVariablesScope (state) { state.environment = VariableScope.isVariableScope(state.environment) ? state.environment : new VariableScope(state.environment); state.globals = VariableScope.isVariableScope(state.globals) ? state.globals : new VariableScope(state.globals); state.vaultSecrets = VariableScope.isVariableScope(state.vaultSecrets) ? state.vaultSecrets : new VariableScope(state.vaultSecrets); state.collectionVariables = VariableScope.isVariableScope(state.collectionVariables) ? state.collectionVariables : new VariableScope(state.collectionVariables); state._variables = VariableScope.isVariableScope(state.localVariables) ? state.localVariables : new VariableScope(state.localVariables); }, prepareLookupHash, extractSNR, /** * Returns the data for the given iteration * * @function getIterationData * @param {Array} data - The data array containing all iterations' data * @param {Number} iteration - The iteration to get data for * @return {Any} - The data for the iteration */ getIterationData (data, iteration) { // if iteration has a corresponding data element use that if (iteration < data.length) { return data[iteration]; } // otherwise use the last data element return data[data.length - 1]; }, /** * Processes SNR (Set Next Request) logic and coordinate handling for both waterfall and parallel execution. * This function extracts the common logic for handling execution results, SNR processing, and coordinate updates. * * @param {Object} options - Configuration options * @param {Object} options.coords - Current coordinates * @param {Object} options.executions - Execution results (prerequest and test) * @param {Error} options.executionError - Any execution error * @param {Object} options.runnerOptions - Runner options (disableSNR, stopOnFailure) * @param {Object} options.snrHash - SNR lookup hash * @param {Array} options.items - Collection items for SNR hash preparation * @returns {Object} Processing result with nextCoords, seekingToStart, stopRunNow flags */ processExecutionResult (options) { var { coords, executions, executionError, runnerOptions, snrHash, items } = options, snr = {}, nextCoords, seekingToStart, stopRunNow, stopOnFailure = runnerOptions.stopOnFailure; if (!executionError) { // extract set next request snr = extractSNR(executions.prerequest); snr = extractSNR(executions.test, snr); } if (!runnerOptions.disableSNR && snr.defined) { // prepare the snr lookup hash if it is not already provided !snrHash && (snrHash = prepareLookupHash(items)); // if it is null, we do not proceed further and move on // see if a request is found in the hash and then reset the coords position to the lookup // value. (snr.value !== null) && (snr.position = // eslint-disable-next-line no-nested-ternary snrHash[_.has(snrHash.ids, snr.value) ? 'ids' : (_.has(snrHash.names, snr.value) ? 'names' : 'obj')][snr.value]); snr.valid = _.isNumber(snr.position); } nextCoords = _.clone(coords); if (snr.valid) { // if the position was detected, we set the position to the one previous to the desired location // this ensures that the next call to .whatnext() will return the desired position. nextCoords.position = snr.position - 1; } else { // if snr was requested, but not valid, we stop this iteration. // stopping an iteration is equivalent to seeking the last position of the current // iteration, so that the next call to .whatnext() will automatically move to the next // iteration. (snr.defined || executionError) && (nextCoords.position = nextCoords.length - 1); // If we need to stop on a run, we set the stop flag to true. (stopOnFailure && executionError) && (stopRunNow = true); } // @todo - do this in unhacky way if (nextCoords.position === -1) { nextCoords.position = 0; seekingToStart = true; } return { nextCoords, seekingToStart, stopRunNow, snrHash }; }, /** * Resolve the URL string from an item by substituting all variables (including vault). * * @param {Item} item - The request item * @param {Object} payload - Payload containing variable scopes * @param {VariableScope} payload._variables - * @param {Object} payload.data - * @param {VariableScope} payload.environment - * @param {VariableScope} payload.collectionVariables - * @param {VariableScope} payload.globals - * @param {VariableScope} payload.vaultSecrets - * @returns {String} - The fully resolved URL string */ resolveUrlString (item, payload) { return resolveUrl(item, payload).urlString; }, /** * Resolve variables in item and auth in context. * * @param {ItemContext} context - * @param {Item} [context.item] - * @param {RequestAuth} [context.auth] - * @param {Object} payload - * @param {VariableScope} payload._variables - * @param {Object} payload.data - * @param {VariableScope} payload.environment - * @param {VariableScope} payload.collectionVariables - * @param {VariableScope} payload.globals - * @param {VariableScope} payload.vaultSecrets - */ resolveVariables (context, payload) { if (!(context.item && context.item.request)) { return; } var resolved = resolveUrl(context.item, payload), urlString = resolved.urlString, variableDefinitions = resolved.variableDefinitions, itemParent = context.item.parent(), item, auth; // add vault variables to the list of variable definitions if (resolved.vaultVariables) { variableDefinitions.push(resolved.vaultVariables); } // @todo - no need to sync variables when SDK starts supporting resolution from scope directly // @todo - avoid resolving the entire item as this unnecessarily resolves URL item = context.item = new sdk.Item(context.item.toObjectResolved(null, variableDefinitions, { ignoreOwnVariables: true })); // restore the parent reference item.setParent(itemParent); // re-parse and update the URL from the resolved string // @note URL string is used to resolve nested variables as URL parser doesn't support them well. urlString && (item.request.url = new sdk.Url(urlString)); auth = context.auth; // resolve variables in auth auth && (context.auth = new sdk.RequestAuth(auth.toObjectResolved(null, variableDefinitions, { ignoreOwnVariables: true }))); } };