/
execute.js
221 lines (188 loc) · 8.68 KB
/
execute.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
const _ = require('lodash'),
chai = require('chai'),
Ajv = require('ajv'),
Scope = require('uniscope'),
sdk = require('postman-collection'),
PostmanEvent = sdk.Event,
Execution = require('./execution'),
PostmanConsole = require('./console'),
PostmanTimers = require('./timers'),
PostmanAPI = require('./pmapi'),
PostmanCookieStore = require('./cookie-store'),
EXECUTION_RESULT_EVENT_BASE = 'execution.result.',
EXECUTION_REQUEST_EVENT_BASE = 'execution.request.',
EXECUTION_ERROR_EVENT = 'execution.error',
EXECUTION_ERROR_EVENT_BASE = 'execution.error.',
EXECUTION_ABORT_EVENT_BASE = 'execution.abort.',
EXECUTION_RESPONSE_EVENT_BASE = 'execution.response.',
EXECUTION_COOKIES_EVENT_BASE = 'execution.cookies.',
EXECUTION_ASSERTION_EVENT = 'execution.assertion',
EXECUTION_ASSERTION_EVENT_BASE = 'execution.assertion.',
executeContext = require('./execute-context');
module.exports = function (bridge, glob) {
// @note we use a common scope for all executions. this causes issues when scripts are run inside the sandbox
// in parallel, but we still use this way for the legacy "persistent" behaviour needed in environment
const scope = Scope.create({
eval: true,
ignore: ['require'],
block: ['bridge']
});
// For caching required information provided during
// initialization which will be used during execution
let initializationOptions = {},
initializeExecution;
/**
* @param {Object} options
* @param {String} [options.template]
* @param {Boolean} [options.disableLegacyAPIs]
* @param {Array.<String>} [options.disabledAPIs]
*/
bridge.once('initialize', ({ template, ...initOptions }) => {
initializationOptions = initOptions || {};
// If no custom template is provided, go ahead with the default steps
if (!template) {
chai.use(require('chai-postman')(sdk, _, Ajv));
return bridge.dispatch('initialize');
}
const _module = { exports: {} },
scope = Scope.create({
eval: true,
ignore: ['require'],
block: ['bridge']
});
scope.import({
Buffer: require('buffer').Buffer,
module: _module
});
scope.exec(template, (err) => {
if (err) {
return bridge.dispatch('initialize', err);
}
const { chaiPlugin, initializeExecution: setupExecution } = (_module && _module.exports) || {};
if (_.isFunction(chaiPlugin)) {
chai.use(chaiPlugin);
}
if (_.isFunction(setupExecution)) {
initializeExecution = setupExecution;
}
bridge.dispatch('initialize');
});
});
/**
* @param {String} id
* @param {Event} event
* @param {Object} context
* @param {Object} options
* @param {Boolean=} [options.debug]
* @param {Object=} [options.cursor]
* @param {Number=} [options.timeout]
*
* @note
* options also take in legacy properties: _itemId, _itemName
*/
bridge.on('execute', function (id, event, context, options) {
if (!(id && _.isString(id))) {
return bridge.dispatch('error', new Error('sandbox: execution identifier parameter(s) missing'));
}
!options && (options = {});
!context && (context = {});
event = (new PostmanEvent(event));
const executionEventName = EXECUTION_RESULT_EVENT_BASE + id,
executionRequestEventName = EXECUTION_REQUEST_EVENT_BASE + id,
errorEventName = EXECUTION_ERROR_EVENT_BASE + id,
abortEventName = EXECUTION_ABORT_EVENT_BASE + id,
responseEventName = EXECUTION_RESPONSE_EVENT_BASE + id,
cookiesEventName = EXECUTION_COOKIES_EVENT_BASE + id,
assertionEventName = EXECUTION_ASSERTION_EVENT_BASE + id,
// extract the code from event. The event can be the code itself and we know that if the event is of type
// string.
code = _.isFunction(event.script && event.script.toSource) && event.script.toSource(),
// create the execution object
execution = new Execution(id, event, context, { ...options, initializeExecution }),
/**
* Dispatch assertions from `pm.test` or legacy `test` API.
*
* @private
* @param {Object[]} assertions -
* @param {String} assertions[].name -
* @param {Number} assertions[].index -
* @param {Object} assertions[].error -
* @param {Boolean} assertions[].async -
* @param {Boolean} assertions[].passed -
* @param {Boolean} assertions[].skipped -
*/
dispatchAssertions = function (assertions) {
// Legacy `test` API accumulates all the assertions and dispatches at once
// whereas, `pm.test` dispatch on every single assertion.
// For compatibility, dispatch the single assertion as an array.
!Array.isArray(assertions) && (assertions = [assertions]);
bridge.dispatch(assertionEventName, options.cursor, assertions);
bridge.dispatch(EXECUTION_ASSERTION_EVENT, options.cursor, assertions);
};
let waiting,
timers;
// create the controlled timers
timers = new PostmanTimers(null, function (err) {
if (err) { // propagate the error out of sandbox
bridge.dispatch(errorEventName, options.cursor, err);
bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err);
}
}, function () {
execution.return.async = true;
}, function (err, dnd) {
// clear timeout tracking timer
waiting && (waiting = timers.getWrappedTimer('clearTimeout')(waiting));
// do not allow any more timers
if (timers) {
timers.seal();
timers.clearAll();
}
// remove listener of disconnection event
bridge.off(abortEventName);
bridge.off(responseEventName);
if (err) { // fire extra execution error event
bridge.dispatch(errorEventName, options.cursor, err);
bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err);
}
// @note delete response from the execution object to avoid dispatching
// the large response payload back due to performance reasons.
execution.response && (delete execution.response);
// fire the execution completion event
(dnd !== true) && bridge.dispatch(executionEventName, err || null, execution);
});
// if a timeout is set, we must ensure that all pending timers are cleared and an execution timeout event is
// triggered.
_.isFinite(options.timeout) && (waiting = timers.getWrappedTimer('setTimeout')(function () {
timers.terminate(new Error('sandbox: ' +
(execution.return.async ? 'asynchronous' : 'synchronous') + ' script execution timeout'));
}, options.timeout));
// if an abort event is sent, compute cleanup and complete
bridge.on(abortEventName, function () {
timers.terminate(null, true);
});
// handle response event from outside sandbox
bridge.on(responseEventName, function (id, err, res, history) {
timers.clearEvent(id, err, res, history);
});
// handle cookies event from outside sandbox
bridge.on(cookiesEventName, function (id, err, res) {
timers.clearEvent(id, err, res);
});
// send control to the function that executes the context and prepares the scope
executeContext(scope, code, execution,
// if a console is sent, we use it. otherwise this also prevents erroneous referencing to any console
// inside this closure.
(new PostmanConsole(bridge, id, options.cursor, options.debug && glob.console)),
timers,
(
new PostmanAPI(execution, function (request, callback) {
var eventId = timers.setEvent(callback);
bridge.dispatch(executionRequestEventName, options.cursor, id, eventId, request);
}, dispatchAssertions, new PostmanCookieStore(id, bridge, timers), {
disabledAPIs: initializationOptions.disabledAPIs
})
),
dispatchAssertions,
{ disableLegacyAPIs: initializationOptions.disableLegacyAPIs });
});
};