@usebruno/js 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ const chai = require('chai');
2
+ const Bru = require('../bru');
3
+ const BrunoGrpcRequest = require('./bruno-grpc-request');
4
+ const BrunoGrpcResponse = require('./bruno-grpc-response');
5
+ const { cleanJson } = require('../utils');
6
+ const { createBruTestResultMethods } = require('../utils/results');
7
+ const { runScriptInNodeVm } = require('../sandbox/node-vm');
8
+ const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
9
+ const { SANDBOX } = require('../utils/sandbox');
10
+ const { createScopeSetter } = require('../runtime/scripted-entries');
11
+
12
+ /**
13
+ * Runs the gRPC lifecycle hooks
14
+ *
15
+ * All four hooks share one body (`#runHook`) and differ only in what they put on `bru.grpc` and
16
+ * what their result object carries — `buildGrpc` and `baseResult` below.
17
+ *
18
+ * The shared body mirrors `ScriptRuntime`'s `runRequestScript` / `runResponseScript` step for step,
19
+ * substituting the gRPC request/response models. Two intentional differences, not oversights:
20
+ * - `bru.runRequest` rejects instead of running anything.
21
+ * - The models live under `bru.grpc` rather than as the `req` / `res` globals HTTP scripts get.
22
+ */
23
+ class GrpcScriptRuntime {
24
+ constructor(props) {
25
+ this.runtime = props?.runtime || 'quickjs';
26
+ }
27
+
28
+ /**
29
+ * @param {object} params
30
+ * @param {string} params.script - The hook body, already decommented by the caller
31
+ * @param {object} params.request - The prepared gRPC request
32
+ * @param {Function} params.buildGrpc - Returns the `bru.grpc` object for this hook
33
+ * @param {object} [params.baseResult] - Hook-specific fields the shared result is built on top of
34
+ */
35
+ async #runHook({
36
+ script,
37
+ request,
38
+ buildGrpc,
39
+ baseResult = {},
40
+ envVariables,
41
+ runtimeVariables,
42
+ secretVariables,
43
+ collectionPath,
44
+ onConsoleLog,
45
+ processEnvVars,
46
+ scriptingConfig,
47
+ collectionName
48
+ }) {
49
+ const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
50
+ const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
51
+ const collectionVariables = request?.collectionVariables || {};
52
+ const folderVariables = request?.folderVariables || {};
53
+ const requestVariables = request?.requestVariables || {};
54
+ const promptVariables = request?.promptVariables || {};
55
+ const assertionResults = request?.assertionResults || [];
56
+ const certsAndProxyConfig = request?.certsAndProxyConfig;
57
+ const scriptPath = request?.pathname;
58
+ const bru = new Bru({
59
+ runtime: this.runtime,
60
+ envVariables,
61
+ runtimeVariables,
62
+ processEnvVars,
63
+ secretVariables,
64
+ collectionPath,
65
+ collectionVariables,
66
+ folderVariables,
67
+ requestVariables,
68
+ globalEnvironmentVariables,
69
+ oauth2CredentialVariables,
70
+ collectionName,
71
+ promptVariables,
72
+ certsAndProxyConfig,
73
+ requestUrl: request?.url
74
+ });
75
+
76
+ bru.grpc = buildGrpc();
77
+
78
+ // extend bru with result getter methods
79
+ const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
80
+
81
+ const context = {
82
+ bru,
83
+ test,
84
+ expect: chai.expect,
85
+ assert: chai.assert,
86
+ __brunoTestResults: __brunoTestResults,
87
+ __bruSetScope: createScopeSetter(bru)
88
+ };
89
+
90
+ if (onConsoleLog && typeof onConsoleLog === 'function') {
91
+ const customLogger = (type) => {
92
+ return (...args) => {
93
+ onConsoleLog(type, cleanJson(args));
94
+ };
95
+ };
96
+ context.console = {
97
+ log: customLogger('log'),
98
+ debug: customLogger('debug'),
99
+ info: customLogger('info'),
100
+ warn: customLogger('warn'),
101
+ error: customLogger('error')
102
+ };
103
+ }
104
+
105
+ // A gRPC request can't run another request yet, so there is no `runRequestByItemPathname` to bind.
106
+ bru.runRequest = () => Promise.reject(new Error('bru.runRequest is not supported in gRPC scripts'));
107
+
108
+ const buildScriptResult = () => ({
109
+ ...baseResult,
110
+ envVariables: bru._envDirty ? cleanJson(envVariables) : null,
111
+ runtimeVariables: bru._runtimeVarsDirty ? cleanJson(runtimeVariables) : null,
112
+ collectionVariables: bru._collVarsDirty ? cleanJson(collectionVariables) : null,
113
+ globalEnvironmentVariables: bru._globalEnvDirty ? cleanJson(globalEnvironmentVariables) : null,
114
+ oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
115
+ results: cleanJson(__brunoTestResults.getResults()),
116
+ nextRequestName: bru.nextRequest,
117
+ skipRequest: bru.skipRequest,
118
+ stopExecution: bru.stopExecution,
119
+ scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
120
+ });
121
+
122
+ // Track script errors to attach partial results before re-throwing, so the variables the hook
123
+ // set before it threw still reach the caller
124
+ let scriptError = null;
125
+
126
+ try {
127
+ if (this.runtime === SANDBOX.NODEVM) {
128
+ await runScriptInNodeVm({
129
+ script,
130
+ context,
131
+ collectionPath,
132
+ scriptingConfig,
133
+ scriptPath
134
+ });
135
+ } else {
136
+ // default runtime is `quickjs`
137
+ await executeQuickJsVmAsync({
138
+ script,
139
+ context,
140
+ collectionPath,
141
+ scriptPath
142
+ });
143
+ }
144
+ } catch (error) {
145
+ scriptError = error;
146
+ }
147
+
148
+ if (scriptError) {
149
+ scriptError.partialResults = buildScriptResult();
150
+ throw scriptError;
151
+ }
152
+
153
+ return buildScriptResult();
154
+ }
155
+
156
+ async runGrpcRequestScript({
157
+ script,
158
+ request,
159
+ envVariables,
160
+ runtimeVariables,
161
+ secretVariables,
162
+ collectionPath,
163
+ onConsoleLog,
164
+ processEnvVars,
165
+ scriptingConfig,
166
+ collectionName
167
+ }) {
168
+ return this.#runHook({
169
+ script,
170
+ request,
171
+ // Initial scope - `request.messages` reports what the call sent, so it is still empty here.
172
+ buildGrpc: () => ({ request: new BrunoGrpcRequest(request, { metadataWritable: true }) }),
173
+ baseResult: { request },
174
+ envVariables,
175
+ runtimeVariables,
176
+ secretVariables,
177
+ collectionPath,
178
+ onConsoleLog,
179
+ processEnvVars,
180
+ scriptingConfig,
181
+ collectionName
182
+ });
183
+ }
184
+
185
+ async runGrpcResponseScript({
186
+ script,
187
+ request,
188
+ response,
189
+ envVariables,
190
+ runtimeVariables,
191
+ secretVariables,
192
+ collectionPath,
193
+ onConsoleLog,
194
+ processEnvVars,
195
+ scriptingConfig,
196
+ collectionName,
197
+ sentMessages = []
198
+ }) {
199
+ return this.#runHook({
200
+ script,
201
+ request,
202
+ // Passing sentMessages so only messages sent by client is accessible
203
+ buildGrpc: () => ({
204
+ request: new BrunoGrpcRequest(request, { sentMessages, metadataWritable: false }),
205
+ response: new BrunoGrpcResponse(response)
206
+ }),
207
+ baseResult: { response },
208
+ envVariables,
209
+ runtimeVariables,
210
+ secretVariables,
211
+ collectionPath,
212
+ onConsoleLog,
213
+ processEnvVars,
214
+ scriptingConfig,
215
+ collectionName
216
+ });
217
+ }
218
+
219
+ /**
220
+ * `before-message-send`. `message` is the message about to be transmitted; it joins
221
+ * `request.messages` only once the send succeeds, so the two never overlap.
222
+ *
223
+ * `bru.grpc.response` is deliberately absent, matching `beforeCallStart` — even on a bidi stream
224
+ * where messages have already been received.
225
+ */
226
+ async runGrpcBeforeMessageSendScript({
227
+ script,
228
+ request,
229
+ message,
230
+ envVariables,
231
+ runtimeVariables,
232
+ secretVariables,
233
+ collectionPath,
234
+ onConsoleLog,
235
+ processEnvVars,
236
+ scriptingConfig,
237
+ collectionName,
238
+ sentMessages = []
239
+ }) {
240
+ return this.#runHook({
241
+ script,
242
+ request,
243
+ buildGrpc: () => ({
244
+ request: new BrunoGrpcRequest(request, { metadataWritable: false, sentMessages, message })
245
+ }),
246
+ // `message` is carried on the result for the planned `message.set`; discarded by callers today.
247
+ baseResult: { request, message },
248
+ envVariables,
249
+ runtimeVariables,
250
+ secretVariables,
251
+ collectionPath,
252
+ onConsoleLog,
253
+ processEnvVars,
254
+ scriptingConfig,
255
+ collectionName
256
+ });
257
+ }
258
+
259
+ /**
260
+ * `after-message-receive`. `message` is the message just received, and is also the last entry of
261
+ * `response.messages` — the call folds it in before the hook runs.
262
+ *
263
+ * The `response` here is *partial*: the call is still open, so `statusCode`, `statusText`,
264
+ * `duration` and `trailers` are not yet known and read as `undefined` / empty. `metadata` is
265
+ * usually populated, since headers precede the first message.
266
+ */
267
+ async runGrpcAfterMessageReceiveScript({
268
+ script,
269
+ request,
270
+ response,
271
+ message,
272
+ envVariables,
273
+ runtimeVariables,
274
+ secretVariables,
275
+ collectionPath,
276
+ onConsoleLog,
277
+ processEnvVars,
278
+ scriptingConfig,
279
+ collectionName,
280
+ sentMessages = []
281
+ }) {
282
+ return this.#runHook({
283
+ script,
284
+ request,
285
+ buildGrpc: () => ({
286
+ request: new BrunoGrpcRequest(request, { metadataWritable: false, sentMessages }),
287
+ response: new BrunoGrpcResponse(response, { message })
288
+ }),
289
+ baseResult: { response, message },
290
+ envVariables,
291
+ runtimeVariables,
292
+ secretVariables,
293
+ collectionPath,
294
+ onConsoleLog,
295
+ processEnvVars,
296
+ scriptingConfig,
297
+ collectionName
298
+ });
299
+ }
300
+ }
301
+
302
+ module.exports = GrpcScriptRuntime;
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const ScriptRuntime = require('./runtime/script-runtime');
2
+ const GrpcScriptRuntime = require('./grpc/grpc-script-runtime');
2
3
  const TestRuntime = require('./runtime/test-runtime');
3
4
  const VarsRuntime = require('./runtime/vars-runtime');
4
5
  const AssertRuntime = require('./runtime/assert-runtime');
@@ -21,6 +22,7 @@ const {
21
22
 
22
23
  module.exports = {
23
24
  ScriptRuntime,
25
+ GrpcScriptRuntime,
24
26
  TestRuntime,
25
27
  VarsRuntime,
26
28
  AssertRuntime,
@@ -2,7 +2,7 @@ const { interpolate } = require('@usebruno/common');
2
2
 
3
3
  const interpolateString = (
4
4
  str,
5
- { envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {}, globalEnvironmentVariables = {} }
5
+ { envVariables = {}, runtimeVariables = {}, iterationData = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {}, globalEnvironmentVariables = {} }
6
6
  ) => {
7
7
  if (!str || !str.length || typeof str !== 'string') {
8
8
  return str;
@@ -15,6 +15,7 @@ const interpolateString = (
15
15
  ...folderVariables,
16
16
  ...requestVariables,
17
17
  ...runtimeVariables,
18
+ ...iterationData,
18
19
  process: {
19
20
  env: {
20
21
  ...processEnvVars
@@ -362,6 +362,7 @@ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
362
362
  folderVariables: context.bru.folderVariables,
363
363
  requestVariables: context.bru.requestVariables,
364
364
  runtimeVariables: context.bru.runtimeVariables,
365
+ iterationData: context.bru.runner.iterationData.get(),
365
366
  envVariables: context.bru.envVariables,
366
367
  processEnvVars: context.bru.processEnvVars
367
368
  };
@@ -32,7 +32,7 @@ class ScriptRuntime {
32
32
  collectionName
33
33
  ) {
34
34
  let visualizations = [];
35
- let setVisualizations = (data) => {
35
+ const setVisualizations = (data) => {
36
36
  if (data.type === VISUALIZATION_CLEAR) {
37
37
  visualizations = [];
38
38
  } else {
@@ -73,7 +73,7 @@ class ScriptRuntime {
73
73
  const req = new BrunoRequest(request, historyLogger);
74
74
 
75
75
  // extend bru with result getter methods
76
- const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
76
+ const { __brunoTestResults, test, waitForPendingTests } = createBruTestResultMethods(bru, assertionResults, chai);
77
77
 
78
78
  const context = {
79
79
  bru,
@@ -119,6 +119,18 @@ class ScriptRuntime {
119
119
  scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
120
120
  });
121
121
 
122
+ const attachScriptResultToOnFailHandler = () => {
123
+ if (typeof request.onFailHandler !== 'function') {
124
+ return;
125
+ }
126
+
127
+ const onFailHandler = request.onFailHandler;
128
+ request.onFailHandler = async (error) => {
129
+ await onFailHandler(error);
130
+ return buildRequestScriptResult();
131
+ };
132
+ };
133
+
122
134
  // Track script errors to attach partial results before re-throwing
123
135
  // This ensures that any test() calls that passed before the error are preserved
124
136
  // Similar pattern to test-runtime.js which already handles this correctly
@@ -136,6 +148,7 @@ class ScriptRuntime {
136
148
  } catch (error) {
137
149
  scriptError = error;
138
150
  }
151
+ await waitForPendingTests();
139
152
 
140
153
  // If script errored, attach partial results so callers can display passed tests
141
154
  // before the error occurred (e.g., 2 tests pass, then script throws)
@@ -144,6 +157,7 @@ class ScriptRuntime {
144
157
  throw scriptError;
145
158
  }
146
159
 
160
+ attachScriptResultToOnFailHandler();
147
161
  return buildRequestScriptResult();
148
162
  }
149
163
 
@@ -164,6 +178,7 @@ class ScriptRuntime {
164
178
  throw scriptError;
165
179
  }
166
180
 
181
+ attachScriptResultToOnFailHandler();
167
182
  return buildRequestScriptResult();
168
183
  }
169
184
 
@@ -183,7 +198,7 @@ class ScriptRuntime {
183
198
  collectionName
184
199
  ) {
185
200
  let visualizations = [];
186
- let setVisualizations = (data) => {
201
+ const setVisualizations = (data) => {
187
202
  if (data.type === VISUALIZATION_CLEAR) {
188
203
  visualizations = [];
189
204
  } else {
@@ -225,7 +240,7 @@ class ScriptRuntime {
225
240
  const res = new BrunoResponse(response);
226
241
 
227
242
  // extend bru with result getter methods
228
- const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
243
+ const { __brunoTestResults, test, waitForPendingTests } = createBruTestResultMethods(bru, assertionResults, chai);
229
244
 
230
245
  const context = {
231
246
  bru,
@@ -289,6 +304,7 @@ class ScriptRuntime {
289
304
  } catch (error) {
290
305
  scriptError = error;
291
306
  }
307
+ await waitForPendingTests();
292
308
 
293
309
  // If script errored, attach partial results so callers can display passed tests
294
310
  // before the error occurred (e.g., 2 tests pass, then script throws)
@@ -64,7 +64,7 @@ class TestRuntime {
64
64
  const res = new BrunoResponse(response);
65
65
 
66
66
  // extend bru with result getter methods
67
- const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
67
+ const { __brunoTestResults, test, waitForPendingTests } = createBruTestResultMethods(bru, assertionResults, chai);
68
68
 
69
69
  if (!testsFile || !testsFile.length) {
70
70
  return {
@@ -74,7 +74,8 @@ class TestRuntime {
74
74
  collectionVariables: null,
75
75
  globalEnvironmentVariables: null,
76
76
  results: __brunoTestResults.getResults(),
77
- nextRequestName: bru.nextRequest
77
+ nextRequestName: bru.nextRequest,
78
+ stopExecution: bru.stopExecution
78
79
  };
79
80
  }
80
81
 
@@ -109,8 +110,8 @@ class TestRuntime {
109
110
 
110
111
  let scriptError = null;
111
112
 
112
- try {
113
- if (this.runtime === SANDBOX.NODEVM) {
113
+ if (this.runtime === SANDBOX.NODEVM) {
114
+ try {
114
115
  await runScriptInNodeVm({
115
116
  script: testsFile,
116
117
  context,
@@ -118,17 +119,22 @@ class TestRuntime {
118
119
  scriptingConfig,
119
120
  scriptPath
120
121
  });
121
- } else {
122
- // default runtime is `quickjs`
122
+ } catch (error) {
123
+ scriptError = error;
124
+ }
125
+ await waitForPendingTests();
126
+ } else {
127
+ // default runtime is `quickjs`
128
+ try {
123
129
  await executeQuickJsVmAsync({
124
130
  script: testsFile,
125
131
  context: context,
126
132
  collectionPath,
127
133
  scriptPath
128
134
  });
135
+ } catch (error) {
136
+ scriptError = error;
129
137
  }
130
- } catch (error) {
131
- scriptError = error;
132
138
  }
133
139
 
134
140
  if (historyLogger) {
@@ -149,6 +155,7 @@ class TestRuntime {
149
155
  oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
150
156
  results: cleanJson(__brunoTestResults.getResults()),
151
157
  nextRequestName: bru.nextRequest,
158
+ stopExecution: bru.stopExecution,
152
159
  scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
153
160
  };
154
161