@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,6 +8,7 @@ import type { Assertion } from '@opencollection/types/common/assertions';
8
8
  import type { Action } from '@opencollection/types/common/actions';
9
9
  import type { HttpRequestParam, HttpRequestBody } from '@opencollection/types/requests/http';
10
10
  import { stringifyYml } from '../utils';
11
+ import { toOpenCollectionApp, OpenCollectionApp } from '../common/app';
11
12
  import { toOpenCollectionAuth } from '../common/auth';
12
13
  import { toOpenCollectionHttpHeaders, toOpenCollectionResponseHeaders } from '../common/headers';
13
14
  import { toOpenCollectionParams } from '../common/params';
@@ -17,6 +18,7 @@ import { toOpenCollectionActions } from '../common/actions';
17
18
  import { toOpenCollectionScripts } from '../common/scripts';
18
19
  import { toOpenCollectionAssertions } from '../common/assertions';
19
20
  import { isNumber, isNonEmptyString } from '../../../utils';
21
+ import { resolveTimeoutSetting } from '@usebruno/common/utils';
20
22
 
21
23
  const stringifyHttpRequest = (item: BrunoItem): string => {
22
24
  try {
@@ -34,6 +36,9 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
34
36
  if (item.tags?.length) {
35
37
  info.tags = item.tags;
36
38
  }
39
+ if (isNonEmptyString(item.description)) {
40
+ info.description = item.description;
41
+ }
37
42
  ocRequest.info = info;
38
43
 
39
44
  // http block
@@ -116,12 +121,7 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
116
121
  settings.encodeUrl = true;
117
122
  }
118
123
 
119
- const timeout = httpSettings?.timeout;
120
- if (isNumber(timeout)) {
121
- settings.timeout = timeout;
122
- } else {
123
- settings.timeout = 0;
124
- }
124
+ settings.timeout = resolveTimeoutSetting(httpSettings?.timeout);
125
125
 
126
126
  if (httpSettings?.followRedirects === true) {
127
127
  settings.followRedirects = true;
@@ -138,6 +138,8 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
138
138
  settings.maxRedirects = 5;
139
139
  }
140
140
 
141
+ settings.forwardAuthorizationHeader = httpSettings?.forwardAuthorizationHeader ?? true;
142
+
141
143
  ocRequest.settings = settings;
142
144
 
143
145
  // examples
@@ -212,6 +214,12 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
212
214
  ocRequest.docs = brunoRequest.docs;
213
215
  }
214
216
 
217
+ // app
218
+ const app: OpenCollectionApp | undefined = toOpenCollectionApp(item.app);
219
+ if (app) {
220
+ (ocRequest as any).app = app;
221
+ }
222
+
215
223
  return stringifyYml(ocRequest);
216
224
  } catch (error) {
217
225
  console.error('Error stringifying HTTP request:', error);
@@ -1,6 +1,6 @@
1
1
  import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
2
2
  import type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket';
3
- import type { WebSocketRequest, WebSocketMessage, WebSocketRequestInfo, WebSocketRequestDetails, WebSocketRequestRuntime } from '@opencollection/types/requests/websocket';
3
+ import type { WebSocketRequest, WebSocketRequestInfo, WebSocketRequestDetails, WebSocketRequestRuntime, WebSocketMessage, WebSocketMessageVariant } from '@opencollection/types/requests/websocket';
4
4
  import type { Auth } from '@opencollection/types/common/auth';
5
5
  import type { Scripts } from '@opencollection/types/common/scripts';
6
6
  import type { Variable } from '@opencollection/types/common/variables';
@@ -28,6 +28,9 @@ const stringifyWebsocketRequest = (item: BrunoItem): string => {
28
28
  if (item.tags?.length) {
29
29
  info.tags = item.tags;
30
30
  }
31
+ if (isNonEmptyString(item.description)) {
32
+ info.description = item.description;
33
+ }
31
34
  ocRequest.info = info;
32
35
 
33
36
  // websocket block
@@ -41,21 +44,31 @@ const stringifyWebsocketRequest = (item: BrunoItem): string => {
41
44
  websocket.headers = headers;
42
45
  }
43
46
 
44
- // message
47
+ // message: single message without a custom name uses flat WebSocketMessage (backward compatible),
48
+ // otherwise uses WebSocketMessageVariant[] to preserve names
45
49
  if (brunoRequest.body?.mode === 'ws' && brunoRequest.body.ws?.length) {
46
50
  const messages = brunoRequest.body.ws;
51
+ const hasCustomName = messages.length === 1 && messages[0].name && messages[0].name.trim().length > 0;
52
+
53
+ const hasContent = messages.length === 1 && (messages[0].content || '').trim().length > 0;
47
54
 
48
- // todo: bruno app supports only one message for now
49
- // update this when bruno app supports multiple messages
50
- if (messages.length) {
55
+ if (messages.length === 1 && !hasCustomName && hasContent) {
51
56
  const msg = messages[0];
52
57
  const message: WebSocketMessage = {
53
- type: (msg.type as 'text' | 'json' | 'xml' | 'binary') || 'text',
58
+ type: (msg.type as WebSocketMessage['type']) || 'text',
54
59
  data: msg.content || ''
55
60
  };
56
- if (message.data.trim().length) {
57
- websocket.message = message;
58
- }
61
+ websocket.message = message;
62
+ } else {
63
+ const variants: WebSocketMessageVariant[] = messages.map((msg, index) => ({
64
+ title: msg.name || `message ${index + 1}`,
65
+ selected: msg.selected || false,
66
+ message: {
67
+ type: (msg.type as WebSocketMessage['type']) || 'text',
68
+ data: msg.content || ''
69
+ }
70
+ }));
71
+ websocket.message = variants;
59
72
  }
60
73
  }
61
74
 
@@ -0,0 +1,95 @@
1
+ import type { HttpRequestHeader, HttpResponseHeader, HttpRequestParam, HttpRequestBody } from '@opencollection/types/requests/http';
2
+
3
+ export interface MockRuleCondition {
4
+ target?: string;
5
+ key?: string;
6
+ operator?: string;
7
+ value?: string;
8
+ }
9
+
10
+ export interface MockRouteRules {
11
+ operator?: 'AND' | 'OR';
12
+ conditions?: MockRuleCondition[];
13
+ }
14
+
15
+ export interface MockRouteEntry {
16
+ name?: string;
17
+ description?: string;
18
+ request?: {
19
+ method?: string;
20
+ url?: string;
21
+ headers?: HttpRequestHeader[];
22
+ params?: HttpRequestParam[];
23
+ body?: HttpRequestBody;
24
+ };
25
+ response?: {
26
+ status?: number;
27
+ statusText?: string;
28
+ headers?: HttpResponseHeader[];
29
+ body?: {
30
+ type?: string;
31
+ data?: string;
32
+ };
33
+ };
34
+ rules?: MockRouteRules;
35
+ copiedFrom?: {
36
+ example?: string | null;
37
+ requestPath?: string | null;
38
+ };
39
+ }
40
+
41
+ export interface MockServerFile {
42
+ info?: {
43
+ name?: string;
44
+ type?: 'mock';
45
+ };
46
+ mock?: {
47
+ port?: number;
48
+ delay?: number;
49
+ source?: {
50
+ type?: 'collection' | 'spec';
51
+ path?: string;
52
+ };
53
+ };
54
+ routes?: MockRouteEntry[];
55
+ }
56
+
57
+ export interface BrunoMockRoute {
58
+ name: string;
59
+ description: string;
60
+ request: {
61
+ url: string;
62
+ method: string;
63
+ headers: any[];
64
+ params: any[];
65
+ body: any;
66
+ };
67
+ response: {
68
+ status: number;
69
+ statusText: string;
70
+ headers: any[];
71
+ body: {
72
+ type: string;
73
+ content: string;
74
+ };
75
+ };
76
+ rules: {
77
+ operator: 'AND' | 'OR';
78
+ conditions: Array<{ target: string; key: string; operator: string; value: string }>;
79
+ };
80
+ copiedFrom?: {
81
+ exampleName: string | null;
82
+ requestPathname: string | null;
83
+ };
84
+ }
85
+
86
+ export interface BrunoMockServer {
87
+ name: string;
88
+ port: number | null;
89
+ delay: number;
90
+ source: {
91
+ type: 'collection' | 'spec';
92
+ path: string;
93
+ } | null;
94
+ routes: BrunoMockRoute[];
95
+ }
@@ -0,0 +1,193 @@
1
+ import parseCollection from './parseCollection';
2
+
3
+ // Typed `request.variables` propagate through parseCollection into the
4
+ // collectionRoot's vars.req with their dataType + coerced value.
5
+
6
+ describe('parseCollection — typed request.variables', () => {
7
+ it('coerces typed values, preserves raw value on un-coercible input, and treats explicit string as the implicit default', () => {
8
+ const yml = `opencollection: "1.0.0"
9
+ info:
10
+ name: c
11
+
12
+ request:
13
+ variables:
14
+ - name: count
15
+ value:
16
+ type: number
17
+ data: "42"
18
+ - name: enabled
19
+ value:
20
+ type: boolean
21
+ data: "true"
22
+ - name: config
23
+ value:
24
+ type: object
25
+ data: '{"a":1}'
26
+ - name: greeting
27
+ value:
28
+ type: string
29
+ data: hi
30
+ - name: plain
31
+ value: hello
32
+ - name: cfg
33
+ value:
34
+ type: object
35
+ data: 'not-json'
36
+ `;
37
+
38
+ const { collectionRoot } = parseCollection(yml);
39
+ const reqVars = collectionRoot.request!.vars!.req!;
40
+
41
+ expect(reqVars).toHaveLength(6);
42
+ expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
43
+ expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
44
+ expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
45
+ // Explicit `type: string` is the implicit default — no dataType materialized.
46
+ expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
47
+ expect(reqVars[3].dataType).toBeUndefined();
48
+ expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
49
+ expect(reqVars[4].dataType).toBeUndefined();
50
+ // Un-coercible: raw value preserved for the UI mismatch warning.
51
+ expect(reqVars[5]).toMatchObject({ name: 'cfg', value: 'not-json', dataType: 'object' });
52
+ });
53
+ });
54
+
55
+ describe('parseCollection — script execution flow', () => {
56
+ it('reads flow: sequential from the bruno extension', () => {
57
+ const yml = `opencollection: "1.0.0"
58
+ info:
59
+ name: c
60
+ extensions:
61
+ bruno:
62
+ scripts:
63
+ flow: sequential
64
+ `;
65
+ const { brunoConfig } = parseCollection(yml);
66
+ expect(brunoConfig.scripts?.flow).toBe('sequential');
67
+ });
68
+
69
+ it('reads flow: sandwich from the bruno extension', () => {
70
+ const yml = `opencollection: "1.0.0"
71
+ info:
72
+ name: c
73
+ extensions:
74
+ bruno:
75
+ scripts:
76
+ flow: sandwich
77
+ `;
78
+ const { brunoConfig } = parseCollection(yml);
79
+ expect(brunoConfig.scripts?.flow).toBe('sandwich');
80
+ });
81
+
82
+ it('ignores an unrecognized flow value', () => {
83
+ const yml = `opencollection: "1.0.0"
84
+ info:
85
+ name: c
86
+ extensions:
87
+ bruno:
88
+ scripts:
89
+ flow: parallel
90
+ `;
91
+ const { brunoConfig } = parseCollection(yml);
92
+ expect(brunoConfig.scripts?.flow).toBeUndefined();
93
+ });
94
+
95
+ it('has no scripts.flow when the file does not set one', () => {
96
+ const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n');
97
+ expect(brunoConfig.scripts?.flow).toBeUndefined();
98
+ });
99
+ });
100
+
101
+ describe('parseCollection — client certificates', () => {
102
+ it('maps pem and pkcs12 certs to Bruno cert/pfx shapes', () => {
103
+ const yml = `opencollection: "1.0.0"
104
+ info:
105
+ name: c
106
+ config:
107
+ clientCertificates:
108
+ - domain: localhost
109
+ type: pem
110
+ certificateFilePath: ./certs/client-cert.pem
111
+ privateKeyFilePath: ./certs/client-key.pem
112
+ passphrase: secret
113
+ - domain: example.com
114
+ type: pkcs12
115
+ pkcs12FilePath: ./certs/client.pfx
116
+ `;
117
+ const { brunoConfig } = parseCollection(yml);
118
+
119
+ expect(brunoConfig.clientCertificates.certs).toEqual([
120
+ {
121
+ domain: 'localhost',
122
+ type: 'cert',
123
+ certFilePath: './certs/client-cert.pem',
124
+ keyFilePath: './certs/client-key.pem',
125
+ passphrase: 'secret'
126
+ },
127
+ {
128
+ domain: 'example.com',
129
+ type: 'pfx',
130
+ pfxFilePath: './certs/client.pfx',
131
+ passphrase: ''
132
+ }
133
+ ]);
134
+ });
135
+
136
+ it('drops certs with an unrecognized type', () => {
137
+ const yml = `opencollection: "1.0.0"
138
+ info:
139
+ name: c
140
+ config:
141
+ clientCertificates:
142
+ - domain: localhost
143
+ type: bogus
144
+ certificateFilePath: ./certs/client-cert.pem
145
+ `;
146
+ const { brunoConfig } = parseCollection(yml);
147
+
148
+ expect(brunoConfig.clientCertificates.certs).toEqual([]);
149
+ });
150
+
151
+ it('reads a per-cert disabled flag when set to true, otherwise leaves it off', () => {
152
+ const yml = `opencollection: "1.0.0"
153
+ info:
154
+ name: c
155
+ config:
156
+ clientCertificates:
157
+ - domain: localhost
158
+ type: pem
159
+ certificateFilePath: ./certs/client-cert.pem
160
+ privateKeyFilePath: ./certs/client-key.pem
161
+ disabled: true
162
+ - domain: example.com
163
+ type: pkcs12
164
+ pkcs12FilePath: ./certs/client.pfx
165
+ disabled: true
166
+ - domain: other.com
167
+ type: pkcs12
168
+ pkcs12FilePath: ./certs/other.pfx
169
+ `;
170
+ const { brunoConfig } = parseCollection(yml);
171
+
172
+ expect(brunoConfig.clientCertificates.certs[0].disabled).toBe(true);
173
+ expect(brunoConfig.clientCertificates.certs[1].disabled).toBe(true);
174
+ expect(brunoConfig.clientCertificates.certs[2]).not.toHaveProperty('disabled');
175
+ });
176
+ });
177
+
178
+ describe('parseCollection — reading the collection version', () => {
179
+ it('reads the version from the file as text', () => {
180
+ const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n version: v1.0.0\n');
181
+ expect(brunoConfig.version).toBe('v1.0.0');
182
+ });
183
+
184
+ it('turns a number version into text (2 becomes "2")', () => {
185
+ const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n version: 2\n');
186
+ expect(brunoConfig.version).toBe('2');
187
+ });
188
+
189
+ it('has no version when the file does not have one', () => {
190
+ const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n');
191
+ expect(brunoConfig.version).toBeUndefined();
192
+ });
193
+ });
@@ -7,6 +7,7 @@ import { toBrunoVariables } from './common/variables';
7
7
  import { toBrunoPostResponseVariables } from './common/actions';
8
8
  import { toBrunoScripts } from './common/scripts';
9
9
  import { ensureString } from '../../utils';
10
+ import type { BrunoPresetsExtension } from '../../types';
10
11
 
11
12
  interface ParsedCollection {
12
13
  collectionRoot: FolderRoot;
@@ -25,6 +26,10 @@ const parseCollection = (ymlString: string): ParsedCollection => {
25
26
  ignore: []
26
27
  };
27
28
 
29
+ if (oc.info?.version != null && oc.info.version !== '') {
30
+ brunoConfig.version = ensureString(oc.info.version, '');
31
+ }
32
+
28
33
  const brunoExtension = (oc.extensions as any)?.bruno;
29
34
  if (brunoExtension?.ignore && Array.isArray(brunoExtension.ignore)) {
30
35
  brunoConfig.ignore = brunoExtension.ignore;
@@ -32,12 +37,15 @@ const parseCollection = (ymlString: string): ParsedCollection => {
32
37
 
33
38
  // presets
34
39
  if (brunoExtension?.presets) {
35
- const presets = brunoExtension.presets as any;
36
- if (presets.request) {
40
+ const presets = brunoExtension.presets as BrunoPresetsExtension;
41
+ if (presets.request || presets.defaultEnvironment) {
37
42
  brunoConfig.presets = {
38
- requestType: presets.request.type || [],
39
- requestUrl: presets.request.url || []
43
+ requestType: presets.request?.type || '',
44
+ requestUrl: presets.request?.url || ''
40
45
  };
46
+ if (presets.defaultEnvironment) {
47
+ brunoConfig.presets.defaultEnvironment = presets.defaultEnvironment;
48
+ }
41
49
  }
42
50
  }
43
51
 
@@ -54,6 +62,14 @@ const parseCollection = (ymlString: string): ParsedCollection => {
54
62
  };
55
63
  }
56
64
  }
65
+ // scripts.flow controls collection/folder/request script execution order
66
+ // ('sandwich' | 'sequential'); unrecognized values fall back to the runtime default.
67
+ if (brunoExtensions?.scripts?.flow === 'sandwich' || brunoExtensions?.scripts?.flow === 'sequential') {
68
+ brunoConfig.scripts = {
69
+ ...brunoConfig.scripts,
70
+ flow: brunoExtensions.scripts.flow
71
+ };
72
+ }
57
73
  if (Array.isArray(brunoExtensions?.openapi) && brunoExtensions.openapi.length > 0) {
58
74
  brunoConfig.openapi = brunoExtensions.openapi.map((entry: any) => ({
59
75
  sourceUrl: entry.sourceUrl,
@@ -137,14 +153,16 @@ const parseCollection = (ymlString: string): ParsedCollection => {
137
153
  type: 'cert',
138
154
  certFilePath: cert.certificateFilePath,
139
155
  keyFilePath: cert.privateKeyFilePath,
140
- passphrase: cert.passphrase || ''
156
+ passphrase: cert.passphrase || '',
157
+ ...(cert.disabled === true && { disabled: true })
141
158
  };
142
159
  } else if (cert.type === 'pkcs12') {
143
160
  return {
144
161
  domain: cert.domain,
145
162
  type: 'pfx',
146
163
  pfxFilePath: cert.pkcs12FilePath,
147
- passphrase: cert.passphrase || ''
164
+ passphrase: cert.passphrase || '',
165
+ ...(cert.disabled === true && { disabled: true })
148
166
  };
149
167
  }
150
168
  return null;
@@ -0,0 +1,99 @@
1
+ import parseEnvironment, { toBrunoEnvironmentVariables } from './parseEnvironment';
2
+
3
+ describe('toBrunoEnvironmentVariables', () => {
4
+ it('returns an empty array for null / empty input', () => {
5
+ expect(toBrunoEnvironmentVariables(null)).toEqual([]);
6
+ expect(toBrunoEnvironmentVariables(undefined)).toEqual([]);
7
+ expect(toBrunoEnvironmentVariables([])).toEqual([]);
8
+ });
9
+
10
+ it('parses a plain string variable as text with secret=false', () => {
11
+ const [variable] = toBrunoEnvironmentVariables([{ name: 'apiKey', value: 'abc' } as any]);
12
+
13
+ expect(variable).toMatchObject({
14
+ name: 'apiKey',
15
+ value: 'abc',
16
+ type: 'text',
17
+ enabled: true,
18
+ secret: false
19
+ });
20
+ expect(variable.uid).toEqual(expect.any(String));
21
+ expect(variable.dataType).toBeUndefined();
22
+ });
23
+
24
+ it('parses typed values and attaches the dataType field', () => {
25
+ const vars = toBrunoEnvironmentVariables([
26
+ { name: 'port', value: { type: 'number', data: '300' } } as any,
27
+ { name: 'flag', value: { type: 'boolean', data: 'true' } } as any,
28
+ { name: 'config', value: { type: 'object', data: '{"a":1}' } } as any
29
+ ]);
30
+
31
+ expect(vars[0]).toMatchObject({ name: 'port', value: 300, dataType: 'number', secret: false });
32
+ expect(vars[1]).toMatchObject({ name: 'flag', value: true, dataType: 'boolean', secret: false });
33
+ expect(vars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object', secret: false });
34
+ });
35
+
36
+ it('parses secret variables with secret=true and attaches the dataType from `type`', () => {
37
+ const vars = toBrunoEnvironmentVariables([
38
+ { secret: true, name: 'apiKey' } as any,
39
+ { secret: true, name: 'port', type: 'number' } as any,
40
+ { secret: true, name: 'flag', type: 'boolean', disabled: true } as any
41
+ ]);
42
+
43
+ expect(vars[0]).toMatchObject({ name: 'apiKey', secret: true, enabled: true, value: '' });
44
+ // A bare secret has no `type`, so no dataType materializes.
45
+ expect(vars[0].dataType).toBeUndefined();
46
+ expect(vars[1]).toMatchObject({ name: 'port', secret: true, enabled: true, dataType: 'number' });
47
+ expect(vars[2]).toMatchObject({ name: 'flag', secret: true, enabled: false, dataType: 'boolean' });
48
+ });
49
+
50
+ it('honors the disabled flag on non-secret variables', () => {
51
+ const [variable] = toBrunoEnvironmentVariables([
52
+ { name: 'apiKey', value: 'abc', disabled: true } as any
53
+ ]);
54
+
55
+ expect(variable.enabled).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe('parseEnvironment (full yml)', () => {
60
+ it('parses an environment with mixed plain, typed and secret variables', () => {
61
+ const yml = `name: test_env
62
+ variables:
63
+ - name: env_str
64
+ value: hello
65
+ - name: env_num
66
+ value:
67
+ type: number
68
+ data: "300"
69
+ - name: env_bool
70
+ value:
71
+ type: boolean
72
+ data: "true"
73
+ - name: env_obj
74
+ value:
75
+ type: object
76
+ data: '{"scope":"env"}'
77
+ - secret: true
78
+ name: env_secret
79
+ `;
80
+
81
+ const env = parseEnvironment(yml);
82
+
83
+ expect(env.name).toBe('test_env');
84
+ expect(env.variables).toHaveLength(5);
85
+
86
+ expect(env.variables[0]).toMatchObject({ name: 'env_str', value: 'hello', secret: false });
87
+ expect(env.variables[1]).toMatchObject({ name: 'env_num', value: 300, dataType: 'number', secret: false });
88
+ expect(env.variables[2]).toMatchObject({ name: 'env_bool', value: true, dataType: 'boolean', secret: false });
89
+ expect(env.variables[3]).toMatchObject({ name: 'env_obj', value: { scope: 'env' }, dataType: 'object', secret: false });
90
+ expect(env.variables[4]).toMatchObject({ name: 'env_secret', secret: true, value: '' });
91
+ expect(env.variables[4].dataType).toBeUndefined();
92
+ });
93
+
94
+ it('defaults environment name when missing', () => {
95
+ const env = parseEnvironment('variables: []');
96
+ expect(env.name).toBe('Untitled Environment');
97
+ expect(env.variables).toEqual([]);
98
+ });
99
+ });
@@ -1,20 +1,26 @@
1
- import type { Environment as BrunoEnvironment, EnvironmentVariable as BrunoEnvironmentVariable } from '@usebruno/schema-types/collection/environment';
1
+ import type {
2
+ Environment as BrunoEnvironment,
3
+ EnvironmentVariable as BrunoEnvironmentVariable,
4
+ SecretEnvironmentVariable as BrunoSecretEnvironmentVariable,
5
+ ExternalSecretProviderType as BrunoExternalSecretProviderType
6
+ } from '@usebruno/schema-types/collection/environment';
2
7
  import type { Environment } from '@opencollection/types/config/environments';
3
8
  import type { Variable, SecretVariable } from '@opencollection/types/common/variables';
4
9
  import { parseYml } from './utils';
5
10
  import { uuid, ensureString } from '../../utils';
6
- import { isTypedValue } from './common/variables';
11
+ import { SECRET_PROVIDER_IDENTIFIER_FIELDS } from '../../constants';
12
+ import { isTypedValue, fromOpenCollectionTypedValue } from './common/datatype';
7
13
 
8
14
  const isSecretVariable = (v: Variable | SecretVariable): v is SecretVariable => {
9
15
  return 'secret' in v && v.secret === true;
10
16
  };
11
17
 
12
- const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] | null | undefined): BrunoEnvironmentVariable[] => {
18
+ export const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] | null | undefined): (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable)[] => {
13
19
  if (!variables?.length) {
14
20
  return [];
15
21
  }
16
22
 
17
- return variables.map((v): BrunoEnvironmentVariable => {
23
+ return variables.map((v): (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable) => {
18
24
  if (isSecretVariable(v)) {
19
25
  const variable: BrunoEnvironmentVariable = {
20
26
  uid: uuid(),
@@ -26,11 +32,16 @@ const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] |
26
32
  };
27
33
 
28
34
  if (v.type && v.type !== 'string' && v.type !== 'null') {
29
- variable.datatype = v.type;
35
+ variable.dataType = v.type;
36
+ }
37
+
38
+ if (v.description) {
39
+ variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
30
40
  }
31
41
 
32
42
  return variable;
33
43
  }
44
+
34
45
  const variable: BrunoEnvironmentVariable = {
35
46
  uid: uuid(),
36
47
  name: ensureString(v.name),
@@ -41,36 +52,47 @@ const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] |
41
52
  };
42
53
 
43
54
  if (isTypedValue(v.value)) {
44
- variable.value = ensureString(v.value.data);
45
- if (v.value.type !== 'string' && v.value.type !== 'null') {
46
- variable.datatype = v.value.type;
47
- }
55
+ Object.assign(variable, fromOpenCollectionTypedValue(v.value));
48
56
  } else {
49
57
  variable.value = ensureString(v.value);
50
58
  }
51
59
 
60
+ if (v.description) {
61
+ variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
62
+ }
63
+
52
64
  return variable;
53
65
  });
54
66
  };
55
67
 
68
+ // Parses the yml `externalSecrets: { type, variables: [{ name, path|secretName|vaultName }] }`
69
+ // block into the runtime shape. `uid` and `enabled` are added here; the fetched `value`,
70
+ // `providerConfig` and `lastSyncedAt` are runtime-only and never present in the file.
56
71
  const toBrunoExternalSecrets = (externalSecrets: any): BrunoEnvironment['externalSecrets'] | undefined => {
57
72
  if (!externalSecrets || typeof externalSecrets !== 'object') {
58
73
  return undefined;
59
74
  }
60
75
 
76
+ const type = ensureString(externalSecrets.type) as BrunoExternalSecretProviderType;
77
+ const identifierField = SECRET_PROVIDER_IDENTIFIER_FIELDS[type];
78
+ if (!identifierField) {
79
+ return undefined;
80
+ }
81
+
82
+ // Keep any named entry, even one with an empty identifier — a half-filled secret row is the user's
83
+ // data; dropping it here would silently delete it on the next save.
61
84
  const variables = Array.isArray(externalSecrets.variables)
62
- ? externalSecrets.variables.map((variable: any) => {
63
- const result: Record<string, string> = { name: ensureString(variable?.name) };
64
- Object.keys(variable || {}).forEach((key) => {
65
- if (key !== 'name') {
66
- result[key] = ensureString(variable[key]);
67
- }
68
- });
69
- return result;
70
- })
85
+ ? externalSecrets.variables
86
+ .filter((v: any) => v && v.name)
87
+ .map((v: any) => ({
88
+ uid: uuid(),
89
+ name: ensureString(v.name),
90
+ enabled: v.disabled !== true,
91
+ [identifierField]: ensureString(v[identifierField])
92
+ }))
71
93
  : [];
72
94
 
73
- return { type: ensureString(externalSecrets.type), variables } as BrunoEnvironment['externalSecrets'];
95
+ return { type, variables } as BrunoEnvironment['externalSecrets'];
74
96
  };
75
97
 
76
98
  const parseEnvironment = (ymlString: string): BrunoEnvironment => {