@usebruno/js 0.51.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
  };
@@ -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
 
@@ -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 {
@@ -110,8 +110,8 @@ class TestRuntime {
110
110
 
111
111
  let scriptError = null;
112
112
 
113
- try {
114
- if (this.runtime === SANDBOX.NODEVM) {
113
+ if (this.runtime === SANDBOX.NODEVM) {
114
+ try {
115
115
  await runScriptInNodeVm({
116
116
  script: testsFile,
117
117
  context,
@@ -119,17 +119,22 @@ class TestRuntime {
119
119
  scriptingConfig,
120
120
  scriptPath
121
121
  });
122
- } else {
123
- // default runtime is `quickjs`
122
+ } catch (error) {
123
+ scriptError = error;
124
+ }
125
+ await waitForPendingTests();
126
+ } else {
127
+ // default runtime is `quickjs`
128
+ try {
124
129
  await executeQuickJsVmAsync({
125
130
  script: testsFile,
126
131
  context: context,
127
132
  collectionPath,
128
133
  scriptPath
129
134
  });
135
+ } catch (error) {
136
+ scriptError = error;
130
137
  }
131
- } catch (error) {
132
- scriptError = error;
133
138
  }
134
139
 
135
140
  if (historyLogger) {