@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.
- 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/common/omit-headers.d.ts +3 -0
- package/dist/formats/yml/common/omit-headers.d.ts.map +1 -0
- package/dist/formats/yml/common/scripts.d.ts +3 -5
- package/dist/formats/yml/common/scripts.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/parseGrpcRequest.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/stringifyGrpcRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyWebsocketRequest.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/parseEnvironment.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/stringifyEnvironment.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 +8 -8
- package/src/constants.ts +5 -1
- package/src/formats/bru/tests/redact-large-text-blocks.spec.js +220 -1
- package/src/formats/bru/utils/redact-large-text-blocks.ts +46 -1
- package/src/formats/yml/common/omit-headers.spec.ts +16 -0
- package/src/formats/yml/common/omit-headers.ts +12 -0
- package/src/formats/yml/common/scripts.spec.ts +155 -0
- package/src/formats/yml/common/scripts.ts +55 -29
- package/src/formats/yml/grpc-round-trip.spec.ts +110 -0
- package/src/formats/yml/index.ts +5 -1
- package/src/formats/yml/items/parseGraphQLRequest.ts +6 -16
- package/src/formats/yml/items/parseGrpcRequest.ts +14 -6
- package/src/formats/yml/items/parseHttpRequest.ts +10 -14
- package/src/formats/yml/items/stringifyGraphQLRequest.ts +8 -14
- package/src/formats/yml/items/stringifyGrpcRequest.ts +2 -1
- package/src/formats/yml/items/stringifyHttpRequest.ts +13 -13
- package/src/formats/yml/items/stringifyWebsocketRequest.ts +3 -1
- package/src/formats/yml/mockServerTypes.ts +95 -0
- package/src/formats/yml/parseCollection.spec.ts +123 -0
- package/src/formats/yml/parseCollection.ts +23 -14
- package/src/formats/yml/parseEnvironment.spec.ts +49 -0
- package/src/formats/yml/parseEnvironment.ts +6 -0
- 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/script.spec.ts +159 -0
- package/src/formats/yml/stringifyCollection.spec.ts +146 -0
- package/src/formats/yml/stringifyCollection.ts +29 -19
- package/src/formats/yml/stringifyEnvironment.spec.ts +58 -0
- package/src/formats/yml/stringifyEnvironment.ts +6 -0
- package/src/formats/yml/stringifyFolder.spec.ts +23 -0
- package/src/formats/yml/stringifyFolder.ts +10 -3
- 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/maxRedirects.spec.ts +144 -0
- 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
|
@@ -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,
|
|
@@ -5,6 +5,7 @@ import type { ClientCertificate, PemCertificate, Pkcs12Certificate } from '@open
|
|
|
5
5
|
import type { Variable } from '@opencollection/types/common/variables';
|
|
6
6
|
import type { Action } from '@opencollection/types/common/actions';
|
|
7
7
|
import type { Scripts } from '@opencollection/types/common/scripts';
|
|
8
|
+
import { normalizeOpenApiSyncConfigs } from '@usebruno/common';
|
|
8
9
|
import { stringifyYml } from './utils';
|
|
9
10
|
import { toOpenCollectionAuth } from './common/auth';
|
|
10
11
|
import { toOpenCollectionHttpHeaders } from './common/headers';
|
|
@@ -12,6 +13,7 @@ import { toOpenCollectionVariables } from './common/variables';
|
|
|
12
13
|
import { toOpenCollectionActions } from './common/actions';
|
|
13
14
|
import { toOpenCollectionScripts } from './common/scripts';
|
|
14
15
|
import type { Auth } from '@opencollection/types/common/auth';
|
|
16
|
+
import { HTTP_SCRIPT_KEYS } from '@usebruno/common';
|
|
15
17
|
|
|
16
18
|
const hasCollectionConfig = (brunoConfig: any): boolean => {
|
|
17
19
|
// protobuf
|
|
@@ -61,7 +63,8 @@ const hasRequestScripts = (collectionRoot: any): boolean => {
|
|
|
61
63
|
|
|
62
64
|
const hasPresets = (brunoConfig: any): boolean => {
|
|
63
65
|
return brunoConfig?.presets?.requestType?.length
|
|
64
|
-
|| brunoConfig?.presets?.requestUrl?.length
|
|
66
|
+
|| brunoConfig?.presets?.requestUrl?.length
|
|
67
|
+
|| brunoConfig?.presets?.defaultEnvironment?.length;
|
|
65
68
|
};
|
|
66
69
|
|
|
67
70
|
const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
@@ -148,7 +151,8 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
148
151
|
type: 'pem',
|
|
149
152
|
certificateFilePath: cert.certFilePath,
|
|
150
153
|
privateKeyFilePath: cert.keyFilePath,
|
|
151
|
-
...(cert.passphrase && { passphrase: cert.passphrase })
|
|
154
|
+
...(cert.passphrase && { passphrase: cert.passphrase }),
|
|
155
|
+
...(cert.disabled === true && { disabled: true })
|
|
152
156
|
};
|
|
153
157
|
return pemCert;
|
|
154
158
|
} else if (cert.type === 'pfx') {
|
|
@@ -156,7 +160,8 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
156
160
|
domain: cert.domain,
|
|
157
161
|
type: 'pkcs12',
|
|
158
162
|
pkcs12FilePath: cert.pfxFilePath,
|
|
159
|
-
...(cert.passphrase && { passphrase: cert.passphrase })
|
|
163
|
+
...(cert.passphrase && { passphrase: cert.passphrase }),
|
|
164
|
+
...(cert.disabled === true && { disabled: true })
|
|
160
165
|
};
|
|
161
166
|
return pkcs12Cert;
|
|
162
167
|
} else {
|
|
@@ -206,7 +211,8 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
206
211
|
|
|
207
212
|
// scripts
|
|
208
213
|
if (hasRequestScripts(collectionRoot)) {
|
|
209
|
-
|
|
214
|
+
// TODO: Widen scope to include GRPC scripts once Collection/Folder level inheritance is added to gRPC.
|
|
215
|
+
const ocScripts: Scripts | undefined = toOpenCollectionScripts(collectionRoot.request, HTTP_SCRIPT_KEYS);
|
|
210
216
|
if (ocScripts) {
|
|
211
217
|
oc.request.scripts = ocScripts;
|
|
212
218
|
}
|
|
@@ -241,6 +247,7 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
if (hasPresets(brunoConfig)) {
|
|
250
|
+
const presetsExtension: any = {};
|
|
244
251
|
const presetsRequest: any = {};
|
|
245
252
|
if (brunoConfig.presets.requestType?.length) {
|
|
246
253
|
presetsRequest.type = brunoConfig.presets.requestType;
|
|
@@ -248,37 +255,40 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
248
255
|
if (brunoConfig.presets.requestUrl?.length) {
|
|
249
256
|
presetsRequest.url = brunoConfig.presets.requestUrl;
|
|
250
257
|
}
|
|
251
|
-
|
|
252
|
-
request
|
|
253
|
-
}
|
|
258
|
+
if (Object.keys(presetsRequest).length) {
|
|
259
|
+
presetsExtension.request = presetsRequest;
|
|
260
|
+
}
|
|
261
|
+
if (brunoConfig.presets.defaultEnvironment?.length) {
|
|
262
|
+
presetsExtension.defaultEnvironment = brunoConfig.presets.defaultEnvironment;
|
|
263
|
+
}
|
|
264
|
+
brunoExtension.presets = presetsExtension;
|
|
254
265
|
}
|
|
255
266
|
|
|
256
267
|
oc.extensions.bruno = brunoExtension;
|
|
257
268
|
}
|
|
258
269
|
|
|
259
270
|
// bruno-specific script extensions
|
|
271
|
+
const brunoScripts: Record<string, unknown> = {};
|
|
260
272
|
if (brunoConfig.scripts?.additionalContextRoots?.length) {
|
|
273
|
+
brunoScripts.additionalContextRoots = brunoConfig.scripts.additionalContextRoots;
|
|
274
|
+
}
|
|
275
|
+
if (brunoConfig.scripts?.flow === 'sandwich' || brunoConfig.scripts?.flow === 'sequential') {
|
|
276
|
+
brunoScripts.flow = brunoConfig.scripts.flow;
|
|
277
|
+
}
|
|
278
|
+
if (Object.keys(brunoScripts).length > 0) {
|
|
261
279
|
if (!oc.extensions.bruno) {
|
|
262
280
|
oc.extensions.bruno = {};
|
|
263
281
|
}
|
|
264
|
-
(oc.extensions.bruno as any).scripts =
|
|
265
|
-
additionalContextRoots: brunoConfig.scripts.additionalContextRoots
|
|
266
|
-
};
|
|
282
|
+
(oc.extensions.bruno as any).scripts = brunoScripts;
|
|
267
283
|
}
|
|
268
284
|
|
|
269
285
|
// bruno-specific extensions
|
|
270
|
-
|
|
286
|
+
const openApiEntries = normalizeOpenApiSyncConfigs(brunoConfig.openapi);
|
|
287
|
+
if (openApiEntries.length > 0) {
|
|
271
288
|
if (!oc.extensions.bruno) {
|
|
272
289
|
oc.extensions.bruno = {};
|
|
273
290
|
}
|
|
274
|
-
(oc.extensions.bruno as any).openapi =
|
|
275
|
-
sourceUrl: entry.sourceUrl,
|
|
276
|
-
groupBy: entry.groupBy,
|
|
277
|
-
...(entry.lastSyncDate && { lastSyncDate: entry.lastSyncDate }),
|
|
278
|
-
...(entry.specHash && { specHash: entry.specHash }),
|
|
279
|
-
autoCheck: entry.autoCheck !== false,
|
|
280
|
-
autoCheckInterval: entry.autoCheckInterval || 5
|
|
281
|
-
}));
|
|
291
|
+
(oc.extensions.bruno as any).openapi = openApiEntries;
|
|
282
292
|
}
|
|
283
293
|
|
|
284
294
|
return stringifyYml(oc);
|
|
@@ -88,4 +88,62 @@ describe('stringifyEnvironment', () => {
|
|
|
88
88
|
const reparsed = parseEnvironment(stringifyEnvironment(env));
|
|
89
89
|
expect(reparsed.color).toBe('#ff0000');
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
describe('extends', () => {
|
|
93
|
+
it('round-trips the parent environment name', () => {
|
|
94
|
+
const env = {
|
|
95
|
+
uid: 'env-uid',
|
|
96
|
+
name: 'prod',
|
|
97
|
+
extends: 'base',
|
|
98
|
+
variables: [{ uid: 'u1', name: 'host', value: 'prod.example.com', type: 'text', enabled: true, secret: false }]
|
|
99
|
+
} as any;
|
|
100
|
+
|
|
101
|
+
const reparsed = parseEnvironment(stringifyEnvironment(env));
|
|
102
|
+
|
|
103
|
+
expect(reparsed.extends).toBe('base');
|
|
104
|
+
expect(reparsed.variables[0]).toMatchObject({ name: 'host', value: 'prod.example.com' });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('omits extends when no parent is set', () => {
|
|
108
|
+
const env = { uid: 'env-uid', name: 'prod', variables: [] } as any;
|
|
109
|
+
|
|
110
|
+
expect(stringifyEnvironment(env)).not.toContain('extends');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('omits extends when the parent names nothing', () => {
|
|
114
|
+
const env = { uid: 'env-uid', name: 'prod', extends: ' ', variables: [] } as any;
|
|
115
|
+
|
|
116
|
+
expect(stringifyEnvironment(env)).not.toContain('extends');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('round-trips a list of parents', () => {
|
|
120
|
+
const env = { uid: 'env-uid', name: 'prod', extends: ['base', 'shared'], variables: [] } as any;
|
|
121
|
+
|
|
122
|
+
const reparsed = parseEnvironment(stringifyEnvironment(env));
|
|
123
|
+
|
|
124
|
+
expect(reparsed.extends).toEqual(['base', 'shared']);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('omits extends when the list of parents is empty', () => {
|
|
128
|
+
const env = { uid: 'env-uid', name: 'prod', extends: [], variables: [] } as any;
|
|
129
|
+
|
|
130
|
+
expect(stringifyEnvironment(env)).not.toContain('extends');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('never writes the inheritedFrom provenance of a resolved variable', () => {
|
|
134
|
+
const env = {
|
|
135
|
+
uid: 'env-uid',
|
|
136
|
+
name: 'prod',
|
|
137
|
+
extends: 'base',
|
|
138
|
+
variables: [
|
|
139
|
+
{ uid: 'u1', name: 'host', value: 'base.example.com', type: 'text', enabled: true, secret: false, inheritedFrom: 'base' }
|
|
140
|
+
]
|
|
141
|
+
} as any;
|
|
142
|
+
|
|
143
|
+
const yml = stringifyEnvironment(env);
|
|
144
|
+
|
|
145
|
+
expect(yml).not.toContain('inheritedFrom');
|
|
146
|
+
expect(parseEnvironment(yml).variables[0]).not.toHaveProperty('inheritedFrom');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
91
149
|
});
|
|
@@ -10,6 +10,7 @@ import { stringifyYml } from './utils';
|
|
|
10
10
|
import { ensureString } from '../../utils';
|
|
11
11
|
import { SECRET_PROVIDER_IDENTIFIER_FIELDS } from '../../constants';
|
|
12
12
|
import { hasTypedMetadata, toOpenCollectionTypedValue, serializeVariableValue } from './common/datatype';
|
|
13
|
+
import { validatedEnvironmentExtendsFrom } from '@usebruno/common/utils';
|
|
13
14
|
|
|
14
15
|
export const toOpenCollectionEnvironmentVariables = (variables: (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable)[]): (Variable | SecretVariable)[] | undefined => {
|
|
15
16
|
if (!variables?.length) {
|
|
@@ -90,6 +91,11 @@ const stringifyEnvironment = (environment: BrunoEnvironment): string => {
|
|
|
90
91
|
name: environment.name
|
|
91
92
|
};
|
|
92
93
|
|
|
94
|
+
const environmentExtendsFrom = validatedEnvironmentExtendsFrom(environment.extends);
|
|
95
|
+
if (environmentExtendsFrom) {
|
|
96
|
+
(ocEnvironment as any).extends = environmentExtendsFrom;
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
if (environment.color) {
|
|
94
100
|
ocEnvironment.color = environment.color;
|
|
95
101
|
}
|
|
@@ -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
|
+
});
|
|
@@ -12,6 +12,7 @@ import { toOpenCollectionVariables } from './common/variables';
|
|
|
12
12
|
import { toOpenCollectionActions } from './common/actions';
|
|
13
13
|
import { toOpenCollectionScripts } from './common/scripts';
|
|
14
14
|
import { stringifyYml } from './utils';
|
|
15
|
+
import { HTTP_SCRIPT_KEYS } from '@usebruno/common';
|
|
15
16
|
|
|
16
17
|
const hasRequestDefaults = (folderRoot: FolderRoot): boolean => {
|
|
17
18
|
const requestDefaults = folderRoot?.request;
|
|
@@ -40,9 +41,14 @@ const stringifyFolder = (folderRoot: FolderRoot): string => {
|
|
|
40
41
|
// info block
|
|
41
42
|
const info: FolderInfo = {
|
|
42
43
|
name: folderRoot.meta?.name || 'Untitled Folder',
|
|
43
|
-
type: 'folder'
|
|
44
|
-
seq: folderRoot.meta?.seq || 1
|
|
44
|
+
type: 'folder'
|
|
45
45
|
};
|
|
46
|
+
// Only write seq when the folder actually has a numeric one. Defaulting to 1 would
|
|
47
|
+
// force every seq-less folder into position 1 on disk and break alphabetical fallback.
|
|
48
|
+
const seq = folderRoot.meta?.seq;
|
|
49
|
+
if (typeof seq === 'number' && Number.isFinite(seq)) {
|
|
50
|
+
info.seq = seq;
|
|
51
|
+
}
|
|
46
52
|
ocFolder.info = info;
|
|
47
53
|
|
|
48
54
|
// request defaults
|
|
@@ -83,7 +89,8 @@ const stringifyFolder = (folderRoot: FolderRoot): string => {
|
|
|
83
89
|
|
|
84
90
|
// scripts
|
|
85
91
|
if (hasRequestScripts(folderRoot)) {
|
|
86
|
-
|
|
92
|
+
// TODO: Widen scope to include GRPC scripts once Collection/Folder level inheritance is added to GRPC.
|
|
93
|
+
const ocScripts: Scripts | undefined = toOpenCollectionScripts(folderRoot?.request, HTTP_SCRIPT_KEYS);
|
|
87
94
|
if (ocScripts) {
|
|
88
95
|
ocFolder.request.scripts = ocScripts;
|
|
89
96
|
}
|
|
@@ -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
|
+
});
|