@usebruno/filestore 0.11.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 (65) 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/index.d.ts +3 -1
  13. package/dist/formats/yml/index.d.ts.map +1 -1
  14. package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
  15. package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
  16. package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
  17. package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
  18. package/dist/formats/yml/mockServerTypes.d.ts +95 -0
  19. package/dist/formats/yml/mockServerTypes.d.ts.map +1 -0
  20. package/dist/formats/yml/parseCollection.d.ts.map +1 -1
  21. package/dist/formats/yml/parseFolder.d.ts.map +1 -1
  22. package/dist/formats/yml/parseMockServer.d.ts +4 -0
  23. package/dist/formats/yml/parseMockServer.d.ts.map +1 -0
  24. package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
  25. package/dist/formats/yml/stringifyFolder.d.ts.map +1 -1
  26. package/dist/formats/yml/stringifyMockServer.d.ts +4 -0
  27. package/dist/formats/yml/stringifyMockServer.d.ts.map +1 -0
  28. package/dist/index.d.ts +14 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/types.d.ts +1 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/workers/WorkerQueue/index.d.ts +3 -2
  33. package/dist/workers/WorkerQueue/index.d.ts.map +1 -1
  34. package/dist/workers/index.d.ts +4 -0
  35. package/dist/workers/index.d.ts.map +1 -1
  36. package/package.json +5 -5
  37. package/src/constants.ts +5 -1
  38. package/src/formats/bru/tests/redact-large-text-blocks.spec.js +160 -1
  39. package/src/formats/bru/utils/redact-large-text-blocks.ts +42 -1
  40. package/src/formats/yml/index.ts +5 -1
  41. package/src/formats/yml/items/parseGraphQLRequest.ts +6 -16
  42. package/src/formats/yml/items/parseHttpRequest.ts +6 -16
  43. package/src/formats/yml/items/stringifyGraphQLRequest.ts +4 -6
  44. package/src/formats/yml/items/stringifyHttpRequest.ts +4 -6
  45. package/src/formats/yml/mockServerTypes.ts +95 -0
  46. package/src/formats/yml/parseCollection.spec.ts +123 -0
  47. package/src/formats/yml/parseCollection.ts +18 -5
  48. package/src/formats/yml/parseFolder.spec.ts +18 -0
  49. package/src/formats/yml/parseFolder.ts +5 -1
  50. package/src/formats/yml/parseMockServer.spec.ts +180 -0
  51. package/src/formats/yml/parseMockServer.ts +113 -0
  52. package/src/formats/yml/stringifyCollection.spec.ts +146 -0
  53. package/src/formats/yml/stringifyCollection.ts +22 -9
  54. package/src/formats/yml/stringifyFolder.spec.ts +23 -0
  55. package/src/formats/yml/stringifyFolder.ts +7 -2
  56. package/src/formats/yml/stringifyMockServer.spec.ts +177 -0
  57. package/src/formats/yml/stringifyMockServer.ts +129 -0
  58. package/src/formats/yml/tests/defaultEnvironment.spec.js +50 -0
  59. package/src/formats/yml/tests/stringifyEnvironment.spec.ts +26 -0
  60. package/src/formats/yml/utils.ts +1 -1
  61. package/src/index.ts +34 -1
  62. package/src/types.ts +1 -0
  63. package/src/workers/WorkerQueue/index.ts +10 -2
  64. package/src/workers/index.ts +25 -1
  65. package/src/workers/worker-script.ts +45 -21
@@ -11,6 +11,8 @@ import { toBrunoScripts } from '../common/scripts';
11
11
  import { toBrunoAssertions } from '../common/assertions';
12
12
  import { toBrunoApp } from '../common/app';
13
13
  import { uuid, ensureString } from '../../../utils';
14
+ import { utils } from '@usebruno/common';
15
+ const { toBool, toNumber } = utils;
14
16
 
15
17
  const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
16
18
  const info = ocRequest.info;
@@ -113,11 +115,7 @@ const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
113
115
  if (ocRequest.settings) {
114
116
  const settings: BrunoHttpItemSettings = {};
115
117
 
116
- if (typeof ocRequest.settings.encodeUrl === 'boolean') {
117
- settings.encodeUrl = ocRequest.settings.encodeUrl;
118
- } else {
119
- settings.encodeUrl = true;
120
- }
118
+ settings.encodeUrl = toBool(ocRequest.settings.encodeUrl, true);
121
119
 
122
120
  if (typeof ocRequest.settings.timeout === 'number') {
123
121
  settings.timeout = ocRequest.settings.timeout;
@@ -127,17 +125,9 @@ const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
127
125
  settings.timeout = 0;
128
126
  }
129
127
 
130
- if (typeof ocRequest.settings.followRedirects === 'boolean') {
131
- settings.followRedirects = ocRequest.settings.followRedirects;
132
- } else {
133
- settings.followRedirects = true;
134
- }
135
-
136
- if (typeof ocRequest.settings.maxRedirects === 'number') {
137
- settings.maxRedirects = ocRequest.settings.maxRedirects;
138
- } else {
139
- settings.maxRedirects = 5;
140
- }
128
+ settings.followRedirects = toBool(ocRequest.settings.followRedirects, true);
129
+ settings.maxRedirects = toNumber(ocRequest.settings.maxRedirects, 5);
130
+ settings.forwardAuthorizationHeader = toBool(ocRequest.settings.forwardAuthorizationHeader, true);
141
131
 
142
132
  brunoItem.settings = settings;
143
133
  }
@@ -16,6 +16,7 @@ import { toOpenCollectionVariables } from '../common/variables';
16
16
  import { toOpenCollectionActions } from '../common/actions';
17
17
  import { toOpenCollectionScripts } from '../common/scripts';
18
18
  import { toOpenCollectionAssertions } from '../common/assertions';
19
+ import { resolveTimeoutSetting } from '@usebruno/common/utils';
19
20
 
20
21
  const stringifyGraphQLRequest = (item: BrunoItem): string => {
21
22
  try {
@@ -132,12 +133,7 @@ const stringifyGraphQLRequest = (item: BrunoItem): string => {
132
133
  settings.encodeUrl = true;
133
134
  }
134
135
 
135
- const timeout = httpSettings?.timeout;
136
- if (isNumber(timeout)) {
137
- settings.timeout = timeout;
138
- } else {
139
- settings.timeout = 0;
140
- }
136
+ settings.timeout = resolveTimeoutSetting(httpSettings?.timeout);
141
137
 
142
138
  if (httpSettings?.followRedirects === true) {
143
139
  settings.followRedirects = true;
@@ -154,6 +150,8 @@ const stringifyGraphQLRequest = (item: BrunoItem): string => {
154
150
  settings.maxRedirects = 5;
155
151
  }
156
152
 
153
+ settings.forwardAuthorizationHeader = httpSettings?.forwardAuthorizationHeader ?? true;
154
+
157
155
  ocRequest.settings = settings;
158
156
 
159
157
  // docs
@@ -18,6 +18,7 @@ import { toOpenCollectionActions } from '../common/actions';
18
18
  import { toOpenCollectionScripts } from '../common/scripts';
19
19
  import { toOpenCollectionAssertions } from '../common/assertions';
20
20
  import { isNumber, isNonEmptyString } from '../../../utils';
21
+ import { resolveTimeoutSetting } from '@usebruno/common/utils';
21
22
 
22
23
  const stringifyHttpRequest = (item: BrunoItem): string => {
23
24
  try {
@@ -120,12 +121,7 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
120
121
  settings.encodeUrl = true;
121
122
  }
122
123
 
123
- const timeout = httpSettings?.timeout;
124
- if (isNumber(timeout)) {
125
- settings.timeout = timeout;
126
- } else {
127
- settings.timeout = 0;
128
- }
124
+ settings.timeout = resolveTimeoutSetting(httpSettings?.timeout);
129
125
 
130
126
  if (httpSettings?.followRedirects === true) {
131
127
  settings.followRedirects = true;
@@ -142,6 +138,8 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
142
138
  settings.maxRedirects = 5;
143
139
  }
144
140
 
141
+ settings.forwardAuthorizationHeader = httpSettings?.forwardAuthorizationHeader ?? true;
142
+
145
143
  ocRequest.settings = settings;
146
144
 
147
145
  // examples
@@ -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
+ }
@@ -52,6 +52,129 @@ request:
52
52
  });
53
53
  });
54
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
+
55
178
  describe('parseCollection — reading the collection version', () => {
56
179
  it('reads the version from the file as text', () => {
57
180
  const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n version: v1.0.0\n');
@@ -38,11 +38,14 @@ const parseCollection = (ymlString: string): ParsedCollection => {
38
38
  // presets
39
39
  if (brunoExtension?.presets) {
40
40
  const presets = brunoExtension.presets as BrunoPresetsExtension;
41
- if (presets.request) {
41
+ if (presets.request || presets.defaultEnvironment) {
42
42
  brunoConfig.presets = {
43
- requestType: presets.request.type || '',
44
- requestUrl: presets.request.url || ''
43
+ requestType: presets.request?.type || '',
44
+ requestUrl: presets.request?.url || ''
45
45
  };
46
+ if (presets.defaultEnvironment) {
47
+ brunoConfig.presets.defaultEnvironment = presets.defaultEnvironment;
48
+ }
46
49
  }
47
50
  }
48
51
 
@@ -59,6 +62,14 @@ const parseCollection = (ymlString: string): ParsedCollection => {
59
62
  };
60
63
  }
61
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
+ }
62
73
  if (Array.isArray(brunoExtensions?.openapi) && brunoExtensions.openapi.length > 0) {
63
74
  brunoConfig.openapi = brunoExtensions.openapi.map((entry: any) => ({
64
75
  sourceUrl: entry.sourceUrl,
@@ -142,14 +153,16 @@ const parseCollection = (ymlString: string): ParsedCollection => {
142
153
  type: 'cert',
143
154
  certFilePath: cert.certificateFilePath,
144
155
  keyFilePath: cert.privateKeyFilePath,
145
- passphrase: cert.passphrase || ''
156
+ passphrase: cert.passphrase || '',
157
+ ...(cert.disabled === true && { disabled: true })
146
158
  };
147
159
  } else if (cert.type === 'pkcs12') {
148
160
  return {
149
161
  domain: cert.domain,
150
162
  type: 'pfx',
151
163
  pfxFilePath: cert.pkcs12FilePath,
152
- passphrase: cert.passphrase || ''
164
+ passphrase: cert.passphrase || '',
165
+ ...(cert.disabled === true && { disabled: true })
153
166
  };
154
167
  }
155
168
  return null;
@@ -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
+ });