@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
|
@@ -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
|
|
44
|
-
requestUrl: presets.request
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
...
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
+
});
|