@usebruno/filestore 0.10.0 → 0.12.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.
Files changed (143) hide show
  1. package/dist/cjs/index.js +1 -1
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/workers/worker-script.js +1 -1
  4. package/dist/cjs/workers/worker-script.js.map +1 -1
  5. package/dist/constants.d.ts +2 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/esm/index.js +1 -1
  8. package/dist/esm/index.js.map +1 -1
  9. package/dist/esm/workers/worker-script.js +1 -1
  10. package/dist/esm/workers/worker-script.js.map +1 -1
  11. package/dist/formats/bru/index.d.ts.map +1 -1
  12. package/dist/formats/bru/utils/redact-large-text-blocks.d.ts +11 -0
  13. package/dist/formats/bru/utils/redact-large-text-blocks.d.ts.map +1 -0
  14. package/dist/formats/yml/common/app.d.ts +8 -0
  15. package/dist/formats/yml/common/app.d.ts.map +1 -0
  16. package/dist/formats/yml/common/auth.d.ts.map +1 -1
  17. package/dist/formats/yml/common/body.d.ts.map +1 -1
  18. package/dist/formats/yml/common/datatype.d.ts +14 -0
  19. package/dist/formats/yml/common/datatype.d.ts.map +1 -0
  20. package/dist/formats/yml/common/headers.d.ts.map +1 -1
  21. package/dist/formats/yml/common/variables.d.ts +1 -2
  22. package/dist/formats/yml/common/variables.d.ts.map +1 -1
  23. package/dist/formats/yml/index.d.ts +3 -1
  24. package/dist/formats/yml/index.d.ts.map +1 -1
  25. package/dist/formats/yml/items/parseApp.d.ts +13 -0
  26. package/dist/formats/yml/items/parseApp.d.ts.map +1 -0
  27. package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
  28. package/dist/formats/yml/items/parseGrpcRequest.d.ts.map +1 -1
  29. package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
  30. package/dist/formats/yml/items/parseWebsocketRequest.d.ts.map +1 -1
  31. package/dist/formats/yml/items/stringifyApp.d.ts +4 -0
  32. package/dist/formats/yml/items/stringifyApp.d.ts.map +1 -0
  33. package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
  34. package/dist/formats/yml/items/stringifyGrpcRequest.d.ts.map +1 -1
  35. package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
  36. package/dist/formats/yml/items/stringifyWebsocketRequest.d.ts.map +1 -1
  37. package/dist/formats/yml/mockServerTypes.d.ts +95 -0
  38. package/dist/formats/yml/mockServerTypes.d.ts.map +1 -0
  39. package/dist/formats/yml/parseCollection.d.ts.map +1 -1
  40. package/dist/formats/yml/parseEnvironment.d.ts +3 -1
  41. package/dist/formats/yml/parseEnvironment.d.ts.map +1 -1
  42. package/dist/formats/yml/parseFolder.d.ts.map +1 -1
  43. package/dist/formats/yml/parseItem.d.ts.map +1 -1
  44. package/dist/formats/yml/parseMockServer.d.ts +4 -0
  45. package/dist/formats/yml/parseMockServer.d.ts.map +1 -0
  46. package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
  47. package/dist/formats/yml/stringifyEnvironment.d.ts +3 -1
  48. package/dist/formats/yml/stringifyEnvironment.d.ts.map +1 -1
  49. package/dist/formats/yml/stringifyFolder.d.ts.map +1 -1
  50. package/dist/formats/yml/stringifyItem.d.ts.map +1 -1
  51. package/dist/formats/yml/stringifyMockServer.d.ts +4 -0
  52. package/dist/formats/yml/stringifyMockServer.d.ts.map +1 -0
  53. package/dist/index.d.ts +18 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/types.d.ts +7 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/utils/secretsJsonToEnvironments.d.ts +38 -0
  58. package/dist/utils/secretsJsonToEnvironments.d.ts.map +1 -0
  59. package/dist/workers/WorkerQueue/index.d.ts +3 -2
  60. package/dist/workers/WorkerQueue/index.d.ts.map +1 -1
  61. package/dist/workers/index.d.ts +4 -0
  62. package/dist/workers/index.d.ts.map +1 -1
  63. package/package.json +7 -5
  64. package/src/constants.ts +13 -0
  65. package/src/formats/bru/index.ts +52 -0
  66. package/src/formats/bru/tests/parseBruEnvironment.spec.js +63 -0
  67. package/src/formats/bru/tests/redact-large-text-blocks.spec.js +472 -0
  68. package/src/formats/bru/utils/redact-large-text-blocks.ts +143 -0
  69. package/src/formats/yml/common/actions.ts +1 -1
  70. package/src/formats/yml/common/app.ts +27 -0
  71. package/src/formats/yml/common/auth.ts +53 -1
  72. package/src/formats/yml/common/body.spec.ts +62 -0
  73. package/src/formats/yml/common/body.ts +27 -9
  74. package/src/formats/yml/common/datatype.spec.ts +110 -0
  75. package/src/formats/yml/common/datatype.ts +57 -0
  76. package/src/formats/yml/common/headers.ts +4 -0
  77. package/src/formats/yml/common/variables.spec.ts +127 -0
  78. package/src/formats/yml/common/variables.ts +14 -22
  79. package/src/formats/yml/index.ts +5 -1
  80. package/src/formats/yml/items/parseApp.ts +37 -0
  81. package/src/formats/yml/items/parseGraphQLRequest.ts +14 -16
  82. package/src/formats/yml/items/parseGrpcRequest.ts +24 -3
  83. package/src/formats/yml/items/parseHttpRequest.ts +19 -16
  84. package/src/formats/yml/items/parseWebsocketRequest.ts +29 -9
  85. package/src/formats/yml/items/stringifyApp.ts +45 -0
  86. package/src/formats/yml/items/stringifyGraphQLRequest.ts +7 -6
  87. package/src/formats/yml/items/stringifyGrpcRequest.ts +9 -11
  88. package/src/formats/yml/items/stringifyHttpRequest.ts +14 -6
  89. package/src/formats/yml/items/stringifyWebsocketRequest.ts +22 -9
  90. package/src/formats/yml/mockServerTypes.ts +95 -0
  91. package/src/formats/yml/parseCollection.spec.ts +193 -0
  92. package/src/formats/yml/parseCollection.ts +24 -6
  93. package/src/formats/yml/parseEnvironment.spec.ts +99 -0
  94. package/src/formats/yml/parseEnvironment.ts +41 -19
  95. package/src/formats/yml/parseFolder.spec.ts +70 -0
  96. package/src/formats/yml/parseFolder.ts +19 -22
  97. package/src/formats/yml/parseItem.spec.ts +147 -0
  98. package/src/formats/yml/parseItem.ts +4 -0
  99. package/src/formats/yml/parseMockServer.spec.ts +180 -0
  100. package/src/formats/yml/parseMockServer.ts +113 -0
  101. package/src/formats/yml/stringifyCollection.spec.ts +223 -0
  102. package/src/formats/yml/stringifyCollection.ts +26 -9
  103. package/src/formats/yml/stringifyEnvironment.spec.ts +91 -0
  104. package/src/formats/yml/stringifyEnvironment.ts +55 -20
  105. package/src/formats/yml/stringifyFolder.spec.ts +70 -0
  106. package/src/formats/yml/stringifyFolder.ts +7 -2
  107. package/src/formats/yml/stringifyItem.spec.ts +53 -0
  108. package/src/formats/yml/stringifyItem.ts +4 -0
  109. package/src/formats/yml/stringifyMockServer.spec.ts +177 -0
  110. package/src/formats/yml/stringifyMockServer.ts +129 -0
  111. package/src/formats/yml/tests/defaultEnvironment.spec.js +50 -0
  112. package/src/formats/yml/tests/fixtures/presets/empty-request.yml +7 -0
  113. package/src/formats/yml/tests/fixtures/presets/no-presets.yml +3 -0
  114. package/src/formats/yml/tests/fixtures/presets/no-request-key.yml +6 -0
  115. package/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml +24 -0
  116. package/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml +25 -0
  117. package/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml +9 -0
  118. package/src/formats/yml/tests/parseCollection.spec.js +63 -0
  119. package/src/formats/yml/tests/parseEnvironment.spec.js +24 -63
  120. package/src/formats/yml/tests/parseEnvironment.spec.ts +160 -0
  121. package/src/formats/yml/tests/stringifyEnvironment.spec.ts +230 -0
  122. package/src/formats/yml/tests/variables.spec.js +7 -7
  123. package/src/formats/yml/utils.ts +1 -1
  124. package/src/index.ts +39 -1
  125. package/src/types/bruno-lang.d.ts +1 -0
  126. package/src/types.ts +8 -0
  127. package/src/utils/secretsJsonToEnvironments.spec.ts +211 -0
  128. package/src/utils/secretsJsonToEnvironments.ts +129 -0
  129. package/src/workers/WorkerQueue/index.ts +10 -2
  130. package/src/workers/index.ts +25 -1
  131. package/src/workers/worker-script.ts +45 -21
  132. package/dist/formats/bru/tests/example-status-swap.spec.d.ts +0 -2
  133. package/dist/formats/bru/tests/example-status-swap.spec.d.ts.map +0 -1
  134. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts +0 -57
  135. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts.map +0 -1
  136. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts +0 -2
  137. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts.map +0 -1
  138. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts +0 -2
  139. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts.map +0 -1
  140. package/dist/formats/yml/tests/parseEnvironment.spec.d.ts +0 -2
  141. package/dist/formats/yml/tests/parseEnvironment.spec.d.ts.map +0 -1
  142. package/dist/formats/yml/tests/variables.spec.d.ts +0 -2
  143. package/dist/formats/yml/tests/variables.spec.d.ts.map +0 -1
@@ -8,11 +8,26 @@ import {
8
8
  jsonToCollectionBru as _jsonToCollectionBru
9
9
  } from '@usebruno/lang';
10
10
  import { getOauth2AdditionalParameters } from './utils/oauth2-additional-params';
11
+ import { uuid } from '../../utils';
11
12
 
12
13
  export const parseBruRequest = (data: string | any, parsed: boolean = false): any => {
13
14
  try {
14
15
  const json = parsed ? data : bruToJsonV2(data);
15
16
 
17
+ if (_.get(json, 'meta.type') === 'app') {
18
+ const seq = _.get(json, 'meta.seq');
19
+ const tags = _.get(json, 'meta.tags', []);
20
+ return {
21
+ type: 'app',
22
+ name: _.get(json, 'meta.name'),
23
+ seq: !_.isNaN(seq) ? Number(seq) : 1,
24
+ tags: Array.isArray(tags) ? tags : [],
25
+ settings: _.get(json, 'settings', {}),
26
+ app: { code: _.get(json, 'app.code', null) },
27
+ request: null
28
+ };
29
+ }
30
+
16
31
  let requestType = _.get(json, 'meta.type');
17
32
  switch (requestType) {
18
33
  case 'http':
@@ -38,11 +53,18 @@ export const parseBruRequest = (data: string | any, parsed: boolean = false): an
38
53
  'ws-request': 'ws.url',
39
54
  'default': 'http.url'
40
55
  };
56
+
57
+ const appData = _.get(json, 'app');
58
+ const app = appData
59
+ ? { code: _.get(appData, 'code', null), enabled: _.get(appData, 'enabled', false) === true }
60
+ : null;
61
+
41
62
  const transformedJson = {
42
63
  type: requestType,
43
64
  name: _.get(json, 'meta.name'),
44
65
  seq: !_.isNaN(sequence) ? Number(sequence) : 1,
45
66
  settings: _.get(json, 'settings', {}),
67
+ app,
46
68
  tags: Array.isArray(tags) ? tags : [],
47
69
  request: {
48
70
  // Preserving special characters in custom methods. Using _.upperCase strips special characters.
@@ -117,6 +139,22 @@ export const parseBruRequest = (data: string | any, parsed: boolean = false): an
117
139
 
118
140
  export const stringifyBruRequest = (json: any): string => {
119
141
  try {
142
+ // Standalone app item — emit only meta + the app code block.
143
+ if (_.get(json, 'type') === 'app') {
144
+ const seq = _.get(json, 'seq');
145
+ const bruJson: any = {
146
+ meta: {
147
+ name: _.get(json, 'name'),
148
+ type: 'app',
149
+ seq: !_.isNaN(seq) ? Number(seq) : 1,
150
+ tags: _.get(json, 'tags', [])
151
+ },
152
+ settings: _.get(json, 'settings', {}),
153
+ app: { code: _.get(json, 'app.code', '') }
154
+ };
155
+ return jsonToBruV2(bruJson);
156
+ }
157
+
120
158
  let type = _.get(json, 'type');
121
159
  switch (type) {
122
160
  case 'http-request':
@@ -221,6 +259,11 @@ export const stringifyBruRequest = (json: any): string => {
221
259
  bruJson.docs = _.get(json, 'request.docs', '');
222
260
  bruJson.examples = _.get(json, 'examples', []).map((e: any) => jsonExampleToBru(e));
223
261
 
262
+ const app = _.get(json, 'app');
263
+ if (app && (app.enabled === true || (app.code && app.code.length))) {
264
+ bruJson.app = { code: app.code || null, enabled: app.enabled === true };
265
+ }
266
+
224
267
  const bru = jsonToBruV2(bruJson);
225
268
  return bru;
226
269
  } catch (error) {
@@ -326,6 +369,15 @@ export const parseBruEnvironment = (bru: string): any => {
326
369
  _.each(json.variables, (v: any) => (v.type = 'text'));
327
370
  }
328
371
 
372
+ // bruToEnvJsonV2 emits the nested `externalSecrets: { type, variables: [{ name,
373
+ // <identifier>, enabled }] }`. The app/schema additionally expect a `uid` per variable.
374
+ if (json?.externalSecrets && Array.isArray(json.externalSecrets.variables)) {
375
+ json.externalSecrets.variables = json.externalSecrets.variables.map((v: any) => ({
376
+ uid: uuid(),
377
+ ...v
378
+ }));
379
+ }
380
+
329
381
  return json;
330
382
  } catch (error) {
331
383
  return Promise.reject(error);
@@ -0,0 +1,63 @@
1
+ const { parseBruEnvironment } = require('../index');
2
+
3
+ describe('parseBruEnvironment external secrets normalization', () => {
4
+ it('should normalize the external secrets block to the app shape with uid and enabled', () => {
5
+ const bru = `vars {
6
+ }
7
+ vars:externalsecrets:hashicorp-vault-server {
8
+ secret: secret/data/secret
9
+ password: secret/data/password
10
+ }
11
+ `;
12
+
13
+ const output = parseBruEnvironment(bru);
14
+
15
+ expect(output.externalSecrets.type).toBe('hashicorp-vault-server');
16
+ expect(output.externalSecrets.variables).toHaveLength(2);
17
+
18
+ output.externalSecrets.variables.forEach((variable) => {
19
+ expect(typeof variable.uid).toBe('string');
20
+ expect(variable.uid).toMatch(/^[a-zA-Z0-9]{21}$/);
21
+ expect(variable.enabled).toBe(true);
22
+ expect(variable).not.toHaveProperty('disabled');
23
+ });
24
+
25
+ expect(output.externalSecrets.variables.map(({ name, path, enabled }) => ({ name, path, enabled }))).toEqual([
26
+ { name: 'secret', path: 'secret/data/secret', enabled: true },
27
+ { name: 'password', path: 'secret/data/password', enabled: true }
28
+ ]);
29
+ });
30
+
31
+ it('should map a disabled external secret entry to enabled: false', () => {
32
+ const bru = `vars:externalsecrets:aws-secrets-manager {
33
+ active: my-active-secret
34
+ ~inactive: my-inactive-secret
35
+ }
36
+ `;
37
+
38
+ const output = parseBruEnvironment(bru);
39
+
40
+ expect(output.externalSecrets.type).toBe('aws-secrets-manager');
41
+ const variables = output.externalSecrets.variables;
42
+ expect(variables.map(({ name, secretName, enabled }) => ({ name, secretName, enabled }))).toEqual([
43
+ { name: 'active', secretName: 'my-active-secret', enabled: true },
44
+ { name: 'inactive', secretName: 'my-inactive-secret', enabled: false }
45
+ ]);
46
+ variables.forEach((variable) => {
47
+ expect(variable).not.toHaveProperty('disabled');
48
+ expect(variable.uid).toMatch(/^[a-zA-Z0-9]{21}$/);
49
+ });
50
+ });
51
+
52
+ it('should leave environments without external secrets unchanged', () => {
53
+ const bru = `vars {
54
+ host: localhost
55
+ }
56
+ `;
57
+
58
+ const output = parseBruEnvironment(bru);
59
+
60
+ expect(output.externalSecrets).toBeUndefined();
61
+ expect(output.variables).toEqual([{ name: 'host', value: 'localhost', enabled: true, secret: false, type: 'text' }]);
62
+ });
63
+ });
@@ -0,0 +1,472 @@
1
+ const { redactLargeBruTextBlocks, restoreRedactedBlocks } = require('../utils/redact-large-text-blocks');
2
+ const { parseBruRequest, parseBruCollection } = require('../index');
3
+
4
+ const parseViaRedaction = (content, parser) => {
5
+ const { skeleton, blocks } = redactLargeBruTextBlocks(content);
6
+ return restoreRedactedBlocks(parser(skeleton), blocks);
7
+ };
8
+
9
+ const toCRLF = (content) => content.replace(/\n/g, '\r\n');
10
+
11
+ const requestBru = `meta {
12
+ name: Redaction Test
13
+ type: http
14
+ seq: 1
15
+ }
16
+
17
+ get {
18
+ url: https://example.com/api
19
+ body: json
20
+ }
21
+
22
+ headers {
23
+ content-type: application/json
24
+ }
25
+
26
+ body:json {
27
+ {
28
+ "hello": "world",
29
+ "nested": {
30
+ "a": 1,
31
+ "b": [1, 2, 3]
32
+ }
33
+ }
34
+ }
35
+
36
+ body:text {
37
+ This is a text body
38
+ spanning multiple lines
39
+ }
40
+
41
+ script:pre-request {
42
+ const x = 1;
43
+ if (x) {
44
+ console.log("hi");
45
+ }
46
+ }
47
+
48
+ script:post-response {
49
+ bru.setVar("y", 2);
50
+ }
51
+
52
+ tests {
53
+ test("ok", function() {
54
+ expect(1).to.equal(1);
55
+ });
56
+ }
57
+
58
+ docs {
59
+ # Title
60
+
61
+ Some **markdown** docs with a fenced block:
62
+
63
+ code line stays indented
64
+ }
65
+ `;
66
+
67
+ const graphqlBru = `meta {
68
+ name: GraphQL Test
69
+ type: graphql
70
+ seq: 1
71
+ }
72
+
73
+ post {
74
+ url: https://example.com/graphql
75
+ body: graphql
76
+ }
77
+
78
+ body:graphql {
79
+ {
80
+ launchesPast {
81
+ launch_site {
82
+ site_name
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ body:graphql:vars {
89
+ {
90
+ "limit": 10
91
+ }
92
+ }
93
+ `;
94
+
95
+ const xmlSparqlBru = `meta {
96
+ name: Others
97
+ type: http
98
+ seq: 1
99
+ }
100
+
101
+ post {
102
+ url: https://example.com
103
+ body: xml
104
+ }
105
+
106
+ body:xml {
107
+ <xml>
108
+ <name>John</name>
109
+ </xml>
110
+ }
111
+
112
+ body:sparql {
113
+ SELECT * WHERE {
114
+ ?s ?p ?o .
115
+ }
116
+ LIMIT 10
117
+ }
118
+ `;
119
+
120
+ const collectionBru = `headers {
121
+ x-trace: enabled
122
+ }
123
+
124
+ script:pre-request {
125
+ bru.setEnvVar("base", "https://example.com");
126
+ }
127
+
128
+ tests {
129
+ test("collection level", function() {
130
+ expect(true).to.equal(true);
131
+ });
132
+ }
133
+
134
+ docs {
135
+ # Collection docs
136
+
137
+ Notes for the whole collection.
138
+ }
139
+ `;
140
+
141
+ const folderBru = `meta {
142
+ name: My Folder
143
+ seq: 2
144
+ }
145
+
146
+ docs {
147
+ # Folder docs
148
+ }
149
+ `;
150
+
151
+ const blankLinesBru = `meta {
152
+ name: Blanks
153
+ type: http
154
+ seq: 1
155
+ }
156
+
157
+ post {
158
+ url: https://example.com
159
+ body: json
160
+ }
161
+
162
+ body:json {
163
+
164
+ {
165
+ "a": 1
166
+ }
167
+
168
+
169
+ }
170
+ `;
171
+
172
+ const appBru = `meta {
173
+ name: App Request
174
+ type: http
175
+ seq: 1
176
+ }
177
+
178
+ get {
179
+ url: https://example.com
180
+ body: none
181
+ auth: none
182
+ }
183
+
184
+ app {
185
+ enabled: true
186
+ code: '''
187
+ <div id="x">hi</div>
188
+ <style>
189
+ body {
190
+ color: red;
191
+ }
192
+ </style>
193
+ <script>
194
+ function go() {
195
+ return { ok: 1 };
196
+ }
197
+ </script>
198
+ '''
199
+ }
200
+
201
+ docs {
202
+ # Docs after the app block
203
+ }
204
+ `;
205
+
206
+ const standaloneAppBru = `meta {
207
+ name: Dashboard
208
+ type: app
209
+ seq: 2
210
+ }
211
+
212
+ app {
213
+ code: '''
214
+ <div id="app">standalone</div>
215
+ '''
216
+ }
217
+ `;
218
+
219
+ const appWithoutCodeBru = `meta {
220
+ name: No Code
221
+ type: http
222
+ seq: 1
223
+ }
224
+
225
+ get {
226
+ url: https://example.com
227
+ body: none
228
+ auth: none
229
+ }
230
+
231
+ app {
232
+ enabled: true
233
+ }
234
+ `;
235
+
236
+ const appEmptyCodeBru = `meta {
237
+ name: Empty Code
238
+ type: http
239
+ seq: 1
240
+ }
241
+
242
+ app {
243
+ enabled: true
244
+ code: '''
245
+ '''
246
+ }
247
+ `;
248
+
249
+ const docsAboutAppCodeBru = `meta {
250
+ name: Docs About App Code
251
+ type: http
252
+ seq: 1
253
+ }
254
+
255
+ get {
256
+ url: https://example.com
257
+ }
258
+
259
+ docs {
260
+ # How the app block works
261
+
262
+ app {
263
+ code: '''
264
+ <div>hi</div>
265
+ '''
266
+ }
267
+ }
268
+ `;
269
+
270
+ const appWithoutCodeThenDocsBru = `meta {
271
+ name: No Code Then Docs
272
+ type: http
273
+ seq: 1
274
+ }
275
+
276
+ get {
277
+ url: https://example.com
278
+ }
279
+
280
+ app {
281
+ enabled: true
282
+ }
283
+
284
+ docs {
285
+ Set the code like this:
286
+
287
+ code: '''
288
+ <div>hi</div>
289
+ '''
290
+ }
291
+ `;
292
+
293
+ describe('redactLargeBruTextBlocks', () => {
294
+ describe('matches a normal parse (ohm oracle)', () => {
295
+ const requestCases = [
296
+ ['request with bodies, scripts, tests, docs', requestBru],
297
+ ['graphql query + variables', graphqlBru],
298
+ ['xml + sparql bodies', xmlSparqlBru],
299
+ ['CRLF request', toCRLF(requestBru)],
300
+ ['leading + trailing blank lines in body', blankLinesBru],
301
+ ['CRLF leading + trailing blank lines', toCRLF(blankLinesBru)],
302
+ ['request-level app code', appBru],
303
+ ['CRLF request-level app code', toCRLF(appBru)],
304
+ ['standalone app item', standaloneAppBru],
305
+ ['app block without code', appWithoutCodeBru],
306
+ ['app block with an empty code value', appEmptyCodeBru],
307
+ ['docs documenting an app code block', docsAboutAppCodeBru],
308
+ ['app block without code followed by docs holding a code pair', appWithoutCodeThenDocsBru]
309
+ ];
310
+
311
+ it.each(requestCases)('%s', (_name, content) => {
312
+ expect(parseViaRedaction(content, parseBruRequest)).toEqual(parseBruRequest(content));
313
+ });
314
+
315
+ const collectionCases = [
316
+ ['collection', collectionBru],
317
+ ['folder', folderBru],
318
+ ['CRLF collection', toCRLF(collectionBru)]
319
+ ];
320
+
321
+ it.each(collectionCases)('%s', (_name, content) => {
322
+ expect(parseViaRedaction(content, parseBruCollection)).toEqual(parseBruCollection(content));
323
+ });
324
+ });
325
+
326
+ it('extracts each large text block and shrinks the skeleton', () => {
327
+ const { skeleton, blocks } = redactLargeBruTextBlocks(requestBru);
328
+ expect(blocks.length).toBe(6);
329
+ expect(skeleton.length).toBeLessThan(requestBru.length);
330
+ blocks.forEach((block) => {
331
+ expect(skeleton).toContain(block.token);
332
+ expect(skeleton).not.toContain(block.value);
333
+ });
334
+ });
335
+
336
+ it('keeps the skeleton bounded regardless of block size', () => {
337
+ const blob = 'x'.repeat(4 * 1024 * 1024);
338
+ const bru = `meta {
339
+ name: Huge
340
+ type: http
341
+ seq: 1
342
+ }
343
+
344
+ post {
345
+ url: https://example.com
346
+ body: json
347
+ }
348
+
349
+ body:json {
350
+ { "blob": "${blob}" }
351
+ }
352
+ `;
353
+ const { skeleton, blocks } = redactLargeBruTextBlocks(bru);
354
+ expect(bru.length).toBeGreaterThan(4 * 1024 * 1024);
355
+ expect(skeleton.length).toBeLessThan(1024);
356
+ expect(blocks[0].value).toContain(blob);
357
+ });
358
+
359
+ it('keeps huge app code out of the skeleton, pair and delimiters intact', () => {
360
+ const blob = 'x'.repeat(4 * 1024 * 1024);
361
+ const bru = appBru.replace('<div id="x">hi</div>', `<div>${blob}</div>`);
362
+
363
+ const { skeleton, blocks } = redactLargeBruTextBlocks(bru);
364
+ expect(skeleton.length).toBeLessThan(1024);
365
+ expect(skeleton).toMatch(/code: '''\n\s+__BRU_REDACTED_TEXT_BLOCK_\w+__\n\s+'''/);
366
+ expect(skeleton).toContain('enabled: true');
367
+ expect(blocks.some((block) => block.value.includes(blob))).toBe(true);
368
+ });
369
+
370
+ it('leaves an app block whose code has no value untouched', () => {
371
+ const { blocks } = redactLargeBruTextBlocks(appEmptyCodeBru);
372
+ expect(blocks).toEqual([]);
373
+ });
374
+
375
+ it('leaves a code pair outside the app block untouched', () => {
376
+ const { blocks } = redactLargeBruTextBlocks(docsAboutAppCodeBru);
377
+ expect(blocks.length).toBe(1);
378
+ expect(blocks[0].value).toContain("code: '''");
379
+ expect(blocks[0].value).toContain('<div>hi</div>');
380
+ expect(blocks[0].value).not.toContain('__BRU_REDACTED_TEXT_BLOCK_');
381
+ });
382
+
383
+ it('leaves a code pair after an app block that has no code untouched', () => {
384
+ const { blocks } = redactLargeBruTextBlocks(appWithoutCodeThenDocsBru);
385
+ expect(blocks.length).toBe(1);
386
+ expect(blocks[0].value).toContain("code: '''");
387
+ expect(blocks[0].value).not.toContain('__BRU_REDACTED_TEXT_BLOCK_');
388
+ });
389
+
390
+ it('leaves dictionary body blocks untouched', () => {
391
+ const multipart = `meta {
392
+ name: Multipart
393
+ type: http
394
+ seq: 1
395
+ }
396
+
397
+ post {
398
+ url: https://example.com
399
+ body: multipartForm
400
+ }
401
+
402
+ body:multipart-form {
403
+ field: value
404
+ }
405
+ `;
406
+ const { blocks } = redactLargeBruTextBlocks(multipart);
407
+ expect(blocks.length).toBe(0);
408
+ expect(parseViaRedaction(multipart, parseBruRequest)).toEqual(parseBruRequest(multipart));
409
+ });
410
+
411
+ it('captures nested braces in content without ending the block early', () => {
412
+ const nested = `body:json {
413
+ {
414
+ "a": {
415
+ "b": [ { "c": 1 } ],
416
+ "d": {}
417
+ }
418
+ }
419
+ }
420
+
421
+ script:pre-request {
422
+ function outer() {
423
+ if (true) {
424
+ return { ok: 1 };
425
+ }
426
+ }
427
+ }
428
+ `;
429
+ const { blocks } = redactLargeBruTextBlocks(nested);
430
+ expect(blocks.length).toBe(2);
431
+ expect(blocks[0].value).toContain('"c": 1');
432
+ expect(blocks[0].value).toContain('"d": {}');
433
+ expect(blocks[1].value).toContain('return { ok: 1 };');
434
+ expect(parseViaRedaction(nested, parseBruRequest)).toEqual(parseBruRequest(nested));
435
+ });
436
+
437
+ it('only redacts blocks at column 0, never indented tags', () => {
438
+ const indented = `docs {
439
+ Nested pseudo-block should be treated as content:
440
+ body:json {
441
+ { "a": 1 }
442
+ }
443
+ }
444
+ `;
445
+ const { blocks } = redactLargeBruTextBlocks(indented);
446
+ expect(blocks.length).toBe(1);
447
+ expect(blocks[0].value).toContain('body:json {');
448
+ });
449
+
450
+ it('returns content unchanged when no large text blocks are present', () => {
451
+ const minimal = `meta {
452
+ name: Bare
453
+ type: http
454
+ seq: 1
455
+ }
456
+
457
+ get {
458
+ url: https://example.com
459
+ }
460
+ `;
461
+ const { skeleton, blocks } = redactLargeBruTextBlocks(minimal);
462
+ expect(blocks).toEqual([]);
463
+ expect(skeleton).toBe(minimal);
464
+ });
465
+ });
466
+
467
+ describe('restoreRedactedBlocks', () => {
468
+ it('returns the parsed object unchanged when there are no blocks', () => {
469
+ const parsed = { request: { body: { json: 'x' } } };
470
+ expect(restoreRedactedBlocks(parsed, [])).toBe(parsed);
471
+ });
472
+ });