@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.
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/workers/worker-script.js +1 -1
- package/dist/cjs/workers/worker-script.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/workers/worker-script.js +1 -1
- package/dist/esm/workers/worker-script.js.map +1 -1
- package/dist/formats/bru/utils/redact-large-text-blocks.d.ts.map +1 -1
- package/dist/formats/yml/index.d.ts +3 -1
- package/dist/formats/yml/index.d.ts.map +1 -1
- package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
- package/dist/formats/yml/mockServerTypes.d.ts +95 -0
- package/dist/formats/yml/mockServerTypes.d.ts.map +1 -0
- package/dist/formats/yml/parseCollection.d.ts.map +1 -1
- package/dist/formats/yml/parseFolder.d.ts.map +1 -1
- package/dist/formats/yml/parseMockServer.d.ts +4 -0
- package/dist/formats/yml/parseMockServer.d.ts.map +1 -0
- package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
- package/dist/formats/yml/stringifyFolder.d.ts.map +1 -1
- package/dist/formats/yml/stringifyMockServer.d.ts +4 -0
- package/dist/formats/yml/stringifyMockServer.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/workers/WorkerQueue/index.d.ts +3 -2
- package/dist/workers/WorkerQueue/index.d.ts.map +1 -1
- package/dist/workers/index.d.ts +4 -0
- package/dist/workers/index.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/constants.ts +5 -1
- package/src/formats/bru/tests/redact-large-text-blocks.spec.js +160 -1
- package/src/formats/bru/utils/redact-large-text-blocks.ts +42 -1
- package/src/formats/yml/index.ts +5 -1
- package/src/formats/yml/items/parseGraphQLRequest.ts +6 -16
- package/src/formats/yml/items/parseHttpRequest.ts +6 -16
- package/src/formats/yml/items/stringifyGraphQLRequest.ts +4 -6
- package/src/formats/yml/items/stringifyHttpRequest.ts +4 -6
- package/src/formats/yml/mockServerTypes.ts +95 -0
- package/src/formats/yml/parseCollection.spec.ts +123 -0
- package/src/formats/yml/parseCollection.ts +18 -5
- package/src/formats/yml/parseFolder.spec.ts +18 -0
- package/src/formats/yml/parseFolder.ts +5 -1
- package/src/formats/yml/parseMockServer.spec.ts +180 -0
- package/src/formats/yml/parseMockServer.ts +113 -0
- package/src/formats/yml/stringifyCollection.spec.ts +146 -0
- package/src/formats/yml/stringifyCollection.ts +22 -9
- package/src/formats/yml/stringifyFolder.spec.ts +23 -0
- package/src/formats/yml/stringifyFolder.ts +7 -2
- package/src/formats/yml/stringifyMockServer.spec.ts +177 -0
- package/src/formats/yml/stringifyMockServer.ts +129 -0
- package/src/formats/yml/tests/defaultEnvironment.spec.js +50 -0
- package/src/formats/yml/tests/stringifyEnvironment.spec.ts +26 -0
- package/src/formats/yml/utils.ts +1 -1
- package/src/index.ts +34 -1
- package/src/types.ts +1 -0
- package/src/workers/WorkerQueue/index.ts +10 -2
- package/src/workers/index.ts +25 -1
- package/src/workers/worker-script.ts +45 -21
|
@@ -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;
|
|
@@ -48,6 +48,152 @@ describe('stringifyCollection — typed request.variables', () => {
|
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
describe('stringifyCollection — script execution flow', () => {
|
|
52
|
+
const baseRoot = {
|
|
53
|
+
meta: null,
|
|
54
|
+
request: { headers: [], auth: { mode: 'none' }, script: { req: null, res: null }, tests: null, vars: { req: [], res: [] } },
|
|
55
|
+
docs: null
|
|
56
|
+
} as any;
|
|
57
|
+
|
|
58
|
+
it('round-trips flow: sequential through the bruno extension', () => {
|
|
59
|
+
const yml = stringifyCollection(baseRoot, { name: 'c', scripts: { flow: 'sequential' } });
|
|
60
|
+
expect(yml).toMatch(/flow:\s*sequential/);
|
|
61
|
+
expect(parseCollection(yml).brunoConfig.scripts?.flow).toBe('sequential');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('round-trips flow: sandwich through the bruno extension', () => {
|
|
65
|
+
const yml = stringifyCollection(baseRoot, { name: 'c', scripts: { flow: 'sandwich' } });
|
|
66
|
+
expect(parseCollection(yml).brunoConfig.scripts?.flow).toBe('sandwich');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('writes no flow when the collection has none', () => {
|
|
70
|
+
const yml = stringifyCollection(baseRoot, { name: 'c' });
|
|
71
|
+
expect(yml).not.toMatch(/flow:/);
|
|
72
|
+
expect(parseCollection(yml).brunoConfig.scripts?.flow).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('drops an unrecognized flow value on write', () => {
|
|
76
|
+
const yml = stringifyCollection(baseRoot, { name: 'c', scripts: { flow: 'parallel' } });
|
|
77
|
+
expect(yml).not.toMatch(/flow:/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('preserves additionalContextRoots alongside a written flow value', () => {
|
|
81
|
+
const yml = stringifyCollection(baseRoot, {
|
|
82
|
+
name: 'c',
|
|
83
|
+
scripts: { flow: 'sequential', additionalContextRoots: ['../libs'] }
|
|
84
|
+
});
|
|
85
|
+
const { brunoConfig } = parseCollection(yml);
|
|
86
|
+
expect(brunoConfig.scripts?.flow).toBe('sequential');
|
|
87
|
+
expect(brunoConfig.scripts?.additionalContextRoots).toEqual(['../libs']);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('stringifyCollection — client certificates', () => {
|
|
92
|
+
const baseRoot = {
|
|
93
|
+
meta: null,
|
|
94
|
+
request: { headers: [], auth: { mode: 'none' }, script: { req: null, res: null }, tests: null, vars: { req: [], res: [] } },
|
|
95
|
+
docs: null
|
|
96
|
+
} as any;
|
|
97
|
+
|
|
98
|
+
it('round-trips cert/pfx certificates', () => {
|
|
99
|
+
const brunoConfig = {
|
|
100
|
+
name: 'c',
|
|
101
|
+
clientCertificates: {
|
|
102
|
+
certs: [
|
|
103
|
+
{
|
|
104
|
+
domain: 'localhost',
|
|
105
|
+
type: 'cert',
|
|
106
|
+
certFilePath: './certs/client-cert.pem',
|
|
107
|
+
keyFilePath: './certs/client-key.pem',
|
|
108
|
+
passphrase: 'secret'
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
domain: 'example.com',
|
|
112
|
+
type: 'pfx',
|
|
113
|
+
pfxFilePath: './certs/client.pfx',
|
|
114
|
+
passphrase: ''
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const yml = stringifyCollection(baseRoot, brunoConfig);
|
|
121
|
+
const { brunoConfig: reparsed } = parseCollection(yml);
|
|
122
|
+
|
|
123
|
+
expect(reparsed.clientCertificates.certs).toEqual([
|
|
124
|
+
{
|
|
125
|
+
domain: 'localhost',
|
|
126
|
+
type: 'cert',
|
|
127
|
+
certFilePath: './certs/client-cert.pem',
|
|
128
|
+
keyFilePath: './certs/client-key.pem',
|
|
129
|
+
passphrase: 'secret'
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
domain: 'example.com',
|
|
133
|
+
type: 'pfx',
|
|
134
|
+
pfxFilePath: './certs/client.pfx',
|
|
135
|
+
passphrase: ''
|
|
136
|
+
}
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('writes disabled: true for disabled certs and round-trips it', () => {
|
|
141
|
+
const brunoConfig = {
|
|
142
|
+
name: 'c',
|
|
143
|
+
clientCertificates: {
|
|
144
|
+
certs: [
|
|
145
|
+
{
|
|
146
|
+
domain: 'localhost',
|
|
147
|
+
type: 'cert',
|
|
148
|
+
certFilePath: './certs/client-cert.pem',
|
|
149
|
+
keyFilePath: './certs/client-key.pem',
|
|
150
|
+
passphrase: 'secret',
|
|
151
|
+
disabled: true
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
domain: 'example.com',
|
|
155
|
+
type: 'pfx',
|
|
156
|
+
pfxFilePath: './certs/client.pfx',
|
|
157
|
+
passphrase: '',
|
|
158
|
+
disabled: true
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const yml = stringifyCollection(baseRoot, brunoConfig);
|
|
165
|
+
|
|
166
|
+
expect(yml).toMatch(/disabled:\s*true/);
|
|
167
|
+
|
|
168
|
+
const { brunoConfig: reparsed } = parseCollection(yml);
|
|
169
|
+
expect(reparsed.clientCertificates.certs[0].disabled).toBe(true);
|
|
170
|
+
expect(reparsed.clientCertificates.certs[1].disabled).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('does not write disabled for enabled certs', () => {
|
|
174
|
+
const brunoConfig = {
|
|
175
|
+
name: 'c',
|
|
176
|
+
clientCertificates: {
|
|
177
|
+
certs: [
|
|
178
|
+
{
|
|
179
|
+
domain: 'example.com',
|
|
180
|
+
type: 'pfx',
|
|
181
|
+
pfxFilePath: './certs/client.pfx',
|
|
182
|
+
passphrase: ''
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const yml = stringifyCollection(baseRoot, brunoConfig);
|
|
189
|
+
|
|
190
|
+
expect(yml).not.toMatch(/disabled:/);
|
|
191
|
+
|
|
192
|
+
const { brunoConfig: reparsed } = parseCollection(yml);
|
|
193
|
+
expect(reparsed.clientCertificates.certs[0]).not.toHaveProperty('disabled');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
51
197
|
describe('stringifyCollection — writing the collection version', () => {
|
|
52
198
|
const baseRoot = {
|
|
53
199
|
meta: null,
|
|
@@ -61,7 +61,8 @@ const hasRequestScripts = (collectionRoot: any): boolean => {
|
|
|
61
61
|
|
|
62
62
|
const hasPresets = (brunoConfig: any): boolean => {
|
|
63
63
|
return brunoConfig?.presets?.requestType?.length
|
|
64
|
-
|| brunoConfig?.presets?.requestUrl?.length
|
|
64
|
+
|| brunoConfig?.presets?.requestUrl?.length
|
|
65
|
+
|| brunoConfig?.presets?.defaultEnvironment?.length;
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
@@ -148,7 +149,8 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
148
149
|
type: 'pem',
|
|
149
150
|
certificateFilePath: cert.certFilePath,
|
|
150
151
|
privateKeyFilePath: cert.keyFilePath,
|
|
151
|
-
...(cert.passphrase && { passphrase: cert.passphrase })
|
|
152
|
+
...(cert.passphrase && { passphrase: cert.passphrase }),
|
|
153
|
+
...(cert.disabled === true && { disabled: true })
|
|
152
154
|
};
|
|
153
155
|
return pemCert;
|
|
154
156
|
} else if (cert.type === 'pfx') {
|
|
@@ -156,7 +158,8 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
156
158
|
domain: cert.domain,
|
|
157
159
|
type: 'pkcs12',
|
|
158
160
|
pkcs12FilePath: cert.pfxFilePath,
|
|
159
|
-
...(cert.passphrase && { passphrase: cert.passphrase })
|
|
161
|
+
...(cert.passphrase && { passphrase: cert.passphrase }),
|
|
162
|
+
...(cert.disabled === true && { disabled: true })
|
|
160
163
|
};
|
|
161
164
|
return pkcs12Cert;
|
|
162
165
|
} else {
|
|
@@ -241,6 +244,7 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
241
244
|
}
|
|
242
245
|
|
|
243
246
|
if (hasPresets(brunoConfig)) {
|
|
247
|
+
const presetsExtension: any = {};
|
|
244
248
|
const presetsRequest: any = {};
|
|
245
249
|
if (brunoConfig.presets.requestType?.length) {
|
|
246
250
|
presetsRequest.type = brunoConfig.presets.requestType;
|
|
@@ -248,22 +252,31 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
248
252
|
if (brunoConfig.presets.requestUrl?.length) {
|
|
249
253
|
presetsRequest.url = brunoConfig.presets.requestUrl;
|
|
250
254
|
}
|
|
251
|
-
|
|
252
|
-
request
|
|
253
|
-
}
|
|
255
|
+
if (Object.keys(presetsRequest).length) {
|
|
256
|
+
presetsExtension.request = presetsRequest;
|
|
257
|
+
}
|
|
258
|
+
if (brunoConfig.presets.defaultEnvironment?.length) {
|
|
259
|
+
presetsExtension.defaultEnvironment = brunoConfig.presets.defaultEnvironment;
|
|
260
|
+
}
|
|
261
|
+
brunoExtension.presets = presetsExtension;
|
|
254
262
|
}
|
|
255
263
|
|
|
256
264
|
oc.extensions.bruno = brunoExtension;
|
|
257
265
|
}
|
|
258
266
|
|
|
259
267
|
// bruno-specific script extensions
|
|
268
|
+
const brunoScripts: Record<string, unknown> = {};
|
|
260
269
|
if (brunoConfig.scripts?.additionalContextRoots?.length) {
|
|
270
|
+
brunoScripts.additionalContextRoots = brunoConfig.scripts.additionalContextRoots;
|
|
271
|
+
}
|
|
272
|
+
if (brunoConfig.scripts?.flow === 'sandwich' || brunoConfig.scripts?.flow === 'sequential') {
|
|
273
|
+
brunoScripts.flow = brunoConfig.scripts.flow;
|
|
274
|
+
}
|
|
275
|
+
if (Object.keys(brunoScripts).length > 0) {
|
|
261
276
|
if (!oc.extensions.bruno) {
|
|
262
277
|
oc.extensions.bruno = {};
|
|
263
278
|
}
|
|
264
|
-
(oc.extensions.bruno as any).scripts =
|
|
265
|
-
additionalContextRoots: brunoConfig.scripts.additionalContextRoots
|
|
266
|
-
};
|
|
279
|
+
(oc.extensions.bruno as any).scripts = brunoScripts;
|
|
267
280
|
}
|
|
268
281
|
|
|
269
282
|
// bruno-specific extensions
|
|
@@ -45,3 +45,26 @@ describe('stringifyFolder — typed request.variables', () => {
|
|
|
45
45
|
expect(reqVars[4].dataType).toBeUndefined();
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
describe('stringifyFolder — seq', () => {
|
|
50
|
+
it('omits seq when the folder has none', () => {
|
|
51
|
+
const folderRoot = { meta: { name: 'no-seq-folder' }, docs: null } as any;
|
|
52
|
+
|
|
53
|
+
const yml = stringifyFolder(folderRoot);
|
|
54
|
+
const { meta } = parseFolder(yml);
|
|
55
|
+
|
|
56
|
+
expect(yml).not.toMatch(/seq:/);
|
|
57
|
+
expect(meta).toEqual(expect.objectContaining({ name: 'no-seq-folder' }));
|
|
58
|
+
expect(meta!.seq).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('preserves an explicit numeric seq', () => {
|
|
62
|
+
const folderRoot = { meta: { name: 'ordered-folder', seq: 3 }, docs: null } as any;
|
|
63
|
+
|
|
64
|
+
const yml = stringifyFolder(folderRoot);
|
|
65
|
+
const { meta } = parseFolder(yml);
|
|
66
|
+
|
|
67
|
+
expect(meta).toEqual(expect.objectContaining({ name: 'ordered-folder' }));
|
|
68
|
+
expect(meta!.seq).toBe(3);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -40,9 +40,14 @@ const stringifyFolder = (folderRoot: FolderRoot): string => {
|
|
|
40
40
|
// info block
|
|
41
41
|
const info: FolderInfo = {
|
|
42
42
|
name: folderRoot.meta?.name || 'Untitled Folder',
|
|
43
|
-
type: 'folder'
|
|
44
|
-
seq: folderRoot.meta?.seq || 1
|
|
43
|
+
type: 'folder'
|
|
45
44
|
};
|
|
45
|
+
// Only write seq when the folder actually has a numeric one. Defaulting to 1 would
|
|
46
|
+
// force every seq-less folder into position 1 on disk and break alphabetical fallback.
|
|
47
|
+
const seq = folderRoot.meta?.seq;
|
|
48
|
+
if (typeof seq === 'number' && Number.isFinite(seq)) {
|
|
49
|
+
info.seq = seq;
|
|
50
|
+
}
|
|
46
51
|
ocFolder.info = info;
|
|
47
52
|
|
|
48
53
|
// request defaults
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import stringifyMockServer from './stringifyMockServer';
|
|
2
|
+
import parseMockServer from './parseMockServer';
|
|
3
|
+
import type { BrunoMockServer } from './mockServerTypes';
|
|
4
|
+
|
|
5
|
+
const fullMockServer: BrunoMockServer = {
|
|
6
|
+
name: 'Dog API Mock',
|
|
7
|
+
port: 4001,
|
|
8
|
+
delay: 500,
|
|
9
|
+
source: {
|
|
10
|
+
type: 'collection',
|
|
11
|
+
path: '/Users/x/collections/dog-api'
|
|
12
|
+
},
|
|
13
|
+
routes: [
|
|
14
|
+
{
|
|
15
|
+
name: 'Get user 200',
|
|
16
|
+
description: 'Returns a user',
|
|
17
|
+
request: {
|
|
18
|
+
url: '/users/:id',
|
|
19
|
+
method: 'GET',
|
|
20
|
+
headers: [{ uid: 'h1', name: 'X-Client', value: 'bruno', enabled: true }],
|
|
21
|
+
params: [{ uid: 'p1', name: 'id', value: '1', type: 'path', enabled: true }],
|
|
22
|
+
body: { mode: 'json', json: '{"filter": true}' }
|
|
23
|
+
},
|
|
24
|
+
response: {
|
|
25
|
+
status: 200,
|
|
26
|
+
statusText: 'OK',
|
|
27
|
+
headers: [{ uid: 'rh1', name: 'Content-Type', value: 'application/json', enabled: true }],
|
|
28
|
+
body: {
|
|
29
|
+
type: 'json',
|
|
30
|
+
content: '{"id": 1}'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
rules: {
|
|
34
|
+
operator: 'AND',
|
|
35
|
+
conditions: [
|
|
36
|
+
{ target: 'header', key: 'Authorization', operator: 'equals', value: 'token' }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
copiedFrom: {
|
|
40
|
+
exampleName: 'Success',
|
|
41
|
+
requestPathname: '/Users/x/collections/dog-api/get-user.yml'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('stringifyMockServer', () => {
|
|
48
|
+
it('writes the info block with type mock', () => {
|
|
49
|
+
const content = stringifyMockServer(fullMockServer);
|
|
50
|
+
|
|
51
|
+
expect(content).toContain('info:');
|
|
52
|
+
expect(content).toContain('name: Dog API Mock');
|
|
53
|
+
expect(content).toContain('type: mock');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('writes port, delay and source under the mock block', () => {
|
|
57
|
+
const content = stringifyMockServer(fullMockServer);
|
|
58
|
+
|
|
59
|
+
expect(content).toContain('mock:');
|
|
60
|
+
expect(content).toContain('port: 4001');
|
|
61
|
+
expect(content).toContain('delay: 500');
|
|
62
|
+
expect(content).toContain('type: collection');
|
|
63
|
+
expect(content).toContain('path: /Users/x/collections/dog-api');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('omits delay when zero and source when manual', () => {
|
|
67
|
+
const content = stringifyMockServer({
|
|
68
|
+
name: 'Manual Mock',
|
|
69
|
+
port: 4000,
|
|
70
|
+
delay: 0,
|
|
71
|
+
source: null,
|
|
72
|
+
routes: []
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(content).not.toContain('delay:');
|
|
76
|
+
expect(content).not.toContain('source:');
|
|
77
|
+
expect(content).not.toContain('routes:');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('omits rules when there are no conditions', () => {
|
|
81
|
+
const content = stringifyMockServer({
|
|
82
|
+
name: 'Mock',
|
|
83
|
+
port: 4000,
|
|
84
|
+
delay: 0,
|
|
85
|
+
source: null,
|
|
86
|
+
routes: [
|
|
87
|
+
{
|
|
88
|
+
name: 'Ping',
|
|
89
|
+
description: '',
|
|
90
|
+
request: { url: '/ping', method: 'GET', headers: [], params: [], body: { mode: 'none' } },
|
|
91
|
+
response: { status: 204, statusText: 'No Content', headers: [], body: { type: 'text', content: '' } },
|
|
92
|
+
rules: { operator: 'AND', conditions: [] }
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(content).not.toContain('rules:');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('does not write uids anywhere', () => {
|
|
101
|
+
const content = stringifyMockServer(fullMockServer);
|
|
102
|
+
expect(content).not.toContain('uid');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('mock server round-trip', () => {
|
|
107
|
+
it('parse(stringify(x)) preserves all fields', () => {
|
|
108
|
+
const parsed = parseMockServer(stringifyMockServer(fullMockServer));
|
|
109
|
+
|
|
110
|
+
expect(parsed.name).toBe('Dog API Mock');
|
|
111
|
+
expect(parsed.port).toBe(4001);
|
|
112
|
+
expect(parsed.delay).toBe(500);
|
|
113
|
+
expect(parsed.source).toEqual({ type: 'collection', path: '/Users/x/collections/dog-api' });
|
|
114
|
+
|
|
115
|
+
expect(parsed.routes).toHaveLength(1);
|
|
116
|
+
const route = parsed.routes[0];
|
|
117
|
+
expect(route.name).toBe('Get user 200');
|
|
118
|
+
expect(route.description).toBe('Returns a user');
|
|
119
|
+
expect(route.request.url).toBe('/users/:id');
|
|
120
|
+
expect(route.request.method).toBe('GET');
|
|
121
|
+
expect(route.request.headers).toEqual([
|
|
122
|
+
expect.objectContaining({ name: 'X-Client', value: 'bruno', enabled: true })
|
|
123
|
+
]);
|
|
124
|
+
expect(route.request.params).toEqual([
|
|
125
|
+
expect.objectContaining({ name: 'id', value: '1', type: 'path', enabled: true })
|
|
126
|
+
]);
|
|
127
|
+
expect(route.request.body).toEqual(expect.objectContaining({ mode: 'json', json: '{"filter": true}' }));
|
|
128
|
+
expect(route.response.status).toBe(200);
|
|
129
|
+
expect(route.response.statusText).toBe('OK');
|
|
130
|
+
expect(route.response.headers).toEqual([
|
|
131
|
+
expect.objectContaining({ name: 'Content-Type', value: 'application/json' })
|
|
132
|
+
]);
|
|
133
|
+
expect(route.response.body).toEqual({ type: 'json', content: '{"id": 1}' });
|
|
134
|
+
expect(route.rules).toEqual({
|
|
135
|
+
operator: 'AND',
|
|
136
|
+
conditions: [{ target: 'header', key: 'Authorization', operator: 'equals', value: 'token' }]
|
|
137
|
+
});
|
|
138
|
+
expect(route.copiedFrom).toEqual({
|
|
139
|
+
exampleName: 'Success',
|
|
140
|
+
requestPathname: '/Users/x/collections/dog-api/get-user.yml'
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('round-trips a spec-sourced server', () => {
|
|
145
|
+
const parsed = parseMockServer(stringifyMockServer({
|
|
146
|
+
name: 'Spec Mock',
|
|
147
|
+
port: 4002,
|
|
148
|
+
delay: 0,
|
|
149
|
+
source: { type: 'spec', path: '/Users/x/specs/petstore.yml' },
|
|
150
|
+
routes: []
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
expect(parsed.source).toEqual({ type: 'spec', path: '/Users/x/specs/petstore.yml' });
|
|
154
|
+
expect(parsed.routes).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('round-trips multiline json response bodies byte-safely', () => {
|
|
158
|
+
const content = '{\n "id": 1,\n "name": "rex: \'good\' dog"\n}';
|
|
159
|
+
const parsed = parseMockServer(stringifyMockServer({
|
|
160
|
+
name: 'Mock',
|
|
161
|
+
port: 4000,
|
|
162
|
+
delay: 0,
|
|
163
|
+
source: null,
|
|
164
|
+
routes: [
|
|
165
|
+
{
|
|
166
|
+
name: 'Get dog',
|
|
167
|
+
description: '',
|
|
168
|
+
request: { url: '/dogs/1', method: 'GET', headers: [], params: [], body: { mode: 'none' } },
|
|
169
|
+
response: { status: 200, statusText: 'OK', headers: [], body: { type: 'json', content } },
|
|
170
|
+
rules: { operator: 'AND', conditions: [] }
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
expect(parsed.routes[0].response.body.content).toBe(content);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { stringifyYml } from './utils';
|
|
2
|
+
import { isNonEmptyString } from '../../utils';
|
|
3
|
+
import { toOpenCollectionHttpHeaders, toOpenCollectionResponseHeaders } from './common/headers';
|
|
4
|
+
import { toOpenCollectionParams } from './common/params';
|
|
5
|
+
import { toOpenCollectionBody } from './common/body';
|
|
6
|
+
import type {
|
|
7
|
+
MockServerFile,
|
|
8
|
+
MockRouteEntry,
|
|
9
|
+
BrunoMockServer,
|
|
10
|
+
BrunoMockRoute
|
|
11
|
+
} from './mockServerTypes';
|
|
12
|
+
|
|
13
|
+
const toOcRuleConditions = (conditions: BrunoMockRoute['rules']['conditions']) => (
|
|
14
|
+
conditions.map((condition) => ({
|
|
15
|
+
target: condition.target || '',
|
|
16
|
+
key: condition.key || '',
|
|
17
|
+
operator: condition.operator || 'equals',
|
|
18
|
+
value: condition.value || ''
|
|
19
|
+
}))
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const toOcMockRoute = (route: BrunoMockRoute): MockRouteEntry => {
|
|
23
|
+
const ocRoute: MockRouteEntry = {
|
|
24
|
+
name: route.name || 'Untitled Mock Response'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (isNonEmptyString(route.description)) {
|
|
28
|
+
ocRoute.description = route.description;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ocRoute.request = {
|
|
32
|
+
method: route.request?.method || 'GET',
|
|
33
|
+
url: route.request?.url || '/'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const requestHeaders = toOpenCollectionHttpHeaders(route.request?.headers);
|
|
37
|
+
if (requestHeaders) {
|
|
38
|
+
ocRoute.request.headers = requestHeaders;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const requestParams = toOpenCollectionParams(route.request?.params);
|
|
42
|
+
if (requestParams) {
|
|
43
|
+
ocRoute.request.params = requestParams;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const requestBody = toOpenCollectionBody(route.request?.body);
|
|
47
|
+
if (requestBody !== undefined && !Array.isArray(requestBody)) {
|
|
48
|
+
ocRoute.request.body = requestBody;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ocRoute.response = {
|
|
52
|
+
status: Number(route.response?.status) || 200
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (isNonEmptyString(route.response?.statusText)) {
|
|
56
|
+
ocRoute.response.statusText = route.response.statusText;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const responseHeaders = toOpenCollectionResponseHeaders(route.response?.headers);
|
|
60
|
+
if (responseHeaders) {
|
|
61
|
+
ocRoute.response.headers = responseHeaders;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const responseBody = route.response?.body;
|
|
65
|
+
if (responseBody && responseBody.type) {
|
|
66
|
+
const content = responseBody.content;
|
|
67
|
+
ocRoute.response.body = {
|
|
68
|
+
type: responseBody.type,
|
|
69
|
+
data: typeof content === 'string' ? content : JSON.stringify(content, null, 2)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (route.rules?.conditions?.length) {
|
|
74
|
+
ocRoute.rules = {
|
|
75
|
+
operator: route.rules.operator === 'OR' ? 'OR' : 'AND',
|
|
76
|
+
conditions: toOcRuleConditions(route.rules.conditions)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (route.copiedFrom && (route.copiedFrom.exampleName || route.copiedFrom.requestPathname)) {
|
|
81
|
+
ocRoute.copiedFrom = {};
|
|
82
|
+
if (route.copiedFrom.exampleName) {
|
|
83
|
+
ocRoute.copiedFrom.example = route.copiedFrom.exampleName;
|
|
84
|
+
}
|
|
85
|
+
if (route.copiedFrom.requestPathname) {
|
|
86
|
+
ocRoute.copiedFrom.requestPath = route.copiedFrom.requestPathname;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return ocRoute;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const stringifyMockServer = (mockServer: BrunoMockServer): string => {
|
|
94
|
+
try {
|
|
95
|
+
const ocMockServer: MockServerFile = {
|
|
96
|
+
info: {
|
|
97
|
+
name: mockServer.name && mockServer.name.trim().length ? mockServer.name : 'Mock Server',
|
|
98
|
+
type: 'mock'
|
|
99
|
+
},
|
|
100
|
+
mock: {
|
|
101
|
+
port: Number(mockServer.port) || 4000
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const delay = Number(mockServer.delay);
|
|
106
|
+
if (delay > 0) {
|
|
107
|
+
ocMockServer.mock!.delay = delay;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const source = mockServer.source;
|
|
111
|
+
if (source && (source.type === 'collection' || source.type === 'spec') && source.path) {
|
|
112
|
+
ocMockServer.mock!.source = {
|
|
113
|
+
type: source.type,
|
|
114
|
+
path: source.path
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (mockServer.routes?.length) {
|
|
119
|
+
ocMockServer.routes = mockServer.routes.map(toOcMockRoute);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return stringifyYml(ocMockServer);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Error stringifying mock server:', error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default stringifyMockServer;
|