@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
@@ -0,0 +1,70 @@
1
+ import parseFolder from './parseFolder';
2
+
3
+ // Typed `request.variables` propagate through parseFolder into the folder's
4
+ // vars.req with their dataType + coerced value.
5
+
6
+ describe('parseFolder — 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 = `info:
9
+ name: my-folder
10
+
11
+ request:
12
+ variables:
13
+ - name: count
14
+ value:
15
+ type: number
16
+ data: "42"
17
+ - name: enabled
18
+ value:
19
+ type: boolean
20
+ data: "true"
21
+ - name: config
22
+ value:
23
+ type: object
24
+ data: '{"a":1}'
25
+ - name: greeting
26
+ value:
27
+ type: string
28
+ data: hi
29
+ - name: plain
30
+ value: hello
31
+ - name: flag
32
+ value:
33
+ type: boolean
34
+ data: 'maybe'
35
+ `;
36
+
37
+ const folder = parseFolder(yml);
38
+ const reqVars = folder.request!.vars!.req!;
39
+
40
+ expect(reqVars).toHaveLength(6);
41
+ expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
42
+ expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
43
+ expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
44
+ // Explicit `type: string` is the implicit default — no dataType materialized.
45
+ expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
46
+ expect(reqVars[3].dataType).toBeUndefined();
47
+ expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
48
+ expect(reqVars[4].dataType).toBeUndefined();
49
+ // Un-coercible: raw value preserved for the UI mismatch warning.
50
+ expect(reqVars[5]).toMatchObject({ name: 'flag', value: 'maybe', dataType: 'boolean' });
51
+ });
52
+ });
53
+
54
+ describe('parseFolder — seq', () => {
55
+ it('leaves seq undefined when folder.yml has no seq field', () => {
56
+ const yml = `info:\n name: my-folder\n`;
57
+ const { meta } = parseFolder(yml);
58
+
59
+ expect(meta).toEqual(expect.objectContaining({ name: 'my-folder' }));
60
+ expect(meta!.seq).toBeUndefined();
61
+ });
62
+
63
+ it('preserves an explicit numeric seq from the file', () => {
64
+ const yml = `info:\n name: my-folder\n seq: 3\n`;
65
+ const { meta } = parseFolder(yml);
66
+
67
+ expect(meta).toEqual(expect.objectContaining({ name: 'my-folder' }));
68
+ expect(meta!.seq).toBe(3);
69
+ });
70
+ });
@@ -13,21 +13,19 @@ const parseFolder = (ymlString: string): FolderRoot => {
13
13
  const ocFolder: Folder = parseYml(ymlString);
14
14
 
15
15
  const info = ocFolder.info;
16
+ const seq = info?.seq;
16
17
 
17
18
  const folderRoot: FolderRoot = {
18
19
  meta: {
19
20
  name: ensureString(info?.name, 'Untitled Folder'),
20
- seq: info?.seq || 1
21
+ // Only set seq when the source has a numeric value. Missing seq must stay absent:
22
+ // defaulting to 1 makes every seq-less folder look "ordered at position 1" to
23
+ // sortByNameThenSequence, pinning them all to slot 1 instead of alphabetical sort.
24
+ ...(typeof seq === 'number' && Number.isFinite(seq) ? { seq } : {})
21
25
  },
22
- request: null,
23
- docs: null
24
- };
25
-
26
- // request defaults
27
- if (ocFolder.request) {
28
- folderRoot.request = {
26
+ request: {
29
27
  headers: [],
30
- auth: null,
28
+ auth: toBrunoAuth(ocFolder.request?.auth),
31
29
  script: {
32
30
  req: null,
33
31
  res: null
@@ -37,40 +35,39 @@ const parseFolder = (ymlString: string): FolderRoot => {
37
35
  res: []
38
36
  },
39
37
  tests: null
40
- };
38
+ },
39
+ docs: null
40
+ };
41
+
42
+ if (ocFolder.request) {
43
+ const folderRequest = folderRoot.request!;
41
44
 
42
45
  // headers
43
46
  const headers = toBrunoHttpHeaders(ocFolder.request.headers);
44
47
  if (headers) {
45
- folderRoot.request.headers = headers;
46
- }
47
-
48
- // auth
49
- const auth = toBrunoAuth(ocFolder.request.auth);
50
- if (auth) {
51
- folderRoot.request.auth = auth;
48
+ folderRequest.headers = headers;
52
49
  }
53
50
 
54
51
  // variables
55
52
  const variables = toBrunoVariables(ocFolder.request.variables);
56
53
  const postResponseVars = toBrunoPostResponseVariables((ocFolder.request as any).actions);
57
- folderRoot.request.vars = {
54
+ folderRequest.vars = {
58
55
  req: variables.req,
59
56
  res: postResponseVars
60
57
  };
61
58
 
62
59
  // scripts
63
60
  const scripts = toBrunoScripts(ocFolder.request.scripts);
64
- if (scripts?.script && folderRoot.request.script) {
61
+ if (scripts?.script && folderRequest.script) {
65
62
  if (scripts.script.req) {
66
- folderRoot.request.script.req = scripts.script.req;
63
+ folderRequest.script.req = scripts.script.req;
67
64
  }
68
65
  if (scripts.script.res) {
69
- folderRoot.request.script.res = scripts.script.res;
66
+ folderRequest.script.res = scripts.script.res;
70
67
  }
71
68
  }
72
69
  if (scripts?.tests) {
73
- folderRoot.request.tests = scripts.tests;
70
+ folderRequest.tests = scripts.tests;
74
71
  }
75
72
  }
76
73
 
@@ -0,0 +1,147 @@
1
+ import parseItem from './parseItem';
2
+
3
+ // Typed `runtime.variables` propagate through parseItem with their dataType
4
+ // + coerced value. Helper-level coverage lives in the variables/datatype specs.
5
+
6
+ describe('parseItem — typed runtime.variables', () => {
7
+ it('coerces typed values, preserves raw value on un-coercible input, and treats explicit string as the implicit default', () => {
8
+ const yml = `info:
9
+ name: r
10
+ type: http
11
+
12
+ http:
13
+ url: https://example.com
14
+ method: GET
15
+
16
+ runtime:
17
+ variables:
18
+ - name: count
19
+ value:
20
+ type: number
21
+ data: "42"
22
+ - name: enabled
23
+ value:
24
+ type: boolean
25
+ data: "true"
26
+ - name: config
27
+ value:
28
+ type: object
29
+ data: '{"a":1}'
30
+ - name: greeting
31
+ value:
32
+ type: string
33
+ data: hi
34
+ - name: plain
35
+ value: hello
36
+ - name: bad
37
+ value:
38
+ type: number
39
+ data: 'not-a-number'
40
+ `;
41
+
42
+ const item = parseItem(yml);
43
+ const reqVars = item.request!.vars!.req!;
44
+
45
+ expect(reqVars).toHaveLength(6);
46
+ expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
47
+ expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
48
+ expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
49
+ // Explicit `type: string` is the implicit default — no dataType materialized.
50
+ expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
51
+ expect(reqVars[3].dataType).toBeUndefined();
52
+ expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
53
+ expect(reqVars[4].dataType).toBeUndefined();
54
+ // Un-coercible: raw value preserved for the UI mismatch warning.
55
+ expect(reqVars[5]).toMatchObject({ name: 'bad', value: 'not-a-number', dataType: 'number' });
56
+ });
57
+
58
+ it('normalizes raw YAML scalars in `data` to a string before coercing', () => {
59
+ // YAML parses unquoted scalars per their natural JS type. The `String(... ?? '')`
60
+ // cast in fromOpenCollectionTypedValue keeps coercion consistent.
61
+ const yml = `info:
62
+ name: r
63
+ type: http
64
+
65
+ http:
66
+ url: https://example.com
67
+ method: GET
68
+
69
+ runtime:
70
+ variables:
71
+ - name: stringy
72
+ value:
73
+ type: string
74
+ data: 42
75
+ - name: numeric
76
+ value:
77
+ type: number
78
+ data: 42
79
+ - name: nullish
80
+ value:
81
+ type: number
82
+ data: null
83
+ `;
84
+
85
+ const item = parseItem(yml);
86
+ const reqVars = item.request!.vars!.req!;
87
+
88
+ // type=string: raw YAML number → string; no dataType field.
89
+ expect(reqVars[0]).toMatchObject({ name: 'stringy', value: '42' });
90
+ expect(reqVars[0].dataType).toBeUndefined();
91
+ // type=number: '42' → 42.
92
+ expect(reqVars[1]).toMatchObject({ name: 'numeric', value: 42, dataType: 'number' });
93
+ // data: null → '' (the `?? ''` arm); coerce bails on empty → raw ''.
94
+ expect(reqVars[2]).toMatchObject({ name: 'nullish', value: '', dataType: 'number' });
95
+ });
96
+
97
+ it('propagates typed variables for graphql/grpc/websocket requests too', () => {
98
+ const graphqlYml = `info:
99
+ name: g
100
+ type: graphql
101
+
102
+ graphql:
103
+ url: https://example.com/graphql
104
+ method: POST
105
+
106
+ runtime:
107
+ variables:
108
+ - name: count
109
+ value: { type: number, data: '7' }
110
+ `;
111
+ expect(parseItem(graphqlYml).request!.vars!.req![0]).toMatchObject({
112
+ name: 'count', value: 7, dataType: 'number'
113
+ });
114
+
115
+ const grpcYml = `info:
116
+ name: gr
117
+ type: grpc
118
+
119
+ grpc:
120
+ url: localhost:50051
121
+
122
+ runtime:
123
+ variables:
124
+ - name: flag
125
+ value: { type: boolean, data: 'false' }
126
+ `;
127
+ expect(parseItem(grpcYml).request!.vars!.req![0]).toMatchObject({
128
+ name: 'flag', value: false, dataType: 'boolean'
129
+ });
130
+
131
+ const wsYml = `info:
132
+ name: ws
133
+ type: websocket
134
+
135
+ websocket:
136
+ url: wss://example.com/socket
137
+
138
+ runtime:
139
+ variables:
140
+ - name: payload
141
+ value: { type: object, data: '{"k":1}' }
142
+ `;
143
+ expect(parseItem(wsYml).request!.vars!.req![0]).toMatchObject({
144
+ name: 'payload', value: { k: 1 }, dataType: 'object'
145
+ });
146
+ });
147
+ });
@@ -10,6 +10,7 @@ import parseGraphQLRequest from './items/parseGraphQLRequest';
10
10
  import parseGrpcRequest from './items/parseGrpcRequest';
11
11
  import parseWebsocketRequest from './items/parseWebsocketRequest';
12
12
  import parseScript from './items/parseScript';
13
+ import parseApp, { type AppFile } from './items/parseApp';
13
14
 
14
15
  // Helper to get the type from an item (now in info block)
15
16
  const getItemType = (item: Item): string | undefined => {
@@ -87,6 +88,9 @@ const parseItem = (ymlString: string): BrunoItem => {
87
88
  case 'script':
88
89
  return parseScript(ocItem as ScriptFile);
89
90
 
91
+ case 'app':
92
+ return parseApp(ocItem as unknown as AppFile);
93
+
90
94
  case 'folder':
91
95
  throw new Error('Folder items should be handled separately using parseFolder');
92
96
 
@@ -0,0 +1,180 @@
1
+ import parseMockServer from './parseMockServer';
2
+
3
+ describe('parseMockServer', () => {
4
+ it('parses a full mock server file', () => {
5
+ const content = `
6
+ info:
7
+ name: Dog API Mock
8
+ type: mock
9
+
10
+ mock:
11
+ port: 4001
12
+ delay: 250
13
+ source:
14
+ type: collection
15
+ path: /Users/x/collections/dog-api
16
+
17
+ routes:
18
+ - name: Get user 200
19
+ description: Returns a user
20
+ request:
21
+ method: GET
22
+ url: /users/:id
23
+ headers:
24
+ - name: X-Client
25
+ value: bruno
26
+ response:
27
+ status: 200
28
+ statusText: OK
29
+ headers:
30
+ - name: Content-Type
31
+ value: application/json
32
+ body:
33
+ type: json
34
+ data: |-
35
+ {"id": 1}
36
+ rules:
37
+ operator: OR
38
+ conditions:
39
+ - target: query
40
+ key: token
41
+ operator: equals
42
+ value: abc
43
+ copiedFrom:
44
+ example: Success
45
+ requestPath: /Users/x/collections/dog-api/get-user.yml
46
+ `;
47
+
48
+ const mockServer = parseMockServer(content);
49
+
50
+ expect(mockServer.name).toBe('Dog API Mock');
51
+ expect(mockServer.port).toBe(4001);
52
+ expect(mockServer.delay).toBe(250);
53
+ expect(mockServer.source).toEqual({ type: 'collection', path: '/Users/x/collections/dog-api' });
54
+
55
+ expect(mockServer.routes).toHaveLength(1);
56
+ const route = mockServer.routes[0];
57
+ expect(route.name).toBe('Get user 200');
58
+ expect(route.request.method).toBe('GET');
59
+ expect(route.request.url).toBe('/users/:id');
60
+ expect(route.request.headers[0]).toEqual(
61
+ expect.objectContaining({ name: 'X-Client', value: 'bruno', enabled: true })
62
+ );
63
+ expect(route.request.headers[0].uid).toBeTruthy();
64
+ expect(route.response.status).toBe(200);
65
+ expect(route.response.body).toEqual({ type: 'json', content: '{"id": 1}' });
66
+ expect(route.rules.operator).toBe('OR');
67
+ expect(route.rules.conditions).toEqual([
68
+ { target: 'query', key: 'token', operator: 'equals', value: 'abc' }
69
+ ]);
70
+ expect(route.copiedFrom).toEqual({
71
+ exampleName: 'Success',
72
+ requestPathname: '/Users/x/collections/dog-api/get-user.yml'
73
+ });
74
+ });
75
+
76
+ it('applies safe defaults for a minimal file', () => {
77
+ const mockServer = parseMockServer('info:\n name: Tiny Mock\n');
78
+
79
+ expect(mockServer).toEqual({
80
+ name: 'Tiny Mock',
81
+ port: null,
82
+ delay: 0,
83
+ source: null,
84
+ routes: []
85
+ });
86
+ });
87
+
88
+ it('defaults route request/response blocks that are missing', () => {
89
+ const mockServer = parseMockServer(`
90
+ info:
91
+ name: Mock
92
+
93
+ routes:
94
+ - name: Bare route
95
+ `);
96
+
97
+ const route = mockServer.routes[0];
98
+ expect(route.request.url).toBe('/');
99
+ expect(route.request.method).toBe('GET');
100
+ expect(route.request.body.mode).toBe('none');
101
+ expect(route.response.status).toBe(200);
102
+ expect(route.response.body).toEqual({ type: 'json', content: '' });
103
+ expect(route.rules).toEqual({ operator: 'AND', conditions: [] });
104
+ expect(route.copiedFrom).toBeUndefined();
105
+ });
106
+
107
+ it('ignores a source block without a path', () => {
108
+ const mockServer = parseMockServer(`
109
+ info:
110
+ name: Mock
111
+
112
+ mock:
113
+ source:
114
+ type: collection
115
+ `);
116
+
117
+ expect(mockServer.source).toBeNull();
118
+ });
119
+
120
+ it('rejects non-string source paths', () => {
121
+ const numericPath = parseMockServer(`
122
+ info:
123
+ name: Mock
124
+
125
+ mock:
126
+ source:
127
+ type: collection
128
+ path: 12345
129
+ `);
130
+ expect(numericPath.source).toBeNull();
131
+
132
+ const objectPath = parseMockServer(`
133
+ info:
134
+ name: Mock
135
+
136
+ mock:
137
+ source:
138
+ type: spec
139
+ path:
140
+ nested: value
141
+ `);
142
+ expect(objectPath.source).toBeNull();
143
+ });
144
+
145
+ it('rejects non-string copiedFrom values', () => {
146
+ const mockServer = parseMockServer(`
147
+ info:
148
+ name: Mock
149
+
150
+ routes:
151
+ - name: Numeric example
152
+ copiedFrom:
153
+ example: 42
154
+ requestPath: /real/path.yml
155
+ - name: Object request path
156
+ copiedFrom:
157
+ example: Success
158
+ requestPath:
159
+ nested: value
160
+ - name: Fully malformed
161
+ copiedFrom:
162
+ example: 1
163
+ requestPath: 2
164
+ `);
165
+
166
+ expect(mockServer.routes[0].copiedFrom).toEqual({
167
+ exampleName: null,
168
+ requestPathname: '/real/path.yml'
169
+ });
170
+ expect(mockServer.routes[1].copiedFrom).toEqual({
171
+ exampleName: 'Success',
172
+ requestPathname: null
173
+ });
174
+ expect(mockServer.routes[2].copiedFrom).toBeUndefined();
175
+ });
176
+
177
+ it('throws on non-object content', () => {
178
+ expect(() => parseMockServer('- a\n- b\n')).toThrow('Invalid mock server file');
179
+ });
180
+ });
@@ -0,0 +1,113 @@
1
+ import { parseYml } from './utils';
2
+ import { ensureString, isNonEmptyString } from '../../utils';
3
+ import { toBrunoHttpHeaders } from './common/headers';
4
+ import { toBrunoParams } from './common/params';
5
+ import { toBrunoBody } from './common/body';
6
+ import type {
7
+ MockServerFile,
8
+ MockRouteEntry,
9
+ BrunoMockServer,
10
+ BrunoMockRoute,
11
+ MockRuleCondition
12
+ } from './mockServerTypes';
13
+
14
+ const emptyRequestBody = () => ({
15
+ mode: 'none',
16
+ json: null,
17
+ text: null,
18
+ xml: null,
19
+ sparql: null,
20
+ formUrlEncoded: null,
21
+ multipartForm: null,
22
+ graphql: null,
23
+ file: null
24
+ });
25
+
26
+ const toBrunoRuleConditions = (conditions: MockRuleCondition[] | null | undefined) => {
27
+ if (!Array.isArray(conditions)) {
28
+ return [];
29
+ }
30
+
31
+ return conditions.map((condition) => ({
32
+ target: ensureString(condition?.target),
33
+ key: ensureString(condition?.key),
34
+ operator: ensureString(condition?.operator, 'equals'),
35
+ value: ensureString(condition?.value)
36
+ }));
37
+ };
38
+
39
+ const toBrunoMockRoute = (route: MockRouteEntry): BrunoMockRoute => {
40
+ const responseStatus = Number(route?.response?.status) || 200;
41
+ const responseStatusText = ensureString(route?.response?.statusText);
42
+
43
+ const brunoRoute: BrunoMockRoute = {
44
+ name: ensureString(route?.name, 'Untitled Mock Response'),
45
+ description: ensureString(route?.description),
46
+ request: {
47
+ url: ensureString(route?.request?.url, '/'),
48
+ method: ensureString(route?.request?.method, 'GET'),
49
+ headers: toBrunoHttpHeaders(route?.request?.headers) || [],
50
+ params: toBrunoParams(route?.request?.params) || [],
51
+ body: toBrunoBody(route?.request?.body) || emptyRequestBody()
52
+ },
53
+ response: {
54
+ status: responseStatus,
55
+ statusText: responseStatusText === 'OK' && responseStatus !== 200 ? '' : responseStatusText,
56
+ headers: toBrunoHttpHeaders(route?.response?.headers) || [],
57
+ body: {
58
+ type: ensureString(route?.response?.body?.type, 'json'),
59
+ content: ensureString(route?.response?.body?.data)
60
+ }
61
+ },
62
+ rules: {
63
+ operator: route?.rules?.operator === 'OR' ? 'OR' : 'AND',
64
+ conditions: toBrunoRuleConditions(route?.rules?.conditions)
65
+ }
66
+ };
67
+
68
+ const rawExample = route?.copiedFrom?.example;
69
+ const rawRequestPath = route?.copiedFrom?.requestPath;
70
+ const copiedFromExample = isNonEmptyString(rawExample) ? rawExample : null;
71
+ const copiedFromRequestPath = isNonEmptyString(rawRequestPath) ? rawRequestPath : null;
72
+
73
+ if (copiedFromExample || copiedFromRequestPath) {
74
+ brunoRoute.copiedFrom = {
75
+ exampleName: copiedFromExample,
76
+ requestPathname: copiedFromRequestPath
77
+ };
78
+ }
79
+
80
+ return brunoRoute;
81
+ };
82
+
83
+ const parseMockServer = (content: string): BrunoMockServer => {
84
+ const parsed: MockServerFile = parseYml(content);
85
+
86
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
87
+ throw new Error('Invalid mock server file');
88
+ }
89
+
90
+ const info = parsed.info || ({} as NonNullable<MockServerFile['info']>);
91
+ const mock = parsed.mock || ({} as NonNullable<MockServerFile['mock']>);
92
+
93
+ const brunoMockServer: BrunoMockServer = {
94
+ name: ensureString(info.name, 'Mock Server'),
95
+ port: Number(mock.port) || null,
96
+ delay: Number(mock.delay) || 0,
97
+ source: null,
98
+ routes: Array.isArray(parsed.routes) ? parsed.routes.map(toBrunoMockRoute) : []
99
+ };
100
+
101
+ const sourceType = mock.source?.type;
102
+ const sourcePath = mock.source?.path;
103
+ if ((sourceType === 'collection' || sourceType === 'spec') && isNonEmptyString(sourcePath)) {
104
+ brunoMockServer.source = {
105
+ type: sourceType,
106
+ path: sourcePath
107
+ };
108
+ }
109
+
110
+ return brunoMockServer;
111
+ };
112
+
113
+ export default parseMockServer;