postman-sandbox
Version:
Sandbox for Postman Scripts to run in Node.js or browser
561 lines (493 loc) • 19.4 kB
JavaScript
const _ = require('lodash'),
sdk = require('postman-collection'),
PostmanCookieJar = require('./cookie-jar'),
VariableScope = sdk.VariableScope,
PostmanRequest = sdk.Request,
PostmanResponse = sdk.Response,
PostmanCookieList = sdk.CookieList,
chai = require('chai'),
/**
* Use this function to assign readonly properties to an object
*
* @private
*
* @param {Object} obj -
* @param {Object} properties -
* @param {Array.<String>} [disabledProperties=[]] -
*/
_assignDefinedReadonly = function (obj, properties, disabledProperties = []) {
var config = {
writable: false
},
prop;
for (prop in properties) {
if (
!_.includes(disabledProperties, prop) &&
Object.hasOwn(properties, prop) &&
(properties[prop] !== undefined)
) {
config.value = properties[prop];
Object.defineProperty(obj, prop, config);
}
}
return obj; // chainable
},
setupTestRunner = require('./pmapi-setup-runner');
/**
* @constructor
*
* @param {Execution} execution - execution context
* @param {Function} onRequest - callback to execute when pm.sendRequest() called
* @param {Function} onSkipRequest - callback to execute when pm.execution.skipRequest() called
* @param {Function} onAssertion - callback to execute when pm.expect() called
* @param {Object} cookieStore - cookie store
* @param {Vault} vault - vault
* @param {Function} datasets - datasets interface function, called as datasets(datasetId) returning a handle
* @param {Function} onRunRequest - callback to execute when pm.execution.runRequest is encountered in the script
* @param {Function} requireFn - requireFn
* @param {Object} [options] - options
* @param {Array.<String>} [options.disabledAPIs] - list of disabled APIs
*/
function Postman (execution,
onRequest,
onSkipRequest,
onAssertion,
cookieStore,
vault,
datasets,
onRunRequest,
requireFn,
options = {}) {
// @todo - ensure runtime passes data in a scope format
let iterationData = new VariableScope();
iterationData.syncVariablesFrom(execution.data);
// instead of creating new instance of variableScope,
// reuse one so that any changes made through pm.variables.set() is directly reflected
execution._variables.addLayer(iterationData.values);
execution._variables.addLayer(execution.environment.values);
execution._variables.addLayer(execution.collectionVariables.values);
execution._variables.addLayer(execution.globals.values);
execution.cookies && (execution.cookies.jar = function () {
return new PostmanCookieJar(cookieStore);
});
_assignDefinedReadonly(this, /** @lends Postman.prototype */ {
/**
* Contains information pertaining to the script execution
*
* @interface Info
*/
/**
* The pm.info object contains information pertaining to the script being executed.
* Useful information such as the request name, request Id, and iteration count are
* stored inside of this object.
*
* @type {Info}
*/
info: _assignDefinedReadonly({}, /** @lends Info */ {
/**
* Contains information whether the script being executed is a "prerequest" or a "test" script.
*
* @type {string}
* @instance
*/
eventName: execution.target,
/**
* Is the value of the current iteration being run.
*
* @type {number}
* @instance
*/
iteration: execution.cursor.iteration,
/**
* Is the total number of iterations that are scheduled to run.
*
* @type {number}
* @instance
*/
iterationCount: execution.cursor.cycles,
/**
* The saved name of the individual request being run.
*
* @type {string}
* @instance
*/
requestName: execution.legacy._itemName,
/**
* The unique guid that identifies the request being run.
*
* @type {string}
* @instance
*/
requestId: execution.legacy._itemId
}),
/**
* @interface Vault
*/
/**
* Get a value from the vault.
*
* @function
* @name Vault#get
* @param {string} key -
* @returns {Promise<string|undefined>}
*/
/**
* Set a value in the vault.
*
* @function
* @name Vault#set
* @param {string} key -
* @param {string} value -
* @returns {Promise<void>}
*/
/**
* Unset a value in the vault.
*
* @function
* @name Vault#unset
* @param {string} key -
* @returns {Promise<void>}
*/
/**
* @type {Vault}
*/
vault: vault,
/**
* @typedef {Object} DatasetStaleDatasource
* @property {string} name - Name of the datasource that could not be refreshed.
* @property {string} reason - Human-readable reason it is stale.
*/
/**
* Result of a dataset view or ad-hoc query execution.
*
* @typedef {Object} DatasetQueryResult
* @property {string[]} columns - Ordered column names.
* @property {AsyncIterable<Object>} rows - Single-pass async iterable of result rows.
* Iterate with `for await (const row of result.rows)`. Once exhausted it yields nothing.
* @property {DatasetStaleDatasource[]} [staleDatasources] -
* Datasources whose underlying file or database could not be refreshed.
* Surface as a soft warning, not a failure.
*/
/**
* Read-only handle for a single dataset, returned by `pm.datasets(datasetId)`.
*
* @interface DatasetHandle
*/
/**
* Execute a named view defined for the dataset.
*
* @function
* @name DatasetHandle#executeView
* @param {string} viewId - View ID or name.
* @param {string[]} [params] - Optional parameter overrides for parameterized views.
* @returns {Promise<DatasetQueryResult>}
*/
/**
* Run an arbitrary SQL query against the dataset.
*
* @function
* @name DatasetHandle#executeQuery
* @param {string} sql - SQL query string. `?` placeholders bind to `params` in order.
* @param {string[]} [params] - Optional parameter values for `?` placeholders.
* @returns {Promise<DatasetQueryResult>}
*/
/**
* Factory that returns a DatasetHandle for a given dataset ID.
*
* @callback Datasets
* @param {string} datasetId - The dataset ID.
* @returns {DatasetHandle}
*/
/**
* Access a dataset by ID. Returns a handle with methods for executing
* named views or ad-hoc SQL against the dataset.
*
* @type {Datasets}
*/
datasets: datasets,
/**
* @type {VariableScope}
*/
globals: execution.globals,
/**
* @type {VariableScope}
*/
environment: execution.environment,
/**
* @type {VariableScope}
*/
collectionVariables: execution.collectionVariables,
/**
* @type {VariableScope}
*/
variables: execution._variables,
/**
* The iterationData object contains data from the data file provided during a collection run.
*
* @type {VariableScope}
*/
iterationData: iterationData,
/**
* The request object inside pm is a representation of the request for which this script is being run.
* For a pre-request script, this is the request that is about to be sent and when in a test script,
* this is the representation of the request that was sent.
*
* @type {Request}
*/
request: execution.request,
/**
* Inside the test scripts, the pm.response object contains all information pertaining
* to the response that was received.
*
* @type {Response}
* @excludeFromPrerequestScript
*/
response: execution.response,
/**
* pm.message is an object with information pertaining to a part of a response in certain protocols.
*/
message: execution.message,
/**
* The cookies object contains a list of cookies that are associated with the domain
* to which the request was made.
*
* @type {CookieList}
*/
cookies: execution.cookies,
/**
* @interface Visualizer
*/
/**
* @type {Visualizer}
*/
visualizer: /** @lends Visualizer */ {
/**
* Set visualizer template and its options
*
* @instance
* @param {String} template - visualisation layout in form of template
* @param {Object} [data] - data object to be used in template
* @param {Object} [options] - options to use while processing the template
*/
set (template, data, options) {
if (typeof template !== 'string') {
throw new Error(`Invalid template. Template must be of type string, found ${typeof template}`);
}
if (data && typeof data !== 'object') {
throw new Error(`Invalid data. Data must be an object, found ${typeof data}`);
}
if (options && typeof options !== 'object') {
throw new Error(`Invalid options. Options must be an object, found ${typeof options}`);
}
/**
*
* @property {String} template - template string
* @property {Object} data - data to use while processing template
* @property {Object} options - options to use while processing template
*/
execution.return.visualizer = {
template,
data,
options
};
},
/**
* Clear all visualizer data
*
* @instance
*/
clear () {
execution.return.visualizer = undefined;
}
},
/**
* Allows one to send request from script asynchronously.
*
* @param {Request|String} req - request object or request url
* @param {Function} [callback] - callback function
* @returns {Promise<Response>|undefined} - returns a promise if callback is not provided
*/
sendRequest: function (req, callback) {
let history;
const self = this,
handler = () => {
return new Promise(function (resolve, reject) {
if (!req) {
reject(new Error('sendrequest: nothing to request'));
return;
}
onRequest(PostmanRequest.isRequest(req) ? req : (new PostmanRequest(req)),
function (err, resp, his) {
if (his && !PostmanCookieList.isCookieList(his.cookies)) {
his.cookies = new PostmanCookieList({}, his.cookies);
}
history = his;
if (err) {
return reject(err);
}
resolve(PostmanResponse.isResponse(resp) ? resp : (new PostmanResponse(resp)));
});
});
};
if (_.isFunction(callback)) {
handler().then((resp) => {
callback.call(self, null, resp, history);
}).catch((err) => {
callback.call(self, err);
});
return self;
}
return handler();
},
/**
* @interface Execution
*/
/**
* Exposes handlers to control or access execution state
*
* @type {Execution}
*/
execution: _assignDefinedReadonly({}, /** @lends Execution */ {
/**
* Stops the current request and its scripts from executing.
*
* @function
* @excludeFromTestScript
* @instance
*/
skipRequest: onSkipRequest,
/**
* @interface ExecutionLocation
* @extends Array<string>
*/
/**
* The path of the current request.
*
* @type {ExecutionLocation} - current execution path
* @instance
*/
location: _assignDefinedReadonly(execution.legacy._itemPath || [], /** @lends ExecutionLocation */ {
/**
* The item name whose script is currently being executed.
*
* @instance
* @type {string}
*/
current: execution.legacy._eventItemName
}),
/**
* Sets the next request to be run after the current request, when
* running the collection. Passing `null` stops the collection run
* after the current request is executed.
*
* @instance
* @param {string|null} request - name of the request to run next
*/
setNextRequest: function setNextRequest (request) {
execution.return && (execution.return.nextRequest = request);
},
/**
* Executes a collection request asynchronously.
*
* This function allows you to programmatically run any request that is part of an existing collection.
* The request will be executed within the current execution context.
*
* @instance
* @param {String} requestId - The UUID of the request to execute.
* This can be found in the request's metadata or corresponding collection JSON.
* @param {Object} [runRequestOptions] - Configuration options for the request execution
* @param {Object} [runRequestOptions.variables] - Key-value pairs of variables to override during
* request execution. These will act as temporary
* overrides for the this specific request run.
* @returns {Promise<Response | null>} A Promise that resolves to:
* - A Postman Response object if the request executes successfully
* - null if the request execution is skipped
* (e.g., via pm.execution.skipRequest)
*
* @throws {Error} Throws an error if:
* - requestId is not provided or is invalid
* - The specified request cannot be found in the collection
* - Request execution fails
*
* @example
* // Run a request by its ID
* try {
* const response = await pm.execution.runRequest('request-id');
* console.log('Status:', response.code);
* console.log('Response:', response.text());
* } catch (error) {
* console.error('Request failed:', error);
* }
*/
runRequest: function (requestId, runRequestOptions) {
return new Promise(function (resolve, reject) {
if (!requestId) {
reject(new Error('runRequest: collection request id not provided'));
return;
}
onRunRequest(requestId, runRequestOptions || {},
function (err, resp, context) {
if (err) {
return reject(err);
}
// For the user to receive `null` as the value of response if it is skipped
// `null` is a special return value for response that's dispatched if skipRequest
// has been encountered for a nested request
if (resp === null) {
return resolve(null);
}
const ResponseClass = context && context.responseType ?
options.getProtocolMetadata(context.responseType)?.Response :
PostmanResponse;
if (!ResponseClass) {
return resolve(resp);
}
resolve(ResponseClass.isResponse(resp) ? resp : (new ResponseClass(resp)));
});
});
}
},
(options.disabledAPIs || []).filter(function (disabledAPI) {
return disabledAPI.startsWith('execution.');
}).map(function (disabledAPI) {
return disabledAPI.split('execution.').pop();
})),
/**
* Imports a package in the script.
*
* @param {String} name - name of the module
* @returns {any} - exports from the module
*/
require: function (name) {
return requireFn(name);
}
}, options.disabledAPIs);
// extend pm api with test runner abilities
setupTestRunner(this, onAssertion);
// add response assertions
if (this.response) {
// these are removed before serializing see `purse.js`
Object.defineProperty(this.response, 'to', {
get () {
return chai.expect(this).to;
}
});
}
// add request assertions
if (this.request) {
// these are removed before serializing see `purse.js`
Object.defineProperty(this.request, 'to', {
get () {
return chai.expect(this).to;
}
});
}
iterationData = null; // precautionary
}
// expose chai assertion library via prototype
/**
* @type {Chai.ExpectStatic}
*/
Postman.prototype.expect = chai.expect;
// export
module.exports = Postman;