@usebruno/filestore 0.9.0 → 0.11.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 +2 -0
- 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/index.d.ts.map +1 -1
- package/dist/formats/bru/utils/redact-large-text-blocks.d.ts +11 -0
- package/dist/formats/bru/utils/redact-large-text-blocks.d.ts.map +1 -0
- package/dist/formats/yml/common/app.d.ts +8 -0
- package/dist/formats/yml/common/app.d.ts.map +1 -0
- package/dist/formats/yml/common/auth.d.ts.map +1 -1
- package/dist/formats/yml/common/body.d.ts.map +1 -1
- package/dist/formats/yml/common/datatype.d.ts +14 -0
- package/dist/formats/yml/common/datatype.d.ts.map +1 -0
- package/dist/formats/yml/common/headers.d.ts.map +1 -1
- package/dist/formats/yml/common/variables.d.ts.map +1 -1
- package/dist/formats/yml/items/parseApp.d.ts +13 -0
- package/dist/formats/yml/items/parseApp.d.ts.map +1 -0
- 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/parseWebsocketRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyApp.d.ts +4 -0
- package/dist/formats/yml/items/stringifyApp.d.ts.map +1 -0
- 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/parseCollection.d.ts.map +1 -1
- package/dist/formats/yml/parseEnvironment.d.ts +3 -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/parseItem.d.ts.map +1 -1
- package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
- package/dist/formats/yml/stringifyEnvironment.d.ts +3 -1
- package/dist/formats/yml/stringifyEnvironment.d.ts.map +1 -1
- package/dist/formats/yml/stringifyItem.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/secretsJsonToEnvironments.d.ts +38 -0
- package/dist/utils/secretsJsonToEnvironments.d.ts.map +1 -0
- package/package.json +7 -5
- package/src/constants.ts +9 -0
- package/src/formats/bru/index.ts +52 -0
- package/src/formats/bru/tests/parseBruEnvironment.spec.js +63 -0
- package/src/formats/bru/tests/redact-large-text-blocks.spec.js +313 -0
- package/src/formats/bru/utils/redact-large-text-blocks.ts +102 -0
- package/src/formats/yml/common/actions.ts +1 -1
- package/src/formats/yml/common/app.ts +27 -0
- package/src/formats/yml/common/auth.ts +53 -1
- package/src/formats/yml/common/body.spec.ts +62 -0
- package/src/formats/yml/common/body.ts +27 -9
- package/src/formats/yml/common/datatype.spec.ts +110 -0
- package/src/formats/yml/common/datatype.ts +57 -0
- package/src/formats/yml/common/headers.ts +4 -0
- package/src/formats/yml/common/variables.spec.ts +127 -0
- package/src/formats/yml/common/variables.ts +20 -6
- package/src/formats/yml/items/parseApp.ts +37 -0
- package/src/formats/yml/items/parseGraphQLRequest.ts +8 -0
- package/src/formats/yml/items/parseGrpcRequest.ts +24 -3
- package/src/formats/yml/items/parseHttpRequest.ts +13 -0
- package/src/formats/yml/items/parseWebsocketRequest.ts +29 -9
- package/src/formats/yml/items/stringifyApp.ts +45 -0
- package/src/formats/yml/items/stringifyGraphQLRequest.ts +3 -0
- package/src/formats/yml/items/stringifyGrpcRequest.ts +9 -11
- package/src/formats/yml/items/stringifyHttpRequest.ts +10 -0
- package/src/formats/yml/items/stringifyWebsocketRequest.ts +22 -9
- package/src/formats/yml/parseCollection.spec.ts +70 -0
- package/src/formats/yml/parseCollection.ts +8 -3
- package/src/formats/yml/parseEnvironment.spec.ts +99 -0
- package/src/formats/yml/parseEnvironment.ts +69 -5
- package/src/formats/yml/parseFolder.spec.ts +52 -0
- package/src/formats/yml/parseFolder.ts +14 -21
- package/src/formats/yml/parseItem.spec.ts +147 -0
- package/src/formats/yml/parseItem.ts +4 -0
- package/src/formats/yml/stringifyCollection.spec.ts +77 -0
- package/src/formats/yml/stringifyCollection.ts +4 -0
- package/src/formats/yml/stringifyEnvironment.spec.ts +91 -0
- package/src/formats/yml/stringifyEnvironment.ts +59 -10
- package/src/formats/yml/stringifyFolder.spec.ts +47 -0
- package/src/formats/yml/stringifyItem.spec.ts +53 -0
- package/src/formats/yml/stringifyItem.ts +4 -0
- package/src/formats/yml/tests/fixtures/presets/empty-request.yml +7 -0
- package/src/formats/yml/tests/fixtures/presets/no-presets.yml +3 -0
- package/src/formats/yml/tests/fixtures/presets/no-request-key.yml +6 -0
- package/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml +24 -0
- package/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml +25 -0
- package/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml +9 -0
- package/src/formats/yml/tests/parseCollection.spec.js +63 -0
- package/src/formats/yml/tests/parseEnvironment.spec.js +122 -0
- package/src/formats/yml/tests/parseEnvironment.spec.ts +160 -0
- package/src/formats/yml/tests/stringifyEnvironment.spec.ts +204 -0
- package/src/formats/yml/tests/variables.spec.js +34 -0
- package/src/index.ts +5 -0
- package/src/types/bruno-lang.d.ts +1 -0
- package/src/types.ts +7 -0
- package/src/utils/secretsJsonToEnvironments.spec.ts +211 -0
- package/src/utils/secretsJsonToEnvironments.ts +129 -0
- package/dist/formats/bru/tests/example-status-swap.spec.d.ts +0 -2
- package/dist/formats/bru/tests/example-status-swap.spec.d.ts.map +0 -1
- package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts +0 -57
- package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts.map +0 -1
- package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts +0 -2
- package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts.map +0 -1
- package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts +0 -2
- package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts.map +0 -1
|
@@ -8,6 +8,7 @@ import type { Assertion } from '@opencollection/types/common/assertions';
|
|
|
8
8
|
import type { Action } from '@opencollection/types/common/actions';
|
|
9
9
|
import type { HttpRequestParam, HttpRequestBody } from '@opencollection/types/requests/http';
|
|
10
10
|
import { stringifyYml } from '../utils';
|
|
11
|
+
import { toOpenCollectionApp, OpenCollectionApp } from '../common/app';
|
|
11
12
|
import { toOpenCollectionAuth } from '../common/auth';
|
|
12
13
|
import { toOpenCollectionHttpHeaders, toOpenCollectionResponseHeaders } from '../common/headers';
|
|
13
14
|
import { toOpenCollectionParams } from '../common/params';
|
|
@@ -34,6 +35,9 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
|
|
|
34
35
|
if (item.tags?.length) {
|
|
35
36
|
info.tags = item.tags;
|
|
36
37
|
}
|
|
38
|
+
if (isNonEmptyString(item.description)) {
|
|
39
|
+
info.description = item.description;
|
|
40
|
+
}
|
|
37
41
|
ocRequest.info = info;
|
|
38
42
|
|
|
39
43
|
// http block
|
|
@@ -212,6 +216,12 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
|
|
|
212
216
|
ocRequest.docs = brunoRequest.docs;
|
|
213
217
|
}
|
|
214
218
|
|
|
219
|
+
// app
|
|
220
|
+
const app: OpenCollectionApp | undefined = toOpenCollectionApp(item.app);
|
|
221
|
+
if (app) {
|
|
222
|
+
(ocRequest as any).app = app;
|
|
223
|
+
}
|
|
224
|
+
|
|
215
225
|
return stringifyYml(ocRequest);
|
|
216
226
|
} catch (error) {
|
|
217
227
|
console.error('Error stringifying HTTP request:', error);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
|
|
2
2
|
import type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket';
|
|
3
|
-
import type { WebSocketRequest,
|
|
3
|
+
import type { WebSocketRequest, WebSocketRequestInfo, WebSocketRequestDetails, WebSocketRequestRuntime, WebSocketMessage, WebSocketMessageVariant } from '@opencollection/types/requests/websocket';
|
|
4
4
|
import type { Auth } from '@opencollection/types/common/auth';
|
|
5
5
|
import type { Scripts } from '@opencollection/types/common/scripts';
|
|
6
6
|
import type { Variable } from '@opencollection/types/common/variables';
|
|
@@ -28,6 +28,9 @@ const stringifyWebsocketRequest = (item: BrunoItem): string => {
|
|
|
28
28
|
if (item.tags?.length) {
|
|
29
29
|
info.tags = item.tags;
|
|
30
30
|
}
|
|
31
|
+
if (isNonEmptyString(item.description)) {
|
|
32
|
+
info.description = item.description;
|
|
33
|
+
}
|
|
31
34
|
ocRequest.info = info;
|
|
32
35
|
|
|
33
36
|
// websocket block
|
|
@@ -41,21 +44,31 @@ const stringifyWebsocketRequest = (item: BrunoItem): string => {
|
|
|
41
44
|
websocket.headers = headers;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
// message
|
|
47
|
+
// message: single message without a custom name uses flat WebSocketMessage (backward compatible),
|
|
48
|
+
// otherwise uses WebSocketMessageVariant[] to preserve names
|
|
45
49
|
if (brunoRequest.body?.mode === 'ws' && brunoRequest.body.ws?.length) {
|
|
46
50
|
const messages = brunoRequest.body.ws;
|
|
51
|
+
const hasCustomName = messages.length === 1 && messages[0].name && messages[0].name.trim().length > 0;
|
|
52
|
+
|
|
53
|
+
const hasContent = messages.length === 1 && (messages[0].content || '').trim().length > 0;
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
// update this when bruno app supports multiple messages
|
|
50
|
-
if (messages.length) {
|
|
55
|
+
if (messages.length === 1 && !hasCustomName && hasContent) {
|
|
51
56
|
const msg = messages[0];
|
|
52
57
|
const message: WebSocketMessage = {
|
|
53
|
-
type: (msg.type as '
|
|
58
|
+
type: (msg.type as WebSocketMessage['type']) || 'text',
|
|
54
59
|
data: msg.content || ''
|
|
55
60
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
websocket.message = message;
|
|
62
|
+
} else {
|
|
63
|
+
const variants: WebSocketMessageVariant[] = messages.map((msg, index) => ({
|
|
64
|
+
title: msg.name || `message ${index + 1}`,
|
|
65
|
+
selected: msg.selected || false,
|
|
66
|
+
message: {
|
|
67
|
+
type: (msg.type as WebSocketMessage['type']) || 'text',
|
|
68
|
+
data: msg.content || ''
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
websocket.message = variants;
|
|
59
72
|
}
|
|
60
73
|
}
|
|
61
74
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import parseCollection from './parseCollection';
|
|
2
|
+
|
|
3
|
+
// Typed `request.variables` propagate through parseCollection into the
|
|
4
|
+
// collectionRoot's vars.req with their dataType + coerced value.
|
|
5
|
+
|
|
6
|
+
describe('parseCollection — typed request.variables', () => {
|
|
7
|
+
it('coerces typed values, preserves raw value on un-coercible input, and treats explicit string as the implicit default', () => {
|
|
8
|
+
const yml = `opencollection: "1.0.0"
|
|
9
|
+
info:
|
|
10
|
+
name: c
|
|
11
|
+
|
|
12
|
+
request:
|
|
13
|
+
variables:
|
|
14
|
+
- name: count
|
|
15
|
+
value:
|
|
16
|
+
type: number
|
|
17
|
+
data: "42"
|
|
18
|
+
- name: enabled
|
|
19
|
+
value:
|
|
20
|
+
type: boolean
|
|
21
|
+
data: "true"
|
|
22
|
+
- name: config
|
|
23
|
+
value:
|
|
24
|
+
type: object
|
|
25
|
+
data: '{"a":1}'
|
|
26
|
+
- name: greeting
|
|
27
|
+
value:
|
|
28
|
+
type: string
|
|
29
|
+
data: hi
|
|
30
|
+
- name: plain
|
|
31
|
+
value: hello
|
|
32
|
+
- name: cfg
|
|
33
|
+
value:
|
|
34
|
+
type: object
|
|
35
|
+
data: 'not-json'
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const { collectionRoot } = parseCollection(yml);
|
|
39
|
+
const reqVars = collectionRoot.request!.vars!.req!;
|
|
40
|
+
|
|
41
|
+
expect(reqVars).toHaveLength(6);
|
|
42
|
+
expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
|
|
43
|
+
expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
|
|
44
|
+
expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
|
|
45
|
+
// Explicit `type: string` is the implicit default — no dataType materialized.
|
|
46
|
+
expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
|
|
47
|
+
expect(reqVars[3].dataType).toBeUndefined();
|
|
48
|
+
expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
|
|
49
|
+
expect(reqVars[4].dataType).toBeUndefined();
|
|
50
|
+
// Un-coercible: raw value preserved for the UI mismatch warning.
|
|
51
|
+
expect(reqVars[5]).toMatchObject({ name: 'cfg', value: 'not-json', dataType: 'object' });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('parseCollection — reading the collection version', () => {
|
|
56
|
+
it('reads the version from the file as text', () => {
|
|
57
|
+
const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n version: v1.0.0\n');
|
|
58
|
+
expect(brunoConfig.version).toBe('v1.0.0');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('turns a number version into text (2 becomes "2")', () => {
|
|
62
|
+
const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n version: 2\n');
|
|
63
|
+
expect(brunoConfig.version).toBe('2');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('has no version when the file does not have one', () => {
|
|
67
|
+
const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n');
|
|
68
|
+
expect(brunoConfig.version).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -7,6 +7,7 @@ import { toBrunoVariables } from './common/variables';
|
|
|
7
7
|
import { toBrunoPostResponseVariables } from './common/actions';
|
|
8
8
|
import { toBrunoScripts } from './common/scripts';
|
|
9
9
|
import { ensureString } from '../../utils';
|
|
10
|
+
import type { BrunoPresetsExtension } from '../../types';
|
|
10
11
|
|
|
11
12
|
interface ParsedCollection {
|
|
12
13
|
collectionRoot: FolderRoot;
|
|
@@ -25,6 +26,10 @@ const parseCollection = (ymlString: string): ParsedCollection => {
|
|
|
25
26
|
ignore: []
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
if (oc.info?.version != null && oc.info.version !== '') {
|
|
30
|
+
brunoConfig.version = ensureString(oc.info.version, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
const brunoExtension = (oc.extensions as any)?.bruno;
|
|
29
34
|
if (brunoExtension?.ignore && Array.isArray(brunoExtension.ignore)) {
|
|
30
35
|
brunoConfig.ignore = brunoExtension.ignore;
|
|
@@ -32,11 +37,11 @@ const parseCollection = (ymlString: string): ParsedCollection => {
|
|
|
32
37
|
|
|
33
38
|
// presets
|
|
34
39
|
if (brunoExtension?.presets) {
|
|
35
|
-
const presets = brunoExtension.presets as
|
|
40
|
+
const presets = brunoExtension.presets as BrunoPresetsExtension;
|
|
36
41
|
if (presets.request) {
|
|
37
42
|
brunoConfig.presets = {
|
|
38
|
-
requestType: presets.request.type ||
|
|
39
|
-
requestUrl: presets.request.url ||
|
|
43
|
+
requestType: presets.request.type || '',
|
|
44
|
+
requestUrl: presets.request.url || ''
|
|
40
45
|
};
|
|
41
46
|
}
|
|
42
47
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import parseEnvironment, { toBrunoEnvironmentVariables } from './parseEnvironment';
|
|
2
|
+
|
|
3
|
+
describe('toBrunoEnvironmentVariables', () => {
|
|
4
|
+
it('returns an empty array for null / empty input', () => {
|
|
5
|
+
expect(toBrunoEnvironmentVariables(null)).toEqual([]);
|
|
6
|
+
expect(toBrunoEnvironmentVariables(undefined)).toEqual([]);
|
|
7
|
+
expect(toBrunoEnvironmentVariables([])).toEqual([]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('parses a plain string variable as text with secret=false', () => {
|
|
11
|
+
const [variable] = toBrunoEnvironmentVariables([{ name: 'apiKey', value: 'abc' } as any]);
|
|
12
|
+
|
|
13
|
+
expect(variable).toMatchObject({
|
|
14
|
+
name: 'apiKey',
|
|
15
|
+
value: 'abc',
|
|
16
|
+
type: 'text',
|
|
17
|
+
enabled: true,
|
|
18
|
+
secret: false
|
|
19
|
+
});
|
|
20
|
+
expect(variable.uid).toEqual(expect.any(String));
|
|
21
|
+
expect(variable.dataType).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('parses typed values and attaches the dataType field', () => {
|
|
25
|
+
const vars = toBrunoEnvironmentVariables([
|
|
26
|
+
{ name: 'port', value: { type: 'number', data: '300' } } as any,
|
|
27
|
+
{ name: 'flag', value: { type: 'boolean', data: 'true' } } as any,
|
|
28
|
+
{ name: 'config', value: { type: 'object', data: '{"a":1}' } } as any
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
expect(vars[0]).toMatchObject({ name: 'port', value: 300, dataType: 'number', secret: false });
|
|
32
|
+
expect(vars[1]).toMatchObject({ name: 'flag', value: true, dataType: 'boolean', secret: false });
|
|
33
|
+
expect(vars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object', secret: false });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('parses secret variables with secret=true and attaches the dataType from `type`', () => {
|
|
37
|
+
const vars = toBrunoEnvironmentVariables([
|
|
38
|
+
{ secret: true, name: 'apiKey' } as any,
|
|
39
|
+
{ secret: true, name: 'port', type: 'number' } as any,
|
|
40
|
+
{ secret: true, name: 'flag', type: 'boolean', disabled: true } as any
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
expect(vars[0]).toMatchObject({ name: 'apiKey', secret: true, enabled: true, value: '' });
|
|
44
|
+
// A bare secret has no `type`, so no dataType materializes.
|
|
45
|
+
expect(vars[0].dataType).toBeUndefined();
|
|
46
|
+
expect(vars[1]).toMatchObject({ name: 'port', secret: true, enabled: true, dataType: 'number' });
|
|
47
|
+
expect(vars[2]).toMatchObject({ name: 'flag', secret: true, enabled: false, dataType: 'boolean' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('honors the disabled flag on non-secret variables', () => {
|
|
51
|
+
const [variable] = toBrunoEnvironmentVariables([
|
|
52
|
+
{ name: 'apiKey', value: 'abc', disabled: true } as any
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
expect(variable.enabled).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('parseEnvironment (full yml)', () => {
|
|
60
|
+
it('parses an environment with mixed plain, typed and secret variables', () => {
|
|
61
|
+
const yml = `name: test_env
|
|
62
|
+
variables:
|
|
63
|
+
- name: env_str
|
|
64
|
+
value: hello
|
|
65
|
+
- name: env_num
|
|
66
|
+
value:
|
|
67
|
+
type: number
|
|
68
|
+
data: "300"
|
|
69
|
+
- name: env_bool
|
|
70
|
+
value:
|
|
71
|
+
type: boolean
|
|
72
|
+
data: "true"
|
|
73
|
+
- name: env_obj
|
|
74
|
+
value:
|
|
75
|
+
type: object
|
|
76
|
+
data: '{"scope":"env"}'
|
|
77
|
+
- secret: true
|
|
78
|
+
name: env_secret
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const env = parseEnvironment(yml);
|
|
82
|
+
|
|
83
|
+
expect(env.name).toBe('test_env');
|
|
84
|
+
expect(env.variables).toHaveLength(5);
|
|
85
|
+
|
|
86
|
+
expect(env.variables[0]).toMatchObject({ name: 'env_str', value: 'hello', secret: false });
|
|
87
|
+
expect(env.variables[1]).toMatchObject({ name: 'env_num', value: 300, dataType: 'number', secret: false });
|
|
88
|
+
expect(env.variables[2]).toMatchObject({ name: 'env_bool', value: true, dataType: 'boolean', secret: false });
|
|
89
|
+
expect(env.variables[3]).toMatchObject({ name: 'env_obj', value: { scope: 'env' }, dataType: 'object', secret: false });
|
|
90
|
+
expect(env.variables[4]).toMatchObject({ name: 'env_secret', secret: true, value: '' });
|
|
91
|
+
expect(env.variables[4].dataType).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('defaults environment name when missing', () => {
|
|
95
|
+
const env = parseEnvironment('variables: []');
|
|
96
|
+
expect(env.name).toBe('Untitled Environment');
|
|
97
|
+
expect(env.variables).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Environment as BrunoEnvironment,
|
|
3
|
+
EnvironmentVariable as BrunoEnvironmentVariable,
|
|
4
|
+
SecretEnvironmentVariable as BrunoSecretEnvironmentVariable,
|
|
5
|
+
ExternalSecretProviderType as BrunoExternalSecretProviderType
|
|
6
|
+
} from '@usebruno/schema-types/collection/environment';
|
|
2
7
|
import type { Environment } from '@opencollection/types/config/environments';
|
|
3
8
|
import type { Variable, SecretVariable } from '@opencollection/types/common/variables';
|
|
4
9
|
import { parseYml } from './utils';
|
|
5
10
|
import { uuid, ensureString } from '../../utils';
|
|
11
|
+
import { SECRET_PROVIDER_IDENTIFIER_FIELDS } from '../../constants';
|
|
12
|
+
import { isTypedValue, fromOpenCollectionTypedValue } from './common/datatype';
|
|
6
13
|
|
|
7
14
|
const isSecretVariable = (v: Variable | SecretVariable): v is SecretVariable => {
|
|
8
15
|
return 'secret' in v && v.secret === true;
|
|
9
16
|
};
|
|
10
17
|
|
|
11
|
-
const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] | null | undefined): BrunoEnvironmentVariable[] => {
|
|
18
|
+
export const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] | null | undefined): (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable)[] => {
|
|
12
19
|
if (!variables?.length) {
|
|
13
20
|
return [];
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
return variables.map((v): BrunoEnvironmentVariable => {
|
|
23
|
+
return variables.map((v): (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable) => {
|
|
17
24
|
if (isSecretVariable(v)) {
|
|
18
|
-
|
|
25
|
+
const variable: BrunoEnvironmentVariable = {
|
|
19
26
|
uid: uuid(),
|
|
20
27
|
name: ensureString(v.name),
|
|
21
28
|
value: '',
|
|
@@ -23,19 +30,71 @@ const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] |
|
|
|
23
30
|
enabled: v.disabled !== true,
|
|
24
31
|
secret: true
|
|
25
32
|
};
|
|
33
|
+
|
|
34
|
+
if (v.type && v.type !== 'string' && v.type !== 'null') {
|
|
35
|
+
variable.dataType = v.type;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (v.description) {
|
|
39
|
+
variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return variable;
|
|
26
43
|
}
|
|
44
|
+
|
|
27
45
|
const variable: BrunoEnvironmentVariable = {
|
|
28
46
|
uid: uuid(),
|
|
29
47
|
name: ensureString(v.name),
|
|
30
|
-
value:
|
|
48
|
+
value: '',
|
|
31
49
|
type: 'text',
|
|
32
50
|
enabled: v.disabled !== true,
|
|
33
51
|
secret: false
|
|
34
52
|
};
|
|
53
|
+
|
|
54
|
+
if (isTypedValue(v.value)) {
|
|
55
|
+
Object.assign(variable, fromOpenCollectionTypedValue(v.value));
|
|
56
|
+
} else {
|
|
57
|
+
variable.value = ensureString(v.value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (v.description) {
|
|
61
|
+
variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
|
|
62
|
+
}
|
|
63
|
+
|
|
35
64
|
return variable;
|
|
36
65
|
});
|
|
37
66
|
};
|
|
38
67
|
|
|
68
|
+
// Parses the yml `externalSecrets: { type, variables: [{ name, path|secretName|vaultName }] }`
|
|
69
|
+
// block into the runtime shape. `uid` and `enabled` are added here; the fetched `value`,
|
|
70
|
+
// `providerConfig` and `lastSyncedAt` are runtime-only and never present in the file.
|
|
71
|
+
const toBrunoExternalSecrets = (externalSecrets: any): BrunoEnvironment['externalSecrets'] | undefined => {
|
|
72
|
+
if (!externalSecrets || typeof externalSecrets !== 'object') {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const type = ensureString(externalSecrets.type) as BrunoExternalSecretProviderType;
|
|
77
|
+
const identifierField = SECRET_PROVIDER_IDENTIFIER_FIELDS[type];
|
|
78
|
+
if (!identifierField) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Keep any named entry, even one with an empty identifier — a half-filled secret row is the user's
|
|
83
|
+
// data; dropping it here would silently delete it on the next save.
|
|
84
|
+
const variables = Array.isArray(externalSecrets.variables)
|
|
85
|
+
? externalSecrets.variables
|
|
86
|
+
.filter((v: any) => v && v.name)
|
|
87
|
+
.map((v: any) => ({
|
|
88
|
+
uid: uuid(),
|
|
89
|
+
name: ensureString(v.name),
|
|
90
|
+
enabled: v.disabled !== true,
|
|
91
|
+
[identifierField]: ensureString(v[identifierField])
|
|
92
|
+
}))
|
|
93
|
+
: [];
|
|
94
|
+
|
|
95
|
+
return { type, variables } as BrunoEnvironment['externalSecrets'];
|
|
96
|
+
};
|
|
97
|
+
|
|
39
98
|
const parseEnvironment = (ymlString: string): BrunoEnvironment => {
|
|
40
99
|
try {
|
|
41
100
|
const ocEnvironment: Environment = parseYml(ymlString);
|
|
@@ -47,6 +106,11 @@ const parseEnvironment = (ymlString: string): BrunoEnvironment => {
|
|
|
47
106
|
color: ocEnvironment.color || null
|
|
48
107
|
};
|
|
49
108
|
|
|
109
|
+
const externalSecrets = toBrunoExternalSecrets((ocEnvironment as any).externalSecrets);
|
|
110
|
+
if (externalSecrets) {
|
|
111
|
+
brunoEnvironment.externalSecrets = externalSecrets;
|
|
112
|
+
}
|
|
113
|
+
|
|
50
114
|
return brunoEnvironment;
|
|
51
115
|
} catch (error) {
|
|
52
116
|
console.error('Error parsing environment:', error);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import parseFolder from './parseFolder';
|
|
2
|
+
|
|
3
|
+
// Typed `request.variables` propagate through parseFolder into the folder's
|
|
4
|
+
// vars.req with their dataType + coerced value.
|
|
5
|
+
|
|
6
|
+
describe('parseFolder — typed request.variables', () => {
|
|
7
|
+
it('coerces typed values, preserves raw value on un-coercible input, and treats explicit string as the implicit default', () => {
|
|
8
|
+
const yml = `info:
|
|
9
|
+
name: my-folder
|
|
10
|
+
|
|
11
|
+
request:
|
|
12
|
+
variables:
|
|
13
|
+
- name: count
|
|
14
|
+
value:
|
|
15
|
+
type: number
|
|
16
|
+
data: "42"
|
|
17
|
+
- name: enabled
|
|
18
|
+
value:
|
|
19
|
+
type: boolean
|
|
20
|
+
data: "true"
|
|
21
|
+
- name: config
|
|
22
|
+
value:
|
|
23
|
+
type: object
|
|
24
|
+
data: '{"a":1}'
|
|
25
|
+
- name: greeting
|
|
26
|
+
value:
|
|
27
|
+
type: string
|
|
28
|
+
data: hi
|
|
29
|
+
- name: plain
|
|
30
|
+
value: hello
|
|
31
|
+
- name: flag
|
|
32
|
+
value:
|
|
33
|
+
type: boolean
|
|
34
|
+
data: 'maybe'
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const folder = parseFolder(yml);
|
|
38
|
+
const reqVars = folder.request!.vars!.req!;
|
|
39
|
+
|
|
40
|
+
expect(reqVars).toHaveLength(6);
|
|
41
|
+
expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
|
|
42
|
+
expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
|
|
43
|
+
expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
|
|
44
|
+
// Explicit `type: string` is the implicit default — no dataType materialized.
|
|
45
|
+
expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
|
|
46
|
+
expect(reqVars[3].dataType).toBeUndefined();
|
|
47
|
+
expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
|
|
48
|
+
expect(reqVars[4].dataType).toBeUndefined();
|
|
49
|
+
// Un-coercible: raw value preserved for the UI mismatch warning.
|
|
50
|
+
expect(reqVars[5]).toMatchObject({ name: 'flag', value: 'maybe', dataType: 'boolean' });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -19,15 +19,9 @@ const parseFolder = (ymlString: string): FolderRoot => {
|
|
|
19
19
|
name: ensureString(info?.name, 'Untitled Folder'),
|
|
20
20
|
seq: info?.seq || 1
|
|
21
21
|
},
|
|
22
|
-
request:
|
|
23
|
-
docs: null
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// request defaults
|
|
27
|
-
if (ocFolder.request) {
|
|
28
|
-
folderRoot.request = {
|
|
22
|
+
request: {
|
|
29
23
|
headers: [],
|
|
30
|
-
auth:
|
|
24
|
+
auth: toBrunoAuth(ocFolder.request?.auth),
|
|
31
25
|
script: {
|
|
32
26
|
req: null,
|
|
33
27
|
res: null
|
|
@@ -37,40 +31,39 @@ const parseFolder = (ymlString: string): FolderRoot => {
|
|
|
37
31
|
res: []
|
|
38
32
|
},
|
|
39
33
|
tests: null
|
|
40
|
-
}
|
|
34
|
+
},
|
|
35
|
+
docs: null
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (ocFolder.request) {
|
|
39
|
+
const folderRequest = folderRoot.request!;
|
|
41
40
|
|
|
42
41
|
// headers
|
|
43
42
|
const headers = toBrunoHttpHeaders(ocFolder.request.headers);
|
|
44
43
|
if (headers) {
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// auth
|
|
49
|
-
const auth = toBrunoAuth(ocFolder.request.auth);
|
|
50
|
-
if (auth) {
|
|
51
|
-
folderRoot.request.auth = auth;
|
|
44
|
+
folderRequest.headers = headers;
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
// variables
|
|
55
48
|
const variables = toBrunoVariables(ocFolder.request.variables);
|
|
56
49
|
const postResponseVars = toBrunoPostResponseVariables((ocFolder.request as any).actions);
|
|
57
|
-
|
|
50
|
+
folderRequest.vars = {
|
|
58
51
|
req: variables.req,
|
|
59
52
|
res: postResponseVars
|
|
60
53
|
};
|
|
61
54
|
|
|
62
55
|
// scripts
|
|
63
56
|
const scripts = toBrunoScripts(ocFolder.request.scripts);
|
|
64
|
-
if (scripts?.script &&
|
|
57
|
+
if (scripts?.script && folderRequest.script) {
|
|
65
58
|
if (scripts.script.req) {
|
|
66
|
-
|
|
59
|
+
folderRequest.script.req = scripts.script.req;
|
|
67
60
|
}
|
|
68
61
|
if (scripts.script.res) {
|
|
69
|
-
|
|
62
|
+
folderRequest.script.res = scripts.script.res;
|
|
70
63
|
}
|
|
71
64
|
}
|
|
72
65
|
if (scripts?.tests) {
|
|
73
|
-
|
|
66
|
+
folderRequest.tests = scripts.tests;
|
|
74
67
|
}
|
|
75
68
|
}
|
|
76
69
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import parseItem from './parseItem';
|
|
2
|
+
|
|
3
|
+
// Typed `runtime.variables` propagate through parseItem with their dataType
|
|
4
|
+
// + coerced value. Helper-level coverage lives in the variables/datatype specs.
|
|
5
|
+
|
|
6
|
+
describe('parseItem — typed runtime.variables', () => {
|
|
7
|
+
it('coerces typed values, preserves raw value on un-coercible input, and treats explicit string as the implicit default', () => {
|
|
8
|
+
const yml = `info:
|
|
9
|
+
name: r
|
|
10
|
+
type: http
|
|
11
|
+
|
|
12
|
+
http:
|
|
13
|
+
url: https://example.com
|
|
14
|
+
method: GET
|
|
15
|
+
|
|
16
|
+
runtime:
|
|
17
|
+
variables:
|
|
18
|
+
- name: count
|
|
19
|
+
value:
|
|
20
|
+
type: number
|
|
21
|
+
data: "42"
|
|
22
|
+
- name: enabled
|
|
23
|
+
value:
|
|
24
|
+
type: boolean
|
|
25
|
+
data: "true"
|
|
26
|
+
- name: config
|
|
27
|
+
value:
|
|
28
|
+
type: object
|
|
29
|
+
data: '{"a":1}'
|
|
30
|
+
- name: greeting
|
|
31
|
+
value:
|
|
32
|
+
type: string
|
|
33
|
+
data: hi
|
|
34
|
+
- name: plain
|
|
35
|
+
value: hello
|
|
36
|
+
- name: bad
|
|
37
|
+
value:
|
|
38
|
+
type: number
|
|
39
|
+
data: 'not-a-number'
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const item = parseItem(yml);
|
|
43
|
+
const reqVars = item.request!.vars!.req!;
|
|
44
|
+
|
|
45
|
+
expect(reqVars).toHaveLength(6);
|
|
46
|
+
expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
|
|
47
|
+
expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
|
|
48
|
+
expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
|
|
49
|
+
// Explicit `type: string` is the implicit default — no dataType materialized.
|
|
50
|
+
expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
|
|
51
|
+
expect(reqVars[3].dataType).toBeUndefined();
|
|
52
|
+
expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
|
|
53
|
+
expect(reqVars[4].dataType).toBeUndefined();
|
|
54
|
+
// Un-coercible: raw value preserved for the UI mismatch warning.
|
|
55
|
+
expect(reqVars[5]).toMatchObject({ name: 'bad', value: 'not-a-number', dataType: 'number' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('normalizes raw YAML scalars in `data` to a string before coercing', () => {
|
|
59
|
+
// YAML parses unquoted scalars per their natural JS type. The `String(... ?? '')`
|
|
60
|
+
// cast in fromOpenCollectionTypedValue keeps coercion consistent.
|
|
61
|
+
const yml = `info:
|
|
62
|
+
name: r
|
|
63
|
+
type: http
|
|
64
|
+
|
|
65
|
+
http:
|
|
66
|
+
url: https://example.com
|
|
67
|
+
method: GET
|
|
68
|
+
|
|
69
|
+
runtime:
|
|
70
|
+
variables:
|
|
71
|
+
- name: stringy
|
|
72
|
+
value:
|
|
73
|
+
type: string
|
|
74
|
+
data: 42
|
|
75
|
+
- name: numeric
|
|
76
|
+
value:
|
|
77
|
+
type: number
|
|
78
|
+
data: 42
|
|
79
|
+
- name: nullish
|
|
80
|
+
value:
|
|
81
|
+
type: number
|
|
82
|
+
data: null
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const item = parseItem(yml);
|
|
86
|
+
const reqVars = item.request!.vars!.req!;
|
|
87
|
+
|
|
88
|
+
// type=string: raw YAML number → string; no dataType field.
|
|
89
|
+
expect(reqVars[0]).toMatchObject({ name: 'stringy', value: '42' });
|
|
90
|
+
expect(reqVars[0].dataType).toBeUndefined();
|
|
91
|
+
// type=number: '42' → 42.
|
|
92
|
+
expect(reqVars[1]).toMatchObject({ name: 'numeric', value: 42, dataType: 'number' });
|
|
93
|
+
// data: null → '' (the `?? ''` arm); coerce bails on empty → raw ''.
|
|
94
|
+
expect(reqVars[2]).toMatchObject({ name: 'nullish', value: '', dataType: 'number' });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('propagates typed variables for graphql/grpc/websocket requests too', () => {
|
|
98
|
+
const graphqlYml = `info:
|
|
99
|
+
name: g
|
|
100
|
+
type: graphql
|
|
101
|
+
|
|
102
|
+
graphql:
|
|
103
|
+
url: https://example.com/graphql
|
|
104
|
+
method: POST
|
|
105
|
+
|
|
106
|
+
runtime:
|
|
107
|
+
variables:
|
|
108
|
+
- name: count
|
|
109
|
+
value: { type: number, data: '7' }
|
|
110
|
+
`;
|
|
111
|
+
expect(parseItem(graphqlYml).request!.vars!.req![0]).toMatchObject({
|
|
112
|
+
name: 'count', value: 7, dataType: 'number'
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const grpcYml = `info:
|
|
116
|
+
name: gr
|
|
117
|
+
type: grpc
|
|
118
|
+
|
|
119
|
+
grpc:
|
|
120
|
+
url: localhost:50051
|
|
121
|
+
|
|
122
|
+
runtime:
|
|
123
|
+
variables:
|
|
124
|
+
- name: flag
|
|
125
|
+
value: { type: boolean, data: 'false' }
|
|
126
|
+
`;
|
|
127
|
+
expect(parseItem(grpcYml).request!.vars!.req![0]).toMatchObject({
|
|
128
|
+
name: 'flag', value: false, dataType: 'boolean'
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const wsYml = `info:
|
|
132
|
+
name: ws
|
|
133
|
+
type: websocket
|
|
134
|
+
|
|
135
|
+
websocket:
|
|
136
|
+
url: wss://example.com/socket
|
|
137
|
+
|
|
138
|
+
runtime:
|
|
139
|
+
variables:
|
|
140
|
+
- name: payload
|
|
141
|
+
value: { type: object, data: '{"k":1}' }
|
|
142
|
+
`;
|
|
143
|
+
expect(parseItem(wsYml).request!.vars!.req![0]).toMatchObject({
|
|
144
|
+
name: 'payload', value: { k: 1 }, dataType: 'object'
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|