UNPKG

postman-sandbox

Version:

Sandbox for Postman Scripts to run in Node.js or browser

526 lines (442 loc) 17 kB
/* eslint-disable max-classes-per-file */ const _ = require('lodash'), scopeLibraries = { JSON: require('liquid-json'), _: require('lodash3').noConflict(), CryptoJS: require('crypto-js'), tv4: require('tv4'), xml2Json: require('./xml2Json'), Backbone: require('backbone'), cheerio: require('cheerio') }, LEGACY_GLOBS = [ 'tests', 'globals', 'environment', 'data', 'request', 'responseCookies', 'responseHeaders', 'responseTime', 'responseCode', 'responseBody', 'iteration', 'postman', // scope libraries '_', 'CryptoJS', 'tv4', 'xml2Json', 'Backbone', 'cheerio' // 'JSON', // removing JSON from the list since it is a standard JS object ], LEGACY_GLOBS_ALTERNATIVES = { tests: '"pm.test()"', globals: '"pm.globals"', environment: '"pm.environment"', data: '"pm.iterationData"', request: '"pm.request"', responseCookies: '"pm.cookies"', responseHeaders: '"pm.response.headers"', responseTime: '"pm.response.responseTime"', responseCode: '"pm.response.code"', responseBody: '"pm.response.text()"', iteration: '"pm.info.iteration"', _: '"require(\'lodash\')"', CryptoJS: 'global "crypto" object', tv4: '"require(\'ajv\')"', xml2Json: '"require(\'xml2js\')"', cheerio: '"require(\'cheerio\')"', 'postman.setNextRequest': '"pm.execution.setNextRequest()"', 'postman.setEnvironmentVariable': '"pm.environment.set()"', 'postman.getEnvironmentVariable': '"pm.environment.get()"', 'postman.clearEnvironmentVariables': '"pm.environment.clear()"', 'postman.clearEnvironmentVariable': '"pm.environment.unset()"', 'postman.setGlobalVariable': '"pm.globals.set()"', 'postman.getGlobalVariable': '"pm.globals.get()"', 'postman.clearGlobalVariables': '"pm.globals.clear()"', 'postman.clearGlobalVariable': '"pm.globals.unset()"', 'postman.getResponseCookie': '"pm.cookies.get()"', 'postman.getResponseHeader': '"pm.response.headers.get()"' }, E = '', FUNCTION = 'function', LEGACY_ASSERTION_ERROR_MESSAGE_PREFIX = 'expected ', LEGACY_ASSERTION_ERROR_MESSAGE_SUFFIX = ' to be truthy', /** * Different modes for a request body. * * @enum {String} */ REQUEST_MODES = { RAW: 'raw', URLENCODED: 'urlencoded', FORMDATA: 'formdata', FILE: 'file' }, MAX_RESPONSE_SIZE = 50 * 1024 * 1024; // 50MB function getRequestBody (request) { var mode = _.get(request, 'body.mode'), body = _.get(request, 'body'), empty = body ? body.isEmpty() : true, content, computedBody; if (empty) { return; } content = body[mode]; if (_.isFunction(content && content.all)) { content = content.all(); } if (mode === REQUEST_MODES.RAW) { computedBody = { body: content }; } else if (mode === REQUEST_MODES.URLENCODED) { computedBody = { form: _.reduce(content, function (accumulator, param) { if (param.disabled) { return accumulator; } // This is actually pretty simple, // If the variable already exists in the accumulator, we need to make the value an Array with // all the variable values inside it. if (accumulator[param.key]) { _.isArray(accumulator[param.key]) ? accumulator[param.key].push(param.value) : (accumulator[param.key] = [accumulator[param.key], param.value]); } else { accumulator[param.key] = param.value; } return accumulator; }, {}) }; } else if (request.body.mode === REQUEST_MODES.FORMDATA) { computedBody = { formData: _.reduce(content, function (accumulator, param) { if (param.disabled) { return accumulator; } // This is actually pretty simple, // If the variable already exists in the accumulator, we need to make the value an Array with // all the variable values inside it. if (accumulator[param.key]) { _.isArray(accumulator[param.key]) ? accumulator[param.key].push(param.value) : (accumulator[param.key] = [accumulator[param.key], param.value]); } else { accumulator[param.key] = param.value; } return accumulator; }, {}) }; } else if (request.body.mode === REQUEST_MODES.FILE) { computedBody = { body: _.get(request, 'body.file.content') }; } return computedBody; } /** * Raises a single assertion event with an array of assertions from legacy `tests` object. * * @param {Uniscope} scope - * @param {Object} pmapi - * @param {Function} onAssertion - */ function raiseAssertionEvent (scope, pmapi, onAssertion) { var tests = scope._imports && scope._imports.tests, assertionIndex = pmapi.test.index(), assertions; if (_.isEmpty(tests)) { return; } assertions = _.map(tests, function (value, key) { var assertionName = String(key), passed = Boolean(value), assertionError = null; // fake an assertion error for legacy tests if (!passed) { assertionError = new Error(LEGACY_ASSERTION_ERROR_MESSAGE_PREFIX + String(value) + LEGACY_ASSERTION_ERROR_MESSAGE_SUFFIX); assertionError.name = 'AssertionError'; } // @todo Move getAssertionObject function from pmapi-setup-runner.js to a common place and reuse it here too return { name: assertionName, skipped: false, passed: passed, error: assertionError, index: assertionIndex++ }; }); onAssertion(assertions); } function logDeprecationWarning (key, legacyUsageSet, console) { // we've already logged warning once. ignore all next usages if (legacyUsageSet.has(key)) { return; } legacyUsageSet.add(key); if (LEGACY_GLOBS_ALTERNATIVES[key]) { console.warn(`Using "${key}" is deprecated. Use ${LEGACY_GLOBS_ALTERNATIVES[key]} instead.`); } else if (LEGACY_GLOBS.includes(key)) { console.warn(`Using "${key}" is deprecated.`); } } class PostmanLegacyInterface { /** * @param {Object} execution - * @param {Object} globalvars - */ constructor (execution, globalvars) { this.__execution = execution; this.__environment = globalvars.environment; this.__globals = globalvars.globals; } setEnvironmentVariable (key, value) { if ((value === false || value) && typeof (value && value.toString) === FUNCTION) { value = value.toString(); } this.__environment[key] = value; return this.__execution.environment.set(key, value); } getEnvironmentVariable (key) { return this.__execution.environment.get(key); } clearEnvironmentVariables () { for (var prop in this.__environment) { if (Object.hasOwn(this.__environment, prop)) { delete this.__environment[prop]; } } return this.__execution.environment.clear(); } clearEnvironmentVariable (key) { key && (delete this.__environment[key]); return this.__execution.environment.unset(key); } setGlobalVariable (key, value) { if ((value === false || value) && typeof (value && value.toString) === FUNCTION) { value = value.toString(); } this.__globals[key] = value; return this.__execution.globals.set(key, value); } getGlobalVariable (key) { return this.__execution.globals.get(key); } clearGlobalVariables () { for (var prop in this.__globals) { if (Object.hasOwn(this.__globals, prop)) { delete this.__globals[prop]; } } return this.__execution.globals.clear(); } clearGlobalVariable (key) { key && (delete this.__globals[key]); return this.__execution.globals.unset(key); } setNextRequest (what) { this.__execution.return && (this.__execution.return.nextRequest = what); } } /** * @constructor * @extends {PostmanLegacyInterface} */ class PostmanLegacyTestInterface extends PostmanLegacyInterface { /** * @param {String} cookieName - * @returns {Object} */ getResponseCookie (cookieName) { return this.__execution.cookies ? this.__execution.cookies.one(String(cookieName)) : undefined; } /** * @param {String} headerName - * @returns {String} */ getResponseHeader (headerName) { var header = (this.__execution.response && this.__execution.response.headers) && this.__execution.response.headers.one(headerName); return header ? header.value : undefined; } } module.exports = { /** * * @param {Uniscope} scope - * @param {Execution} execution - * @param {Object} console - * * @note ensure that globalvars variables added here are added as part of the LEGACY_GLOBS array */ setup (scope, execution, console) { /** * @name SandboxGlobals * @type {Object} */ var globalvars = _.assign({}, scopeLibraries); // set global variables that are exposed in legacy interface // --------------------------------------------------------- // 1. set the tests object (irrespective of target) /** * Store your assertions in this object * * @memberOf SandboxGlobals * @type {Object} */ globalvars.tests = {}; // 2. set common environment, globals and data /** * All global variables at the initial stages when the script ran * * @memberOf SandboxGlobals * @type {Object} */ globalvars.globals = execution.globals.syncVariablesTo(); /** * All environment variables at the initial stages when script ran * * @memberOf SandboxGlobals * @type {Object} */ globalvars.environment = execution.environment.syncVariablesTo(); /** * The data object if it was passed during a collection run * * @memberOf SandboxGlobals * @type {Object} */ globalvars.data = execution.data || (execution.data = {}); // 3. set the request object in legacy structure /** * The request that will be sent (or has been sent) * * @memberOf SandboxGlobals * @type {Object} */ globalvars.request = execution.request ? { id: execution.legacy ? execution.legacy._itemId : undefined, name: execution.legacy ? execution.legacy._itemName : undefined, description: execution.request.description ? execution.request.description.toString() : undefined, headers: execution.request.headers.toObject(true, false, true, true), method: execution.request.method, url: execution.request.url.toString(), data: (function (request) { var body = getRequestBody(request); return body ? (body.form || body.formData || body.body || {}) : {}; }(execution.request)) } : {}; // 4. set the response related objects if (execution.response) { /** * Stores the response cookies * * @memberOf SandboxGlobals * @type {Array.<Object>} */ globalvars.responseCookies = execution.cookies || []; /** * Stores the response headers with the keys being case sensitive * * @memberOf SandboxGlobals * @type {Array.<Object>} */ globalvars.responseHeaders = {}; execution.response && execution.response.headers && execution.response.headers.each(function (header) { header && !header.disabled && (globalvars.responseHeaders[header.key] = header.value); }); /** * @memberOf SandboxGlobals * @type {Number} */ globalvars.responseTime = execution.response ? execution.response.responseTime : NaN; /** * @memberOf SandboxGlobals * @type {Number} */ globalvars.responseCode = execution.response && _.isFunction(execution.response.details) ? _.clone(execution.response.details()) : { code: NaN, name: E, details: E }; /** * @memberOf SandboxGlobals * @type {String} */ globalvars.responseBody = (() => { // Truncating response body if it is too large to avoid negatively affecting // the performance since this get calculated for every execution by default if (!execution.response || execution.response.responseSize > MAX_RESPONSE_SIZE) { return; } return execution.response.text(); })(); } // 5. add the iteration information globalvars.iteration = _.isObject(execution.cursor) ? execution.cursor.iteration : 0; // 6. create the postman interface object const postmanLegacy = new (execution.response ? PostmanLegacyTestInterface : PostmanLegacyInterface)(execution, globalvars), legacyAPIUsage = new Set(); scope.__is_deprecation_warning_enabled = true; /** * @memberOf SandboxGlobals * @type {PostmanLegacyInterface} */ globalvars.postman = new Proxy(postmanLegacy, { get (target, prop) { scope.__is_deprecation_warning_enabled && logDeprecationWarning(`postman.${prop}`, legacyAPIUsage, console); return target[prop]; } }); // wrap all globals to ensure we track their usage to show warnings // on access. LEGACY_GLOBS.forEach((key) => { if (!(globalvars[key] && ['object', 'function'].includes(typeof globalvars[key]))) { return; } globalvars[key] = new Proxy(globalvars[key], { set (target, prop, value) { scope.__is_deprecation_warning_enabled && logDeprecationWarning(key, legacyAPIUsage, console); target[prop] = value; }, get (target, prop) { // special handling for postman.* because we're handling // those in the `PostmanLegacyInterface` itself. if (key !== 'postman') { scope.__is_deprecation_warning_enabled && logDeprecationWarning(key, legacyAPIUsage, console); } return target[prop]; }, apply (target, thisArg, args) { scope.__is_deprecation_warning_enabled && logDeprecationWarning(key, legacyAPIUsage, console); return target.apply(thisArg, args); } }); }); // make a final pass to ensure that the global variables are present // all the globals are now added to scope scope.import(globalvars); globalvars = null; // dereference // add a flag to ensure that when teardown is called, it does not tear down a scope that was never setup scope.__postman_legacy_setup = true; }, teardown (scope) { if (!scope.__postman_legacy_setup) { return; } for (var i = 0, ii = LEGACY_GLOBS.length; i < ii; i++) { scope.unset(LEGACY_GLOBS[i]); } scope.__postman_legacy_setup = false; }, /** * This is the place where we should put all the tasks * that need to be executed after the completion of script execution * * @param {Uniscope} scope - * @param {Object} pmapi - * @param {Function} onAssertion - */ finish (scope, pmapi, onAssertion) { if (!scope.__postman_legacy_setup) { return; } scope.__is_deprecation_warning_enabled = false; raiseAssertionEvent(scope, pmapi, onAssertion); }, LEGACY_GLOBS };