@matimo/bruno 0.1.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/README.md +174 -0
- package/definition.yaml +33 -0
- package/package.json +17 -0
- package/tools/bru-utils.ts +51 -0
- package/tools/bruno_add_request/definition.yaml +108 -0
- package/tools/bruno_add_request/index.ts +101 -0
- package/tools/bruno_create_collection/definition.yaml +46 -0
- package/tools/bruno_create_collection/index.ts +55 -0
- package/tools/bruno_get_collection_info/definition.yaml +61 -0
- package/tools/bruno_get_collection_info/index.ts +119 -0
- package/tools/bruno_import_openapi/definition.yaml +75 -0
- package/tools/bruno_import_openapi/index.ts +102 -0
- package/tools/bruno_list_collections/definition.yaml +49 -0
- package/tools/bruno_list_collections/index.ts +101 -0
- package/tools/bruno_run_collection/definition.yaml +131 -0
- package/tools/bruno_run_collection/index.ts +99 -0
- package/tools/bruno_run_request/definition.yaml +69 -0
- package/tools/bruno_run_request/index.ts +104 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getGlobalMatimoLogger } from '@matimo/core';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
const logger = getGlobalMatimoLogger();
|
|
6
|
+
|
|
7
|
+
interface RequestInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
method: string;
|
|
10
|
+
url: string;
|
|
11
|
+
path: string;
|
|
12
|
+
tags: string[];
|
|
13
|
+
has_tests: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function scanBruFiles(dir: string, results: RequestInfo[]): Promise<void> {
|
|
17
|
+
let entries: string[];
|
|
18
|
+
try {
|
|
19
|
+
entries = await fs.readdir(dir);
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const fullPath = path.join(dir, entry);
|
|
25
|
+
try {
|
|
26
|
+
const stat = await fs.stat(fullPath);
|
|
27
|
+
if (stat.isDirectory() && entry !== 'node_modules') {
|
|
28
|
+
await scanBruFiles(fullPath, results);
|
|
29
|
+
} else if (!stat.isDirectory() && entry.endsWith('.bru')) {
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
32
|
+
const methodMatch = content.match(/^(get|post|put|patch|delete|head|options)\s*\{/im);
|
|
33
|
+
const method = methodMatch ? methodMatch[1].toUpperCase() : 'UNKNOWN';
|
|
34
|
+
const nameMatch = content.match(/meta\s*\{[^}]*name:\s*(.+)/i);
|
|
35
|
+
const name = nameMatch ? nameMatch[1].trim() : path.basename(entry, '.bru');
|
|
36
|
+
const urlMatch = content.match(/^\s+url:\s*(.+)$/m);
|
|
37
|
+
const url = urlMatch ? urlMatch[1].trim() : '';
|
|
38
|
+
const tagsMatch = content.match(/tags:\s*\[([^\]]*)\]/);
|
|
39
|
+
const tags = tagsMatch
|
|
40
|
+
? tagsMatch[1].split(',').map((t) => t.trim()).filter(Boolean)
|
|
41
|
+
: [];
|
|
42
|
+
const hasTests = /\btests\s*\{/.test(content);
|
|
43
|
+
results.push({ name, method, url, path: fullPath, tags, has_tests: hasTests });
|
|
44
|
+
} catch {
|
|
45
|
+
results.push({
|
|
46
|
+
name: path.basename(entry, '.bru'),
|
|
47
|
+
method: 'UNKNOWN',
|
|
48
|
+
url: '',
|
|
49
|
+
path: fullPath,
|
|
50
|
+
tags: [],
|
|
51
|
+
has_tests: false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// skip
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default async function execute(params: Record<string, unknown>): Promise<unknown> {
|
|
62
|
+
const collectionPath = params.collection_path as string;
|
|
63
|
+
|
|
64
|
+
if (!collectionPath) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
collection: undefined,
|
|
68
|
+
errors: ['collection_path parameter is required'],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
logger.info(`Getting collection info: ${collectionPath}`);
|
|
74
|
+
const absolutePath = path.resolve(collectionPath);
|
|
75
|
+
|
|
76
|
+
const brunoJsonPath = path.join(absolutePath, 'bruno.json');
|
|
77
|
+
let collectionName = path.basename(absolutePath);
|
|
78
|
+
|
|
79
|
+
// Verify the path exists before proceeding
|
|
80
|
+
try {
|
|
81
|
+
await fs.stat(absolutePath);
|
|
82
|
+
} catch {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
collection: undefined,
|
|
86
|
+
errors: [`Collection path not found: ${collectionPath}`],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const raw = await fs.readFile(brunoJsonPath, 'utf-8');
|
|
92
|
+
const parsed = JSON.parse(raw) as { name?: string };
|
|
93
|
+
if (parsed.name) collectionName = parsed.name;
|
|
94
|
+
} catch {
|
|
95
|
+
// bruno.json missing — use dirname as name
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const requests: RequestInfo[] = [];
|
|
99
|
+
await scanBruFiles(absolutePath, requests);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
collection: {
|
|
104
|
+
name: collectionName,
|
|
105
|
+
path: absolutePath,
|
|
106
|
+
requests,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.error(
|
|
111
|
+
`Get collection info failed: ${error instanceof Error ? error.message : String(error)}`
|
|
112
|
+
);
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
collection: undefined,
|
|
116
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: bruno_import_openapi
|
|
2
|
+
description: Bootstrap a new Bruno collection from an OpenAPI 3.0 specification (YAML or JSON). Enables agents to auto-generate test collections from API specs without manual setup.
|
|
3
|
+
version: '1.0.0'
|
|
4
|
+
status: approved
|
|
5
|
+
|
|
6
|
+
parameters:
|
|
7
|
+
spec_source:
|
|
8
|
+
type: string
|
|
9
|
+
required: true
|
|
10
|
+
description: Path or URL to OpenAPI specification (YAML or JSON)
|
|
11
|
+
|
|
12
|
+
output_directory:
|
|
13
|
+
type: string
|
|
14
|
+
required: true
|
|
15
|
+
description: Directory where imported collection will be created
|
|
16
|
+
|
|
17
|
+
collection_name:
|
|
18
|
+
type: string
|
|
19
|
+
required: false
|
|
20
|
+
description: Name for imported collection (defaults to spec name)
|
|
21
|
+
|
|
22
|
+
collection_format:
|
|
23
|
+
type: string
|
|
24
|
+
required: false
|
|
25
|
+
default: bru
|
|
26
|
+
description: Format for collection files - 'bru' (classic) or 'opencollection' (YAML)
|
|
27
|
+
|
|
28
|
+
group_by:
|
|
29
|
+
type: string
|
|
30
|
+
required: false
|
|
31
|
+
default: tags
|
|
32
|
+
description: Group requests by 'tags' or 'path'
|
|
33
|
+
|
|
34
|
+
insecure:
|
|
35
|
+
type: boolean
|
|
36
|
+
required: false
|
|
37
|
+
default: false
|
|
38
|
+
description: Skip TLS verification when source is HTTPS URL
|
|
39
|
+
|
|
40
|
+
execution:
|
|
41
|
+
type: function
|
|
42
|
+
code: index.ts
|
|
43
|
+
timeout: 30000
|
|
44
|
+
|
|
45
|
+
output_schema:
|
|
46
|
+
type: object
|
|
47
|
+
properties:
|
|
48
|
+
success:
|
|
49
|
+
type: boolean
|
|
50
|
+
collection_path:
|
|
51
|
+
type: string
|
|
52
|
+
collection_name:
|
|
53
|
+
type: string
|
|
54
|
+
requests_created:
|
|
55
|
+
type: number
|
|
56
|
+
message:
|
|
57
|
+
type: string
|
|
58
|
+
errors:
|
|
59
|
+
type: array
|
|
60
|
+
items:
|
|
61
|
+
type: string
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
examples:
|
|
65
|
+
- name: "Import from local OpenAPI YAML"
|
|
66
|
+
params:
|
|
67
|
+
spec_source: "./specs/payment-api.yaml"
|
|
68
|
+
output_directory: "./collections"
|
|
69
|
+
collection_name: "Payment API"
|
|
70
|
+
|
|
71
|
+
- name: "Import from remote OpenAPI URL"
|
|
72
|
+
params:
|
|
73
|
+
spec_source: "https://api.example.com/openapi.json"
|
|
74
|
+
output_directory: "./collections"
|
|
75
|
+
group_by: "path"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getGlobalMatimoLogger } from '@matimo/core';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { checkBruVersion } from '../bru-utils';
|
|
6
|
+
|
|
7
|
+
const logger = getGlobalMatimoLogger();
|
|
8
|
+
|
|
9
|
+
/** Count .bru files recursively — compatible with Node 18+. */
|
|
10
|
+
async function countBruFilesRecursively(dir: string): Promise<number> {
|
|
11
|
+
let count = 0;
|
|
12
|
+
let entries: string[];
|
|
13
|
+
try {
|
|
14
|
+
entries = await fs.readdir(dir);
|
|
15
|
+
} catch {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const fullPath = path.join(dir, entry);
|
|
20
|
+
try {
|
|
21
|
+
const stat = await fs.stat(fullPath);
|
|
22
|
+
if (stat.isDirectory() && entry !== 'node_modules') {
|
|
23
|
+
count += await countBruFilesRecursively(fullPath);
|
|
24
|
+
} else if (!stat.isDirectory() && entry.endsWith('.bru')) {
|
|
25
|
+
count++;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// skip
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return count;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async function execute(params: Record<string, unknown>): Promise<unknown> {
|
|
35
|
+
const specSource = params.spec_source as string;
|
|
36
|
+
const outputDirectory = params.output_directory as string;
|
|
37
|
+
|
|
38
|
+
if (!specSource || !outputDirectory) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
collection_path: '',
|
|
42
|
+
collection_name: '',
|
|
43
|
+
requests_created: 0,
|
|
44
|
+
message: 'spec_source and output_directory parameters are required',
|
|
45
|
+
errors: ['spec_source and output_directory parameters are required'],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
checkBruVersion();
|
|
50
|
+
|
|
51
|
+
const collectionName = (params.collection_name as string) || 'Imported Collection';
|
|
52
|
+
const absoluteOutput = path.resolve(outputDirectory);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
logger.info(`Importing OpenAPI from: ${specSource} to ${absoluteOutput}`);
|
|
56
|
+
|
|
57
|
+
const args: string[] = [
|
|
58
|
+
'import', 'openapi',
|
|
59
|
+
'--source', specSource,
|
|
60
|
+
'--output', absoluteOutput,
|
|
61
|
+
'--collection-name', collectionName,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
if (params.group_by) args.push('--group-by', params.group_by as string);
|
|
65
|
+
if (params.insecure === true) args.push('--insecure');
|
|
66
|
+
|
|
67
|
+
logger.debug(`Executing: bru ${args.join(' ')}`);
|
|
68
|
+
|
|
69
|
+
execFileSync('bru', args, { encoding: 'utf-8', stdio: 'pipe' });
|
|
70
|
+
|
|
71
|
+
logger.info('OpenAPI import completed');
|
|
72
|
+
|
|
73
|
+
// Count generated .bru files using a Node 18-compatible recursive walk
|
|
74
|
+
let requestsCreated = 0;
|
|
75
|
+
try {
|
|
76
|
+
requestsCreated = await countBruFilesRecursively(absoluteOutput);
|
|
77
|
+
} catch {
|
|
78
|
+
// best-effort count
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
collection_path: absoluteOutput,
|
|
84
|
+
collection_name: collectionName,
|
|
85
|
+
requests_created: requestsCreated,
|
|
86
|
+
message: `Collection "${collectionName}" imported from OpenAPI spec`,
|
|
87
|
+
errors: [],
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error(
|
|
91
|
+
`OpenAPI import failed: ${error instanceof Error ? error.message : String(error)}`
|
|
92
|
+
);
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
collection_path: absoluteOutput,
|
|
96
|
+
collection_name: collectionName,
|
|
97
|
+
requests_created: 0,
|
|
98
|
+
message: 'Import failed',
|
|
99
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: bruno_list_collections
|
|
2
|
+
description: Discover all Bruno API collections in a workspace directory. Returns collection metadata including name, path, and request count.
|
|
3
|
+
version: '1.0.0'
|
|
4
|
+
status: approved
|
|
5
|
+
|
|
6
|
+
parameters:
|
|
7
|
+
workspace_path:
|
|
8
|
+
type: string
|
|
9
|
+
required: true
|
|
10
|
+
description: Path to Bruno workspace directory
|
|
11
|
+
|
|
12
|
+
filter:
|
|
13
|
+
type: string
|
|
14
|
+
required: false
|
|
15
|
+
description: Filter collections by name (substring match)
|
|
16
|
+
|
|
17
|
+
execution:
|
|
18
|
+
type: function
|
|
19
|
+
code: index.ts
|
|
20
|
+
timeout: 10000
|
|
21
|
+
|
|
22
|
+
output_schema:
|
|
23
|
+
type: object
|
|
24
|
+
properties:
|
|
25
|
+
success:
|
|
26
|
+
type: boolean
|
|
27
|
+
collections:
|
|
28
|
+
type: array
|
|
29
|
+
items:
|
|
30
|
+
type: object
|
|
31
|
+
properties:
|
|
32
|
+
name:
|
|
33
|
+
type: string
|
|
34
|
+
path:
|
|
35
|
+
type: string
|
|
36
|
+
request_count:
|
|
37
|
+
type: number
|
|
38
|
+
required: ["name", "path", "request_count"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
examples:
|
|
42
|
+
- name: "List all collections in workspace"
|
|
43
|
+
params:
|
|
44
|
+
workspace_path: "./bruno-collections"
|
|
45
|
+
|
|
46
|
+
- name: "Filter collections by name"
|
|
47
|
+
params:
|
|
48
|
+
workspace_path: "./bruno-collections"
|
|
49
|
+
filter: "api"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { getGlobalMatimoLogger } from '@matimo/core';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
const logger = getGlobalMatimoLogger();
|
|
6
|
+
|
|
7
|
+
async function countBruFilesRecursively(dir: string): Promise<number> {
|
|
8
|
+
let count = 0;
|
|
9
|
+
let entries: string[];
|
|
10
|
+
try {
|
|
11
|
+
entries = await fs.readdir(dir);
|
|
12
|
+
} catch {
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const fullPath = path.join(dir, entry);
|
|
17
|
+
try {
|
|
18
|
+
const stat = await fs.stat(fullPath);
|
|
19
|
+
if (stat.isDirectory() && entry !== 'node_modules') {
|
|
20
|
+
count += await countBruFilesRecursively(fullPath);
|
|
21
|
+
} else if (!stat.isDirectory() && entry.endsWith('.bru')) {
|
|
22
|
+
count++;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// skip
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return count;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function findCollections(
|
|
32
|
+
dir: string
|
|
33
|
+
): Promise<Array<{ name: string; path: string; request_count: number }>> {
|
|
34
|
+
const results: Array<{ name: string; path: string; request_count: number }> = [];
|
|
35
|
+
let entries: string[];
|
|
36
|
+
try {
|
|
37
|
+
entries = await fs.readdir(dir);
|
|
38
|
+
} catch {
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (entries.includes('bruno.json')) {
|
|
43
|
+
const brunoJsonPath = path.join(dir, 'bruno.json');
|
|
44
|
+
let name = path.basename(dir);
|
|
45
|
+
try {
|
|
46
|
+
const raw = await fs.readFile(brunoJsonPath, 'utf-8');
|
|
47
|
+
const parsed = JSON.parse(raw) as { name?: string };
|
|
48
|
+
if (parsed.name) name = parsed.name;
|
|
49
|
+
} catch {
|
|
50
|
+
// use dirname
|
|
51
|
+
}
|
|
52
|
+
// Count .bru files recursively so nested requests are included
|
|
53
|
+
const requestCount = await countBruFilesRecursively(dir);
|
|
54
|
+
results.push({ name, path: dir, request_count: requestCount });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const fullPath = path.join(dir, entry);
|
|
59
|
+
try {
|
|
60
|
+
const stat = await fs.stat(fullPath);
|
|
61
|
+
if (stat.isDirectory() && entry !== 'node_modules') {
|
|
62
|
+
const nested = await findCollections(fullPath);
|
|
63
|
+
results.push(...nested);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// skip
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default async function execute(params: Record<string, unknown>): Promise<unknown> {
|
|
74
|
+
const workspacePath = params.workspace_path as string;
|
|
75
|
+
|
|
76
|
+
if (!workspacePath) {
|
|
77
|
+
return { success: false, collections: [], errors: ['workspace_path parameter is required'] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
logger.info(`Listing collections in: ${workspacePath}`);
|
|
82
|
+
const absolutePath = path.resolve(workspacePath);
|
|
83
|
+
let collections = await findCollections(absolutePath);
|
|
84
|
+
|
|
85
|
+
if (params.filter) {
|
|
86
|
+
const filter = (params.filter as string).toLowerCase();
|
|
87
|
+
collections = collections.filter((c) => c.name.toLowerCase().includes(filter));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { success: true, collections };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logger.error(
|
|
93
|
+
`List collections failed: ${error instanceof Error ? error.message : String(error)}`
|
|
94
|
+
);
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
collections: [],
|
|
98
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
name: bruno_run_collection
|
|
2
|
+
description: Execute a Bruno API collection with configurable environment, data files, and reporting options. Returns structured JSON results with test assertions, metrics, and failure details.
|
|
3
|
+
version: '1.0.0'
|
|
4
|
+
status: approved
|
|
5
|
+
|
|
6
|
+
parameters:
|
|
7
|
+
collection_path:
|
|
8
|
+
type: string
|
|
9
|
+
required: true
|
|
10
|
+
description: Path to Bruno collection file (.bru) or collection directory
|
|
11
|
+
|
|
12
|
+
environment:
|
|
13
|
+
type: string
|
|
14
|
+
required: false
|
|
15
|
+
description: Environment name to use for execution (e.g., dev, staging, prod)
|
|
16
|
+
|
|
17
|
+
env_file:
|
|
18
|
+
type: string
|
|
19
|
+
required: false
|
|
20
|
+
description: Path to environment file (.bru or .json) to override collection environment
|
|
21
|
+
|
|
22
|
+
data_file:
|
|
23
|
+
type: string
|
|
24
|
+
required: false
|
|
25
|
+
description: Path to CSV or JSON data file for data-driven testing
|
|
26
|
+
|
|
27
|
+
iteration_count:
|
|
28
|
+
type: number
|
|
29
|
+
required: false
|
|
30
|
+
default: 1
|
|
31
|
+
description: Number of times to run the collection
|
|
32
|
+
|
|
33
|
+
delay_ms:
|
|
34
|
+
type: number
|
|
35
|
+
required: false
|
|
36
|
+
description: Delay between each request in milliseconds
|
|
37
|
+
|
|
38
|
+
tags:
|
|
39
|
+
type: string
|
|
40
|
+
required: false
|
|
41
|
+
description: Comma-separated tags - only run requests with ALL specified tags
|
|
42
|
+
|
|
43
|
+
exclude_tags:
|
|
44
|
+
type: string
|
|
45
|
+
required: false
|
|
46
|
+
description: Comma-separated tags - skip requests with ANY of these tags
|
|
47
|
+
|
|
48
|
+
tests_only:
|
|
49
|
+
type: boolean
|
|
50
|
+
required: false
|
|
51
|
+
default: false
|
|
52
|
+
description: Only run requests that have tests or active assertions
|
|
53
|
+
|
|
54
|
+
bail_on_failure:
|
|
55
|
+
type: boolean
|
|
56
|
+
required: false
|
|
57
|
+
default: false
|
|
58
|
+
description: Stop execution after first failure
|
|
59
|
+
|
|
60
|
+
parallel:
|
|
61
|
+
type: boolean
|
|
62
|
+
required: false
|
|
63
|
+
default: false
|
|
64
|
+
description: Run requests in parallel order
|
|
65
|
+
|
|
66
|
+
sandbox_mode:
|
|
67
|
+
type: string
|
|
68
|
+
required: false
|
|
69
|
+
default: safe
|
|
70
|
+
description: JavaScript execution mode - 'safe' or 'developer'
|
|
71
|
+
|
|
72
|
+
report_path:
|
|
73
|
+
type: string
|
|
74
|
+
required: false
|
|
75
|
+
description: Custom path for JSON report output (defaults to temp directory)
|
|
76
|
+
|
|
77
|
+
execution:
|
|
78
|
+
type: function
|
|
79
|
+
code: index.ts
|
|
80
|
+
timeout: 120000
|
|
81
|
+
|
|
82
|
+
output_schema:
|
|
83
|
+
type: object
|
|
84
|
+
properties:
|
|
85
|
+
success:
|
|
86
|
+
type: boolean
|
|
87
|
+
summary:
|
|
88
|
+
type: object
|
|
89
|
+
properties:
|
|
90
|
+
total_requests:
|
|
91
|
+
type: number
|
|
92
|
+
passed:
|
|
93
|
+
type: number
|
|
94
|
+
failed:
|
|
95
|
+
type: number
|
|
96
|
+
execution_time_ms:
|
|
97
|
+
type: number
|
|
98
|
+
results:
|
|
99
|
+
type: array
|
|
100
|
+
items:
|
|
101
|
+
type: object
|
|
102
|
+
errors:
|
|
103
|
+
type: array
|
|
104
|
+
items:
|
|
105
|
+
type: string
|
|
106
|
+
report_path:
|
|
107
|
+
type: string
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
examples:
|
|
111
|
+
- name: "Run entire collection against staging"
|
|
112
|
+
params:
|
|
113
|
+
collection_path: "./collections/my-api"
|
|
114
|
+
environment: "staging"
|
|
115
|
+
report_format: json
|
|
116
|
+
report_path: "./reports/staging-results.json"
|
|
117
|
+
|
|
118
|
+
- name: "Run data-driven tests with bail on failure"
|
|
119
|
+
params:
|
|
120
|
+
collection_path: "./collections/auth-flow.bru"
|
|
121
|
+
data_file: "./data/users.csv"
|
|
122
|
+
bail_on_failure: true
|
|
123
|
+
report_format: html
|
|
124
|
+
report_path: "./reports/auth-flow.html"
|
|
125
|
+
|
|
126
|
+
- name: "Run only tagged tests in parallel"
|
|
127
|
+
params:
|
|
128
|
+
collection_path: "./collections/api"
|
|
129
|
+
tags: "smoke,critical"
|
|
130
|
+
parallel: true
|
|
131
|
+
iteration_count: 3
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { getGlobalMatimoLogger } from '@matimo/core';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { checkBruVersion } from '../bru-utils';
|
|
7
|
+
|
|
8
|
+
const logger = getGlobalMatimoLogger();
|
|
9
|
+
|
|
10
|
+
export default async function execute(params: Record<string, unknown>): Promise<unknown> {
|
|
11
|
+
const collectionPath = params.collection_path as string;
|
|
12
|
+
|
|
13
|
+
if (!collectionPath) {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
summary: { total_requests: 0, passed: 0, failed: 0, execution_time_ms: 0 },
|
|
17
|
+
results: [],
|
|
18
|
+
errors: ['collection_path parameter is required'],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
checkBruVersion();
|
|
23
|
+
|
|
24
|
+
const absolutePath = path.resolve(collectionPath);
|
|
25
|
+
const reportPath = path.resolve((params.report_path as string | undefined) ?? path.join(os.tmpdir(), `bru-report-${Date.now()}.json`));
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
logger.info(`Running Bruno collection: ${absolutePath}`);
|
|
29
|
+
|
|
30
|
+
const args: string[] = ['run', '.', '-r', '--reporter-json', reportPath];
|
|
31
|
+
|
|
32
|
+
if (params.environment) args.push('--env', params.environment as string);
|
|
33
|
+
if (params.env_file) args.push('--env-file', params.env_file as string);
|
|
34
|
+
if (params.data_file) args.push('--csv-file-path', params.data_file as string);
|
|
35
|
+
if (params.iteration_count) args.push('--iteration-count', String(params.iteration_count));
|
|
36
|
+
if (params.delay_ms) args.push('--delay', String(params.delay_ms));
|
|
37
|
+
if (params.tags) args.push('--tags', params.tags as string);
|
|
38
|
+
if (params.exclude_tags) args.push('--exclude-tags', params.exclude_tags as string);
|
|
39
|
+
if (params.tests_only === true) args.push('--tests-only');
|
|
40
|
+
if (params.bail_on_failure === true) args.push('--bail');
|
|
41
|
+
if (params.parallel === true) args.push('--parallel');
|
|
42
|
+
args.push('--sandbox', (params.sandbox_mode as string) || 'safe');
|
|
43
|
+
|
|
44
|
+
logger.debug(`Executing: bru ${args.join(' ')}`);
|
|
45
|
+
|
|
46
|
+
let exitCode = 0;
|
|
47
|
+
try {
|
|
48
|
+
execFileSync('bru', args, { encoding: 'utf-8', stdio: 'pipe', cwd: absolutePath });
|
|
49
|
+
} catch (execError) {
|
|
50
|
+
// bru exits non-zero when tests fail — that's OK, we still read the report
|
|
51
|
+
exitCode = 1;
|
|
52
|
+
logger.warn(`bru run exited with non-zero status: ${execError instanceof Error ? execError.message : String(execError)}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Read JSON report written by --reporter-json
|
|
56
|
+
let reportData: Record<string, unknown> = {};
|
|
57
|
+
try {
|
|
58
|
+
const raw = await fs.readFile(reportPath, 'utf-8');
|
|
59
|
+
reportData = JSON.parse(raw) as Record<string, unknown>;
|
|
60
|
+
} catch {
|
|
61
|
+
logger.warn('Could not read/parse JSON report');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Map Bruno report fields to schema-declared keys
|
|
65
|
+
const summaryRaw = (reportData.summary as Record<string, number> | undefined) ?? {};
|
|
66
|
+
const totalRequests = (summaryRaw.totalRequests ?? 0) as number;
|
|
67
|
+
const passed = (summaryRaw.passedRequests ?? 0) as number;
|
|
68
|
+
const failed = (summaryRaw.failedRequests ?? 0) as number;
|
|
69
|
+
const executionTimeMs = (summaryRaw.totalTime ?? 0) as number;
|
|
70
|
+
|
|
71
|
+
const rawResults = (reportData.results as unknown[]) ?? [];
|
|
72
|
+
const results = rawResults.map((r) => {
|
|
73
|
+
const req = r as Record<string, unknown>;
|
|
74
|
+
return {
|
|
75
|
+
name: req.suiteName ?? req.name ?? 'unknown',
|
|
76
|
+
success: req.status === 'pass' || req.passed === true,
|
|
77
|
+
status: (req.response as Record<string, unknown> | undefined)?.status ?? 0,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
success: exitCode === 0,
|
|
83
|
+
summary: { total_requests: totalRequests, passed, failed, execution_time_ms: executionTimeMs },
|
|
84
|
+
results,
|
|
85
|
+
report_path: reportPath,
|
|
86
|
+
errors: [],
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error(
|
|
90
|
+
`Collection execution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
91
|
+
);
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
summary: { total_requests: 0, passed: 0, failed: 0, execution_time_ms: 0 },
|
|
95
|
+
results: [],
|
|
96
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|