@usebruno/filestore 0.11.0 → 0.13.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 (88) 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 +1 -1
  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/utils/redact-large-text-blocks.d.ts.map +1 -1
  12. package/dist/formats/yml/common/omit-headers.d.ts +3 -0
  13. package/dist/formats/yml/common/omit-headers.d.ts.map +1 -0
  14. package/dist/formats/yml/common/scripts.d.ts +3 -5
  15. package/dist/formats/yml/common/scripts.d.ts.map +1 -1
  16. package/dist/formats/yml/index.d.ts +3 -1
  17. package/dist/formats/yml/index.d.ts.map +1 -1
  18. package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
  19. package/dist/formats/yml/items/parseGrpcRequest.d.ts.map +1 -1
  20. package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
  21. package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
  22. package/dist/formats/yml/items/stringifyGrpcRequest.d.ts.map +1 -1
  23. package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
  24. package/dist/formats/yml/items/stringifyWebsocketRequest.d.ts.map +1 -1
  25. package/dist/formats/yml/mockServerTypes.d.ts +95 -0
  26. package/dist/formats/yml/mockServerTypes.d.ts.map +1 -0
  27. package/dist/formats/yml/parseCollection.d.ts.map +1 -1
  28. package/dist/formats/yml/parseEnvironment.d.ts.map +1 -1
  29. package/dist/formats/yml/parseFolder.d.ts.map +1 -1
  30. package/dist/formats/yml/parseMockServer.d.ts +4 -0
  31. package/dist/formats/yml/parseMockServer.d.ts.map +1 -0
  32. package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
  33. package/dist/formats/yml/stringifyEnvironment.d.ts.map +1 -1
  34. package/dist/formats/yml/stringifyFolder.d.ts.map +1 -1
  35. package/dist/formats/yml/stringifyMockServer.d.ts +4 -0
  36. package/dist/formats/yml/stringifyMockServer.d.ts.map +1 -0
  37. package/dist/index.d.ts +14 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/types.d.ts +1 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/workers/WorkerQueue/index.d.ts +3 -2
  42. package/dist/workers/WorkerQueue/index.d.ts.map +1 -1
  43. package/dist/workers/index.d.ts +4 -0
  44. package/dist/workers/index.d.ts.map +1 -1
  45. package/package.json +8 -8
  46. package/src/constants.ts +5 -1
  47. package/src/formats/bru/tests/redact-large-text-blocks.spec.js +220 -1
  48. package/src/formats/bru/utils/redact-large-text-blocks.ts +46 -1
  49. package/src/formats/yml/common/omit-headers.spec.ts +16 -0
  50. package/src/formats/yml/common/omit-headers.ts +12 -0
  51. package/src/formats/yml/common/scripts.spec.ts +155 -0
  52. package/src/formats/yml/common/scripts.ts +55 -29
  53. package/src/formats/yml/grpc-round-trip.spec.ts +110 -0
  54. package/src/formats/yml/index.ts +5 -1
  55. package/src/formats/yml/items/parseGraphQLRequest.ts +6 -16
  56. package/src/formats/yml/items/parseGrpcRequest.ts +14 -6
  57. package/src/formats/yml/items/parseHttpRequest.ts +10 -14
  58. package/src/formats/yml/items/stringifyGraphQLRequest.ts +8 -14
  59. package/src/formats/yml/items/stringifyGrpcRequest.ts +2 -1
  60. package/src/formats/yml/items/stringifyHttpRequest.ts +13 -13
  61. package/src/formats/yml/items/stringifyWebsocketRequest.ts +3 -1
  62. package/src/formats/yml/mockServerTypes.ts +95 -0
  63. package/src/formats/yml/parseCollection.spec.ts +123 -0
  64. package/src/formats/yml/parseCollection.ts +23 -14
  65. package/src/formats/yml/parseEnvironment.spec.ts +49 -0
  66. package/src/formats/yml/parseEnvironment.ts +6 -0
  67. package/src/formats/yml/parseFolder.spec.ts +18 -0
  68. package/src/formats/yml/parseFolder.ts +5 -1
  69. package/src/formats/yml/parseMockServer.spec.ts +180 -0
  70. package/src/formats/yml/parseMockServer.ts +113 -0
  71. package/src/formats/yml/script.spec.ts +159 -0
  72. package/src/formats/yml/stringifyCollection.spec.ts +146 -0
  73. package/src/formats/yml/stringifyCollection.ts +29 -19
  74. package/src/formats/yml/stringifyEnvironment.spec.ts +58 -0
  75. package/src/formats/yml/stringifyEnvironment.ts +6 -0
  76. package/src/formats/yml/stringifyFolder.spec.ts +23 -0
  77. package/src/formats/yml/stringifyFolder.ts +10 -3
  78. package/src/formats/yml/stringifyMockServer.spec.ts +177 -0
  79. package/src/formats/yml/stringifyMockServer.ts +129 -0
  80. package/src/formats/yml/tests/defaultEnvironment.spec.js +50 -0
  81. package/src/formats/yml/tests/stringifyEnvironment.spec.ts +26 -0
  82. package/src/formats/yml/utils.ts +1 -1
  83. package/src/index.ts +34 -1
  84. package/src/maxRedirects.spec.ts +144 -0
  85. package/src/types.ts +1 -0
  86. package/src/workers/WorkerQueue/index.ts +10 -2
  87. package/src/workers/index.ts +25 -1
  88. package/src/workers/worker-script.ts +45 -21
@@ -1,5 +1,6 @@
1
1
  import type { OpenCollection } from '@opencollection/types';
2
2
  import type { FolderRoot } from '@usebruno/schema-types/collection/folder';
3
+ import { normalizeOpenApiSyncConfigs } from '@usebruno/common';
3
4
  import { parseYml } from './utils';
4
5
  import { toBrunoAuth } from './common/auth';
5
6
  import { toBrunoHttpHeaders } from './common/headers';
@@ -38,11 +39,14 @@ const parseCollection = (ymlString: string): ParsedCollection => {
38
39
  // presets
39
40
  if (brunoExtension?.presets) {
40
41
  const presets = brunoExtension.presets as BrunoPresetsExtension;
41
- if (presets.request) {
42
+ if (presets.request || presets.defaultEnvironment) {
42
43
  brunoConfig.presets = {
43
- requestType: presets.request.type || '',
44
- requestUrl: presets.request.url || ''
44
+ requestType: presets.request?.type || '',
45
+ requestUrl: presets.request?.url || ''
45
46
  };
47
+ if (presets.defaultEnvironment) {
48
+ brunoConfig.presets.defaultEnvironment = presets.defaultEnvironment;
49
+ }
46
50
  }
47
51
  }
48
52
 
@@ -59,15 +63,18 @@ const parseCollection = (ymlString: string): ParsedCollection => {
59
63
  };
60
64
  }
61
65
  }
62
- if (Array.isArray(brunoExtensions?.openapi) && brunoExtensions.openapi.length > 0) {
63
- brunoConfig.openapi = brunoExtensions.openapi.map((entry: any) => ({
64
- sourceUrl: entry.sourceUrl,
65
- groupBy: entry.groupBy,
66
- ...(entry.lastSyncDate && { lastSyncDate: entry.lastSyncDate }),
67
- ...(entry.specHash && { specHash: entry.specHash }),
68
- autoCheck: entry.autoCheck !== false,
69
- autoCheckInterval: entry.autoCheckInterval || 5
70
- }));
66
+ // scripts.flow controls collection/folder/request script execution order
67
+ // ('sandwich' | 'sequential'); unrecognized values fall back to the runtime default.
68
+ if (brunoExtensions?.scripts?.flow === 'sandwich' || brunoExtensions?.scripts?.flow === 'sequential') {
69
+ brunoConfig.scripts = {
70
+ ...brunoConfig.scripts,
71
+ flow: brunoExtensions.scripts.flow
72
+ };
73
+ }
74
+
75
+ const openApiEntries = normalizeOpenApiSyncConfigs(brunoExtensions?.openapi);
76
+ if (openApiEntries.length > 0) {
77
+ brunoConfig.openapi = openApiEntries;
71
78
  }
72
79
 
73
80
  // protobuf
@@ -142,14 +149,16 @@ const parseCollection = (ymlString: string): ParsedCollection => {
142
149
  type: 'cert',
143
150
  certFilePath: cert.certificateFilePath,
144
151
  keyFilePath: cert.privateKeyFilePath,
145
- passphrase: cert.passphrase || ''
152
+ passphrase: cert.passphrase || '',
153
+ ...(cert.disabled === true && { disabled: true })
146
154
  };
147
155
  } else if (cert.type === 'pkcs12') {
148
156
  return {
149
157
  domain: cert.domain,
150
158
  type: 'pfx',
151
159
  pfxFilePath: cert.pkcs12FilePath,
152
- passphrase: cert.passphrase || ''
160
+ passphrase: cert.passphrase || '',
161
+ ...(cert.disabled === true && { disabled: true })
153
162
  };
154
163
  }
155
164
  return null;
@@ -96,4 +96,53 @@ variables:
96
96
  expect(env.name).toBe('Untitled Environment');
97
97
  expect(env.variables).toEqual([]);
98
98
  });
99
+
100
+ describe('extends', () => {
101
+ it('parses the parent environment name', () => {
102
+ const env = parseEnvironment(`name: prod
103
+ extends: base
104
+ variables: []
105
+ `);
106
+
107
+ expect(env.extends).toBe('base');
108
+ });
109
+
110
+ it('leaves extends absent for an environment that does not declare it', () => {
111
+ const env = parseEnvironment(`name: prod
112
+ variables: []
113
+ `);
114
+
115
+ expect(env).not.toHaveProperty('extends');
116
+ });
117
+
118
+ it('ignores a parent reference that names nothing', () => {
119
+ const env = parseEnvironment(`name: prod
120
+ extends: " "
121
+ variables: []
122
+ `);
123
+
124
+ expect(env).not.toHaveProperty('extends');
125
+ });
126
+
127
+ it('parses a list of parents unresolved so that a save does not delete it', () => {
128
+ const env = parseEnvironment(`name: prod
129
+ extends:
130
+ - base
131
+ - shared
132
+ variables: []
133
+ `);
134
+
135
+ expect(env.extends).toEqual(['base', 'shared']);
136
+ });
137
+
138
+ it('ignores a parent reference that is neither a name nor a list', () => {
139
+ const env = parseEnvironment(`name: prod
140
+ extends:
141
+ parent: base
142
+ variables: []
143
+ `);
144
+
145
+ expect(env).not.toHaveProperty('extends');
146
+ });
147
+ });
99
148
  });
@@ -10,6 +10,7 @@ import { parseYml } from './utils';
10
10
  import { uuid, ensureString } from '../../utils';
11
11
  import { SECRET_PROVIDER_IDENTIFIER_FIELDS } from '../../constants';
12
12
  import { isTypedValue, fromOpenCollectionTypedValue } from './common/datatype';
13
+ import { validatedEnvironmentExtendsFrom } from '@usebruno/common/utils';
13
14
 
14
15
  const isSecretVariable = (v: Variable | SecretVariable): v is SecretVariable => {
15
16
  return 'secret' in v && v.secret === true;
@@ -106,6 +107,11 @@ const parseEnvironment = (ymlString: string): BrunoEnvironment => {
106
107
  color: ocEnvironment.color || null
107
108
  };
108
109
 
110
+ const ocEnvironmentExtendsFrom = validatedEnvironmentExtendsFrom(ocEnvironment.extends);
111
+ if (ocEnvironmentExtendsFrom) {
112
+ brunoEnvironment.extends = ocEnvironmentExtendsFrom;
113
+ }
114
+
109
115
  const externalSecrets = toBrunoExternalSecrets((ocEnvironment as any).externalSecrets);
110
116
  if (externalSecrets) {
111
117
  brunoEnvironment.externalSecrets = externalSecrets;
@@ -50,3 +50,21 @@ request:
50
50
  expect(reqVars[5]).toMatchObject({ name: 'flag', value: 'maybe', dataType: 'boolean' });
51
51
  });
52
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,11 +13,15 @@ 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
26
  request: {
23
27
  headers: [],
@@ -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;
@@ -0,0 +1,159 @@
1
+ import stringifyItem from './stringifyItem';
2
+ import parseItem from './parseItem';
3
+ import stringifyCollection from './stringifyCollection';
4
+ import parseCollection from './parseCollection';
5
+ import stringifyFolder from './stringifyFolder';
6
+ import parseFolder from './parseFolder';
7
+
8
+ const GRPC_SLOTS = {
9
+ beforeCallStart: 'before()',
10
+ afterCallEnd: 'after()',
11
+ beforeMessageSend: 'beforeSend()',
12
+ afterMessageReceive: 'afterReceive()'
13
+ };
14
+
15
+ const ALL_SLOTS = {
16
+ req: 'pre()',
17
+ res: 'post()',
18
+ ...GRPC_SLOTS
19
+ };
20
+
21
+ const HTTP_TYPES = ['before-request', 'after-response'];
22
+ const GRPC_TYPES = [
23
+ 'grpc:before-call-start',
24
+ 'grpc:after-call-end',
25
+ 'grpc:before-message-send',
26
+ 'grpc:after-message-receive'
27
+ ];
28
+
29
+ const expectScriptTypes = (yml: string, written: string[], dropped: string[]) => {
30
+ written.forEach((type) => expect(yml).toContain(`type: ${type}`));
31
+ dropped.forEach((type) => expect(yml).not.toContain(`type: ${type}`));
32
+ };
33
+
34
+ const itemWithAllSlots = (type: string, request: Record<string, any>) => ({
35
+ uid: 'i1',
36
+ type,
37
+ name: 'r',
38
+ seq: 1,
39
+ request: {
40
+ auth: { mode: 'none' },
41
+ headers: [],
42
+ script: { ...ALL_SLOTS },
43
+ tests: null,
44
+ vars: { req: [], res: [] },
45
+ ...request
46
+ }
47
+ }) as any;
48
+
49
+ const rootWithScript = (script: Record<string, string | null>) => ({
50
+ meta: { name: 'my-folder', seq: 1 },
51
+ request: {
52
+ headers: [],
53
+ auth: { mode: 'none' },
54
+ script,
55
+ tests: null,
56
+ vars: { req: [], res: [] }
57
+ },
58
+ docs: null
59
+ }) as any;
60
+
61
+ describe('script — request items', () => {
62
+ const httpFamily = [
63
+ [
64
+ 'http',
65
+ itemWithAllSlots('http-request', {
66
+ url: 'https://example.com',
67
+ method: 'GET',
68
+ params: [],
69
+ body: { mode: 'none' }
70
+ })
71
+ ],
72
+ [
73
+ 'graphql',
74
+ itemWithAllSlots('graphql-request', {
75
+ url: 'https://example.com/graphql',
76
+ method: 'POST',
77
+ params: [],
78
+ body: { mode: 'graphql', graphql: { query: '{ hi }', variables: '' } }
79
+ })
80
+ ],
81
+ // TODO: Move it to a separate test block, once ws scripts are added.
82
+ [
83
+ 'websocket',
84
+ itemWithAllSlots('ws-request', {
85
+ url: 'wss://example.com',
86
+ body: { mode: 'ws', ws: [{ name: 'message 1', content: '{}' }] }
87
+ })
88
+ ]
89
+ ] as [string, any][];
90
+
91
+ it.each(httpFamily)('a %s request writes req/res and drops the grpc hooks', (_name, item) => {
92
+ const yml = stringifyItem(item);
93
+
94
+ expectScriptTypes(yml, HTTP_TYPES, GRPC_TYPES);
95
+ expect(yml).not.toContain('before()');
96
+ expect(yml).not.toContain('after()');
97
+ expect(yml).not.toContain('beforeSend()');
98
+ expect(yml).not.toContain('afterReceive()');
99
+
100
+ expect(parseItem(yml).request!.script).toEqual({ req: 'pre()', res: 'post()' });
101
+ });
102
+
103
+ it('a grpc request writes the lifecycle hooks and drops req/res', () => {
104
+ const item = itemWithAllSlots('grpc-request', {
105
+ url: 'grpc://localhost:50051',
106
+ method: '/hello.Greeter/SayHello',
107
+ methodType: 'unary',
108
+ body: { mode: 'grpc', grpc: [{ name: 'message 1', content: '{}' }] },
109
+ assertions: []
110
+ });
111
+
112
+ const yml = stringifyItem(item);
113
+
114
+ expectScriptTypes(yml, GRPC_TYPES, HTTP_TYPES);
115
+ expect(yml).not.toContain('pre()');
116
+ expect(yml).not.toContain('post()');
117
+
118
+ expect(parseItem(yml).request!.script).toEqual({
119
+ beforeCallStart: 'before()',
120
+ afterCallEnd: 'after()',
121
+ beforeMessageSend: 'beforeSend()',
122
+ afterMessageReceive: 'afterReceive()'
123
+ });
124
+ });
125
+ });
126
+
127
+ describe('script — collection and folder roots', () => {
128
+ it('a collection root writes req/res and drops the grpc hooks', () => {
129
+ const yml = stringifyCollection(rootWithScript({ ...ALL_SLOTS }), { name: 'c' });
130
+
131
+ expectScriptTypes(yml, HTTP_TYPES, GRPC_TYPES);
132
+
133
+ expect(parseCollection(yml).collectionRoot.request!.script).toEqual({ req: 'pre()', res: 'post()' });
134
+ });
135
+
136
+ it('a folder root writes req/res and drops the grpc hooks', () => {
137
+ const yml = stringifyFolder(rootWithScript({ ...ALL_SLOTS }));
138
+
139
+ expectScriptTypes(yml, HTTP_TYPES, GRPC_TYPES);
140
+
141
+ expect(parseFolder(yml).request!.script).toEqual({ req: 'pre()', res: 'post()' });
142
+ });
143
+
144
+ it('a collection root carrying only grpc hooks writes no scripts at all', () => {
145
+ const yml = stringifyCollection(rootWithScript({ ...GRPC_SLOTS }), { name: 'c' });
146
+
147
+ expect(yml).not.toContain('scripts:');
148
+
149
+ expect(parseCollection(yml).collectionRoot.request?.script).toBeUndefined();
150
+ });
151
+
152
+ it('a folder root carrying only grpc hooks writes no scripts at all', () => {
153
+ const yml = stringifyFolder(rootWithScript({ ...GRPC_SLOTS }));
154
+
155
+ expect(yml).not.toContain('scripts:');
156
+
157
+ expect(parseFolder(yml).request!.script).toEqual({ req: null, res: null });
158
+ });
159
+ });