Skip to content

Commit d0a8d15

Browse files
committed
support for other API clients
1 parent 99cfa26 commit d0a8d15

28 files changed

Lines changed: 4362 additions & 9 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "restbro",
3-
"version": "1.1.4",
3+
"version": "1.2.0",
44
"description": "A modern API testing tool similar to Postman and Insomnia",
55
"main": "dist/main/main/index.js",
66
"scripts": {

src/main/modules/__tests__/ipc-manager.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ describe('ipc-manager.ts', () => {
179179
IPC_CHANNELS.FILE_READ_BINARY,
180180
IPC_CHANNELS.FILE_PICK_FOR_UPLOAD,
181181
IPC_CHANNELS.IMPORT_PARSE_PREVIEW,
182+
IPC_CHANNELS.IMPORT_PARSE_FOLDER_PREVIEW,
183+
IPC_CHANNELS.IMPORT_PICK_FOLDER,
182184
IPC_CHANNELS.IMPORT_COMMIT,
183185
IPC_CHANNELS.COLLECTIONS_STATE_GET,
184186
IPC_CHANNELS.COLLECTIONS_STATE_SET,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { parseBruFile, dictToRecord, findBlock } from '../bruno-bru-parser';
3+
4+
describe('bruno .bru parser', () => {
5+
it('parses dict blocks with disabled keys', () => {
6+
const text = `meta {
7+
name: Get Users
8+
type: http
9+
seq: 1
10+
}
11+
12+
headers {
13+
Accept: application/json
14+
~X-Disabled: nope
15+
}
16+
`;
17+
const blocks = parseBruFile(text);
18+
expect(blocks).toHaveLength(2);
19+
expect(blocks[0].name).toBe('meta');
20+
if (blocks[0].body.kind !== 'dict') throw new Error('expected dict');
21+
expect(dictToRecord(blocks[0].body.entries)).toEqual({
22+
name: 'Get Users',
23+
type: 'http',
24+
seq: '1',
25+
});
26+
27+
if (blocks[1].body.kind !== 'dict') throw new Error('expected dict');
28+
expect(blocks[1].body.entries).toHaveLength(2);
29+
expect(blocks[1].body.entries[1]).toMatchObject({
30+
key: 'X-Disabled',
31+
value: 'nope',
32+
enabled: false,
33+
});
34+
// Disabled keys are dropped by dictToRecord.
35+
expect(dictToRecord(blocks[1].body.entries)).toEqual({
36+
Accept: 'application/json',
37+
});
38+
});
39+
40+
it('captures raw body blocks verbatim including braces', () => {
41+
const text = `body:json {
42+
{
43+
"name": "alice",
44+
"tags": { "x": 1 }
45+
}
46+
}
47+
`;
48+
const blocks = parseBruFile(text);
49+
const body = findBlock(blocks, 'body:json');
50+
expect(body).toBeDefined();
51+
if (body!.body.kind !== 'raw') throw new Error('expected raw');
52+
expect(body!.body.text).toContain('"name": "alice"');
53+
expect(body!.body.text).toContain('"tags": { "x": 1 }');
54+
});
55+
56+
it('throws on unmatched braces', () => {
57+
expect(() => parseBruFile('headers {\n a: b\n')).toThrow(/unmatched/);
58+
});
59+
60+
it('handles CRLF line endings', () => {
61+
const text = 'meta {\r\n name: X\r\n}\r\n';
62+
const blocks = parseBruFile(text);
63+
expect(blocks).toHaveLength(1);
64+
if (blocks[0].body.kind !== 'dict') throw new Error('expected dict');
65+
expect(dictToRecord(blocks[0].body.entries)).toEqual({ name: 'X' });
66+
});
67+
});
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
2+
import { promises as fs } from 'fs';
3+
import * as os from 'os';
4+
import * as path from 'path';
5+
import { isBrunoCollectionDir, mapBrunoCollection } from '../bruno';
6+
7+
async function writeFile(p: string, content: string): Promise<void> {
8+
await fs.mkdir(path.dirname(p), { recursive: true });
9+
await fs.writeFile(p, content, 'utf-8');
10+
}
11+
12+
describe('bruno collection importer', () => {
13+
let tmpDir = '';
14+
15+
beforeEach(async () => {
16+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'restbro-bruno-'));
17+
});
18+
19+
afterEach(async () => {
20+
await fs.rm(tmpDir, { recursive: true, force: true });
21+
});
22+
23+
it('isBrunoCollectionDir true when bruno.json declares collection', async () => {
24+
await writeFile(
25+
path.join(tmpDir, 'bruno.json'),
26+
JSON.stringify({ name: 'My API', version: '1', type: 'collection' })
27+
);
28+
expect(await isBrunoCollectionDir(tmpDir)).toBe(true);
29+
});
30+
31+
it('isBrunoCollectionDir false for an unrelated folder', async () => {
32+
await writeFile(path.join(tmpDir, 'README.md'), 'hi');
33+
expect(await isBrunoCollectionDir(tmpDir)).toBe(false);
34+
});
35+
36+
it('maps a typical collection: nested folders, request, env', async () => {
37+
await writeFile(
38+
path.join(tmpDir, 'bruno.json'),
39+
JSON.stringify({ name: 'Users API', version: '1', type: 'collection' })
40+
);
41+
42+
// Folder with custom display name
43+
await writeFile(
44+
path.join(tmpDir, 'Users', 'folder.bru'),
45+
`meta {\n name: User Endpoints\n}\n`
46+
);
47+
48+
// GET request with headers, query, bearer auth
49+
await writeFile(
50+
path.join(tmpDir, 'Users', 'GetUser.bru'),
51+
`meta {
52+
name: Get User
53+
type: http
54+
seq: 1
55+
}
56+
57+
get {
58+
url: {{baseUrl}}/users/{{id}}
59+
body: none
60+
auth: bearer
61+
}
62+
63+
headers {
64+
Accept: application/json
65+
}
66+
67+
params:query {
68+
verbose: true
69+
~skip: x
70+
}
71+
72+
auth:bearer {
73+
token: {{token}}
74+
}
75+
`
76+
);
77+
78+
// POST request with json body
79+
await writeFile(
80+
path.join(tmpDir, 'Users', 'CreateUser.bru'),
81+
`meta {
82+
name: Create User
83+
type: http
84+
seq: 2
85+
}
86+
87+
post {
88+
url: {{baseUrl}}/users
89+
body: json
90+
auth: none
91+
}
92+
93+
body:json {
94+
{
95+
"name": "alice"
96+
}
97+
}
98+
`
99+
);
100+
101+
// Environment
102+
await writeFile(
103+
path.join(tmpDir, 'environments', 'Dev.bru'),
104+
`vars {
105+
baseUrl: https://api.dev
106+
token: xyz
107+
}
108+
`
109+
);
110+
111+
const { rootFolder, environments } = await mapBrunoCollection(tmpDir);
112+
113+
expect(rootFolder.name).toBe('Users API');
114+
expect(rootFolder.children).toHaveLength(1);
115+
116+
const folder = rootFolder.children![0];
117+
expect(folder.type).toBe('folder');
118+
expect(folder.name).toBe('User Endpoints');
119+
expect(folder.children).toHaveLength(2);
120+
121+
// Children sorted alphabetically by file name → CreateUser, GetUser
122+
const create = folder.children!.find((c) => c.name === 'Create User')!;
123+
const get = folder.children!.find((c) => c.name === 'Get User')!;
124+
125+
expect(get.request!.method).toBe('GET');
126+
expect(get.request!.url).toBe('{{baseUrl}}/users/{{id}}');
127+
expect(get.request!.headers).toEqual({ Accept: 'application/json' });
128+
expect(get.request!.params).toEqual({ verbose: 'true' });
129+
expect(get.request!.auth).toEqual({
130+
type: 'bearer',
131+
config: { token: '{{token}}' },
132+
});
133+
134+
expect(create.request!.method).toBe('POST');
135+
expect(create.request!.body).toEqual({
136+
type: 'json',
137+
content: ' {\n "name": "alice"\n }',
138+
});
139+
140+
expect(environments).toHaveLength(1);
141+
expect(environments[0].name).toBe('Dev');
142+
expect(environments[0].variables).toEqual({
143+
baseUrl: 'https://api.dev',
144+
token: 'xyz',
145+
});
146+
});
147+
148+
it('skips files that do not contain a verb block', async () => {
149+
await writeFile(
150+
path.join(tmpDir, 'bruno.json'),
151+
JSON.stringify({ name: 'C', version: '1', type: 'collection' })
152+
);
153+
await writeFile(
154+
path.join(tmpDir, 'NotARequest.bru'),
155+
`meta {\n name: stub\n}\n`
156+
);
157+
const { rootFolder } = await mapBrunoCollection(tmpDir);
158+
expect(rootFolder.children).toEqual([]);
159+
});
160+
161+
it('falls back to filename when meta.name is missing', async () => {
162+
await writeFile(
163+
path.join(tmpDir, 'bruno.json'),
164+
JSON.stringify({ name: 'C', version: '1', type: 'collection' })
165+
);
166+
await writeFile(
167+
path.join(tmpDir, 'Ping.bru'),
168+
`get {\n url: https://x/ping\n body: none\n auth: none\n}\n`
169+
);
170+
const { rootFolder } = await mapBrunoCollection(tmpDir);
171+
expect(rootFolder.children).toHaveLength(1);
172+
expect(rootFolder.children![0].name).toBe('Ping');
173+
expect(rootFolder.children![0].request!.url).toBe('https://x/ping');
174+
});
175+
176+
it('throws when given a non-Bruno folder', async () => {
177+
await expect(mapBrunoCollection(tmpDir)).rejects.toThrow(
178+
/not a Bruno collection/
179+
);
180+
});
181+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { isCurlCommand, mapCurlCommand } from '../curl';
3+
4+
describe('curl importer', () => {
5+
describe('isCurlCommand', () => {
6+
it('detects a curl command', () => {
7+
expect(isCurlCommand('curl https://x')).toBe(true);
8+
expect(isCurlCommand(' CURL -X POST https://x')).toBe(true);
9+
});
10+
it('rejects non-curl text', () => {
11+
expect(isCurlCommand('GET https://x')).toBe(false);
12+
expect(isCurlCommand('')).toBe(false);
13+
});
14+
});
15+
16+
it('maps a typical curl command to a single-request collection', () => {
17+
const { rootFolder } = mapCurlCommand(
18+
`curl -X POST 'https://api.example.com/users' \\
19+
-H 'Accept: application/json' \\
20+
-H 'Content-Type: application/json' \\
21+
-d '{"name":"alice"}'`
22+
);
23+
expect(rootFolder.children).toHaveLength(1);
24+
const req = rootFolder.children![0].request!;
25+
expect(req.method).toBe('POST');
26+
expect(req.url).toBe('https://api.example.com/users');
27+
expect((req.headers as Record<string, string>).Accept).toBe(
28+
'application/json'
29+
);
30+
expect(req.body).toEqual({
31+
type: 'json',
32+
content: '{"name":"alice"}',
33+
});
34+
});
35+
});

0 commit comments

Comments
 (0)