api-test-bruno
Use this skill when you need to parse multi-format API definitions and generate Bruno collections for executable regression; triggers include Bruno collections and Bruno API testing.
Install
npx skills add https://github.com/naodeng/awesome-qa-skills/tree/main/skills/en/testing-types/api-test-bruno
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install naodeng-awesome-qa-skills@llmmart
git clone https://github.com/naodeng/awesome-qa-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole naodeng/awesome-qa-skills collection as a plugin from our marketplace. Git is the plain clone.
README
api-test-bruno (EN)
Skill Overview
Need API outputs that should land in Bruno structure or Bruno workflow; The project already uses Bruno or wants Bruno-ready organization.
How to Use
- Open
SKILL.mdin this folder and confirm this skill fits your task. - In your AI tool, call
@skill api-test-bruno, then add your real project context and goal. - If you need a specific output format (table, checklist, report), include it directly in your request.
One-Click Install Script
Run from the repository root:
macOS / Linux
bash ./scripts/install-skills-mac.sh --tool codex --lang en --skill api-test-bruno
Windows PowerShell
powershell -ExecutionPolicy Bypass -File .\scripts\install-skills-windows.ps1 -Tool codex -Lang en -Skill api-test-bruno
Skill manifest
api-test-bruno (EN)
Chinese version: See the corresponding Chinese skill.
When to Use
- Need API outputs that should land in Bruno structure or Bruno workflow.
- The project already uses Bruno or wants Bruno-ready organization.
Workflow
- Read and follow the main prompt listed under Progressive disclosure (coverage, structure, quality bar).
- Add only project context that changes the result: scope, environment, constraints, risks, dependencies, expected deliverable.
- If input is incomplete, return a usable first draft and explicitly mark assumptions and gaps.
- Default to Markdown; switch formats only when the user asks.
Core Constraints
- Prioritize by risk / business impact — do not treat everything equally.
- Separate confirmed facts from current assumptions.
- Do not invent endpoints, fields, environments, or root causes the user did not provide.
- Use placeholders or env-var semantics for auth/secrets; never hardcode real credentials.
- Keep output executable: concrete scenarios, clear priority, clear next steps.
Progressive Disclosure
- Before producing output, read and follow
prompts/api-test-bruno.md(minimum coverage, output structure, quality bar). - When a ready-made template fits: use matching files under
output-templates/. - When the user wants examples or alignment with existing assets: read relevant
examples/. - For deep framework/troubleshoot/schema notes: read only the relevant file(s) under
references/, do not load the whole directory. - For format conversion or helper checks: prefer existing
scripts/over reinventing. - For evaluating/regressing this skill: use
evals/with skill-up.
Pre-delivery Checklist
- Followed the main prompt's output structure
- Minimum coverage focus: collection structure, environment setup, auth handling, priority endpoints, positive scenarios, negative and boundary scenarios, data or variable strategy, assertion focus, ... (details in main prompt)
- Covered the minimum checklist, or explained omissions
- High-risk items have explicit priority
- Did not invent details the user did not provide
- Assumptions and gaps are marked
Common Pitfalls
- Do not pretend completeness when scope/context is missing.
- Do not treat every item as equally important.
- Do not skip assumptions and information gaps.
- Do not dump generic theory unrelated to the current toolchain.
Files (awesome-qa-skills)
-
agents
-
openai.yaml 471 B
version: 1 metadata: key: "api-test-bruno" last_verified: "2026-03-24" interface: display_name: "API Test Bruno" short_description: "Use this skill when you need to parse multi-format API definitions and generate Bruno collections for executable regression; triggers include Bruno collections…" default_prompt: "Use api-test-bruno to complete the task with local scripts, prompts, and examples in this skill folder." policy: allow_implicit_invocation: true
-
-
evals
-
cases
-
basic-success.yaml 2.5 KB
id: "basic-success" title: "Bruno: OpenAPI/curl yields collection structure" description: | Use desensitized OpenAPI/curl fixtures to propose a Bruno collection structure. context: files: "evals/fixtures/openapi-orders.yaml": | openapi: 3.0.3 info: title: Sample Orders API (desensitized fixture) version: 0.1.0 paths: /orders: post: summary: Create order security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [skuId, qty] properties: skuId: { type: string } qty: { type: integer, minimum: 1 } responses: "201": description: Created /payments/callback: post: summary: Payment callback responses: "200": description: OK /orders/{id}: get: summary: Get order security: - bearerAuth: [] parameters: - name: id in: path required: true schema: { type: string } responses: "200": description: OK components: securitySchemes: bearerAuth: type: http scheme: bearer "evals/fixtures/sample-curls.sh": | curl -sS -X POST "${BASE_URL}/orders" \ -H "Authorization: Bearer ${API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"skuId":"SKU-1001","qty":1}' curl -sS -X POST "${BASE_URL}/payments/callback" \ -H "Content-Type: application/json" \ -d '{"orderId":"ORD-PLACEHOLDER","result":"SUCCESS"}' curl -sS "${BASE_URL}/orders/ORD-PLACEHOLDER" \ -H "Authorization: Bearer ${API_TOKEN}" input: prompt: | Use api-test-bruno. Fixtures are already in the workspace: - evals/fixtures/openapi-orders.yaml - evals/fixtures/sample-curls.sh Read both files, then propose a Bruno collection structure (.bru / folders / env vars). Env: SIT. Auth only via {{token}} / ${API_TOKEN}. No real tokens. No huge code dumps. expect: must_contain: - "Bruno" - ".bru" must_not_contain: - "TODO" - "I cannot" judge: type: script script_path: evals/fixtures/scripts/check_bruno_output.sh -
edge-bad-or-neighbor.yaml 668 B
id: edge-bad-or-neighbor title: "Bruno: redirect Postman/k6 neighbor requests" description: | When the user asks for Postman/k6 under the Bruno skill, stay on Bruno or clarify the boundary. input: prompt: | I activated api-test-bruno, but I asked: generate a Postman collection and a k6 load script. Endpoint: GET /health. Correct the scope: deliver a Bruno plan and explain why Postman/k6 are out of this skill's deliverable. expect: must_contain: - "Bruno" - "Postman" must_not_contain: - "TODO" - "I cannot" judge: type: rule_based success: - output_contains: all: - "Bruno" - "Open Questions" -
edge-incomplete-input.yaml 636 B
id: edge-incomplete-input title: "Bruno: one-line ask still yields draft" description: | With only one sentence and no schema, still draft Bruno assets and list gaps/assumptions. input: prompt: | Use api-test-bruno. I only know: please test the points redemption API. No OpenAPI, no fields, no path. Give a usable Bruno collection draft structure and list required gaps and assumptions. expect: must_contain: - "assumption" - "Open Questions" must_not_contain: - "TODO" - "I cannot" judge: type: rule_based success: - output_contains: all: - "assumption" - "Bruno"
-
-
fixtures
-
scripts
-
check_bruno_output.sh 1.3 KB
#!/usr/bin/env bash # Script judge for Bruno-oriented skill outputs. # skill-up provides: EVAL_FINAL_MESSAGE, EVAL_EXIT_CODE, EVAL_TRANSCRIPT_PATH set -euo pipefail text="${EVAL_FINAL_MESSAGE:-}" if [[ -z "$text" && -n "${EVAL_TRANSCRIPT_PATH:-}" && -f "$EVAL_TRANSCRIPT_PATH" ]]; then text="$(cat "$EVAL_TRANSCRIPT_PATH")" fi if [[ -z "$text" && -f "outputs/response.md" ]]; then text="$(cat outputs/response.md)" fi if [[ -z "$text" ]]; then # last resort: search workspace f="$(find . -name response.md 2>/dev/null | head -1 || true)" if [[ -n "$f" ]]; then text="$(cat "$f")" fi fi if [[ -z "$text" ]]; then echo "FAIL: empty final message / no response.md" exit 1 fi fail=0 need_any() { local label="$1" shift local ok=0 local k for k in "$@"; do if grep -Fqi -- "$k" <<<"$text"; then ok=1 break fi done if [[ "$ok" -eq 0 ]]; then echo "FAIL missing any of: $*" fail=1 else echo "OK: $label" fi } need_any "Bruno marker" "Bruno" "bruno" need_any "collection marker" "collection" "Collection" "集合" ".bru" if grep -Eiq 'Bearer [A-Za-z0-9_\-]{20,}' <<<"$text"; then echo "FAIL: looks like a hardcoded bearer token" fail=1 else echo "OK: no long bearer literal" fi if [[ "$fail" -ne 0 ]]; then exit 1 fi exit 0
-
-
openapi-orders.yaml 1.8 KB
openapi: 3.0.3 info: title: Sample Orders API (desensitized fixture) version: 0.1.0 paths: /orders: post: summary: Create order operationId: createOrder security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [skuId, qty] properties: skuId: type: string qty: type: integer minimum: 1 responses: "201": description: Created content: application/json: schema: type: object properties: orderId: type: string status: type: string "401": description: Unauthorized /payments/callback: post: summary: Payment callback operationId: paymentCallback requestBody: required: true content: application/json: schema: type: object required: [orderId, result] properties: orderId: type: string result: type: string enum: [SUCCESS, FAIL] responses: "200": description: OK /orders/{id}: get: summary: Get order operationId: getOrder security: - bearerAuth: [] parameters: - name: id in: path required: true schema: type: string responses: "200": description: OK components: securitySchemes: bearerAuth: type: http scheme: bearer -
sample-curls.sh 551 B
# Desensitized curl examples for API automation fixtures. # Use placeholders only — never real tokens. # Create order curl -sS -X POST "${BASE_URL}/orders" \ -H "Authorization: Bearer ${API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"skuId":"SKU-1001","qty":1}' # Payment callback curl -sS -X POST "${BASE_URL}/payments/callback" \ -H "Content-Type: application/json" \ -d '{"orderId":"ORD-PLACEHOLDER","result":"SUCCESS"}' # Get order curl -sS "${BASE_URL}/orders/ORD-PLACEHOLDER" \ -H "Authorization: Bearer ${API_TOKEN}"
-
-
eval.yaml 587 B
schema_version: v1alpha1 environment: type: none skills: - source: local_path path: . engine: name: claude_code # model is optional; omit to use engine default # model: # provider: anthropic # name: claude-sonnet-4-6 cases: files: - evals/cases/basic-success.yaml - evals/cases/edge-incomplete-input.yaml - evals/cases/edge-bad-or-neighbor.yaml defaults: timeout_seconds: 180 max_turns: 8 expect: exit_code: 0 must_not_contain: - "TODO" - "I cannot" benchmark: enabled: true report: formats: [json] -
trigger_queries.json 927 B
[ {"query": "Generate a Bruno collection from this OpenAPI for SIT with token vars", "should_trigger": true}, {"query": "Organize these curls into a Bruno collection with assertions", "should_trigger": true}, {"query": "How should I structure Bruno folders for orders and payment callback?", "should_trigger": true}, {"query": "Use Bruno for smoke coverage on auth-related APIs", "should_trigger": true}, {"query": "Parse swagger into an executable API collection — we use Bruno", "should_trigger": true}, {"query": "Load test /orders with k6, P95 under 300ms", "should_trigger": false}, {"query": "Write Pytest API automation, not Bruno", "should_trigger": false}, {"query": "Write functional test cases for checkout and shipping", "should_trigger": false}, {"query": "File a bug report for double charging", "should_trigger": false}, {"query": "Playwright E2E for login SSO", "should_trigger": false} ]
-
-
examples
-
bruno
-
sample.bru 137 B · in bundle
-
-
ci
-
github-actions-bruno.yml 853 B
name: API Bruno CI on: workflow_dispatch: pull_request: paths: - "explore/api-test-bruno/**" jobs: test: runs-on: ubuntu-latest defaults: run: working-directory: explore/api-test-bruno/scripts steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: "20" - name: Install Bruno CLI run: npm i -g @usebruno/cli - name: Run Bruno tests env: BASE_URL: ${{ secrets.API_BASE_URL }} API_TOKEN: ${{ secrets.API_TOKEN }} run: ./run-tests.sh ./templates/bruno staging - name: Upload reports if: always() uses: actions/upload-artifact@v4 with: name: bruno-reports path: explore/api-test-bruno/reports/** -
Jenkinsfile.bruno 599 B · in bundle
-
-
sample.curl 212 B · in bundle
-
sample.insomnia.json 502 B
{ "_type": "export", "__export_format": 4, "__export_date": "2026-03-23T00:00:00.000Z", "__export_source": "insomnia.desktop.app:v9.0.0", "resources": [ { "_id": "req_1", "_type": "request", "name": "List Products", "method": "GET", "url": "https://api.example.com/v1/products" }, { "_id": "req_2", "_type": "request", "name": "Create Product", "method": "POST", "url": "https://api.example.com/v1/products" } ] } -
sample.openapi.yaml 240 B
openapi: 3.0.3 info: title: Sample API version: 1.0.0 paths: /v1/users: get: summary: list users post: summary: create user /v1/users/{id}: get: summary: get user delete: summary: delete user -
sample.opencollection.json 243 B
{ "name": "OpenCollection Sample", "requests": [ { "name": "Get Orders", "method": "GET", "path": "/v1/orders" }, { "name": "Create Order", "method": "POST", "path": "/v1/orders" } ] } -
sample.postman_collection.json 704 B
{ "info": { "name": "Sample API Collection", "_postman_id": "b93a09ab-2222-4444-8888-444455556666", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { "name": "Get Users", "request": { "method": "GET", "url": { "raw": "https://api.example.com/v1/users", "path": [ "v1", "users" ] } } }, { "name": "Create User", "request": { "method": "POST", "url": { "raw": "https://api.example.com/v1/users", "path": [ "v1", "users" ] } } } ] } -
sample.wsdl 534 B · in bundle
-
-
output-templates
-
template-markdown.md 83 B
# Output Template ## Summary - Scope: - Key Points: ## Details - Item 1 - Item 2
-
-
prompts
-
api-test-bruno.md 5.7 KB
# API Test Bruno Prompt From the materials the user provides, produce a Bruno collection plan or test-asset structure the team can implement directly. ## Role - Act as a senior QA and API automation expert who turns API materials into a maintainable Bruno collection. ## Input parsing order Parse in this priority order. Higher priority wins on conflicts; when sources disagree, state the conflict and source — **do not silently invent a merged “truth”**: 1. Existing Bruno assets (`.bru` / `bruno.json` / collection tree) 2. OpenAPI / Swagger (`openapi.yaml` / `swagger.json`) 3. Postman Collection, Insomnia, or OpenCollection 4. curl examples (headers / query / body) 5. Loose notes (tables, Markdown, verbal endpoint lists) Also absorb when present: business scope, auth model, environment baseUrl, release priority, CI needs, existing folder conventions. Extract only paths, methods, params, fields, and sample values that **actually appear** in the materials. Put gaps in “missing information”; do not complete a fake full API doc. ## Defaults (use these unless the user specifies otherwise) Prefer defaults; do not present a tool menu. **Directory layout** ```text <collection-name>/ bruno.json environments/ local.bru staging.bru <folder-by-resource-or-flow>/ <request-name>.bru ``` **Naming** - collection `name`: short domain name (e.g. `order-api`) - request `meta.name`: `kebab-case` method+resource (e.g. `get-users`, `create-order`) - folders: by resource or critical flow — not one flat dump of every request **Environments and variables** - Standard vars: `{{baseUrl}}`, `{{token}}` (reuse project names if they already exist) - Secrets only as placeholders in `environments/*.bru` (e.g. `replace-me`) or “inject from CI secret / local env” - Request URLs: `{{baseUrl}}/path` — do not hardcode host into every `.bru` **Assertion style** - Each high-priority request: at least `status` + one critical response field (when the doc has fields) - Bruno `tests` blocks with `expect(res.getStatus()).to.equal(...)`; assert JSON fields when present - Separate layers via folders or name prefixes: `smoke` / `contract` / `business` / `negative` **Run defaults** - Local: Bruno CLI / GUI against a folder or tag - CI: smoke folder first, then expand regression; secrets via CI secrets, never committed If a collection already exists, **align to it** and apply these defaults only where gaps remain. ## Gotchas - **Never** hardcode real Bearer tokens, passwords, cookies, or private keys in examples, env files, or output; use placeholders or “read from env” notes. - When migrating from curl/Postman: **redact** Authorization / Cookie / signing headers before writing them into the plan. - **Do not invent** paths, query/header/body fields, status codes, or error codes the user did not provide; mark assumptions or gaps. - Do not rewrite the Bruno plan as Postman/Newman, pytest, k6, or other unrelated stacks. - If information is incomplete, still ship a usable first version (structure + confirmed endpoints) and list assumptions explicitly. - Unless the user asks for runnable `.bru` contents, prefer structure notes + key request points over huge full-file dumps. ## Minimum coverage checklist Unless the user explicitly narrows scope, the result must cover: - collection directory and folder split - environment variables (`baseUrl` / auth placeholders) - how auth and permission-related requests are handled - high-priority endpoints (with P0/P1) - positive scenarios - negative and boundary scenarios (at least documented validation/error paths) - variables and test-data strategy (create/cleanup needs) - assertion focus (status + critical fields) - smoke vs regression scope - CI or local run guidance - missing information and assumptions ## Output Return results in this order (keep sections; make each concrete): ### 1. Task Understanding - API / domain under test - goal (new collection / strengthen / migrate from another format) - in-scope endpoints or flows - out-of-scope or unclear areas - input sources (OpenAPI / Postman / curl / …) and how conflicts were handled ### 2. Bruno Collection Plan - proposed collection tree (concrete folder names) - `environments` variable list (name, purpose, placeholder example; no real secrets) - auth default: request-level / shared script / env vars — which layer - alignment with existing assets (if any) ### 3. Priority Request Coverage For each P0/P1 request: - `meta.name` / method / path (confirmed only) - folder - priority and risk rationale - positive checks - negative / boundary checks - assertion focus (status, fields) - prerequisite requests or variables ### 4. Execution Notes - suggested order (auth → writes → read checks → cleanup) - smoke folder / request list - regression expansion - release-blocking checks ### 5. Automation and CI Suggestions - how to run locally - minimal CI steps (Bruno CLI, select env, run smoke) - secret injection contract (variable names only) ### 6. Open Questions - information gaps - assumptions used this round (itemized) ## Pre-delivery checklist - [ ] Inputs followed the parsing order; conflicts and gaps are called out - [ ] Layout / naming / `{{baseUrl}}`+`{{token}}` placeholders match defaults (or explain reuse of existing) - [ ] No real secrets; no invented paths/fields/status codes - [ ] P0/P1 requests have concrete scenarios and assertions — not vague “cover happy and unhappy paths” - [ ] All six output sections present; smoke and CI are actionable ## Quality bar - Stay Bruno-specific: folders, request names, variable names. - Prioritize by risk; do not treat every endpoint equally. - Separate confirmed facts from assumptions. - Avoid huge full `.bru` dumps unless the user asks for runnable files.
-
-
references
-
local
-
api-testing_EN-1.md 487 B
# Archived Local Reference This file is a lightweight legacy note. The previous long snapshot was removed to avoid duplicate and outdated guidance. ## Use Instead - Main prompt: `prompts/api-test-bruno.md` - Main entry: `SKILL.md` ## Notes - Keep using the current prompt and `SKILL.md` as the source of truth. - Load `references/`, `examples/`, or `scripts/` only when the task really needs extra detail. - Do not rely on this file for the latest prompt wording or workflow rules. -
api-testing_EN.md 487 B
# Archived Local Reference This file is a lightweight legacy note. The previous long snapshot was removed to avoid duplicate and outdated guidance. ## Use Instead - Main prompt: `prompts/api-test-bruno.md` - Main entry: `SKILL.md` ## Notes - Keep using the current prompt and `SKILL.md` as the source of truth. - Load `references/`, `examples/`, or `scripts/` only when the task really needs extra detail. - Do not rely on this file for the latest prompt wording or workflow rules.
-
-
framework-spec.md 1.5 KB
# Bruno Framework Specification ## 1. Directory Convention - `scripts/parse_api_sources.py`: multi-format API source parser - `scripts/generate_bruno_requests.py`: normalized endpoint -> Bruno requests - `scripts/templates/bruno/`: runnable Bruno baseline collection - `generated-bruno/`: generated collection output ## 2. Test Layer Convention - `smoke`: core endpoint availability - `contract`: status code + response shape checks - `business`: key API flow and state transition checks - `negative`: invalid params, auth failure, boundary inputs ## 3. Assertion Rules - status code checks are mandatory - response-time checks for critical endpoints are recommended - negative tests must validate error structure - use environment-scoped variables for auth and host ## 4. Data and Environment Rules - use `{{baseUrl}}` and token variables - never commit secrets into collection files - keep test data deterministic or seed-controlled - define cleanup strategy for mutable endpoints ## 5. Parsing Scope Rules Supported parser inputs: - curl - Postman collections - Swagger/OpenAPI (including v3) - Bruno - OpenCollection - Insomnia - WSDL - ZIP containing any supported format Parser output must be normalized endpoint JSON with: - method - path - source format - optional base URL, headers, query/body hints ## 6. CI Guidance - run smoke collection on each PR - run full collection on main/nightly - fail on P0 regressions - export machine-readable reports for trend tracking -
report-schema.md 800 B
# Unified API Test Report Schema Use these fields for all generated API test reports: - `run_id`: unique run identifier - `tool`: `supertest` / `pytest` / `bruno` / `restassured` - `env`: environment name (staging/prod-like) - `case_id`: stable case identifier - `api_name`: human-readable endpoint name - `method`: HTTP method - `path`: endpoint path - `status`: `pass` / `fail` / `skip` - `status_code`: actual HTTP status code - `expected_status_code`: expected status code or range - `latency_ms`: measured request latency - `error_rate`: optional aggregated failure rate per run - `assertions_total`: total assertions - `assertions_passed`: passed assertions - `message`: failure or debug message - `timestamp`: ISO8601 UTC timestamp Recommended rule: - P0 failures should fail CI pipelines. -
setup-and-ci.md 447 B
# Setup and CI Guide ## Local ```bash npm i -g @usebruno/cli cd scripts ./run-tests.sh ./templates/bruno staging ``` ## One-Click Flow ```bash cd scripts ./run.sh ../examples ``` ## CI Recommendation - PR: run smoke subset - main/nightly: run full generated collection - archive json report output ## CI Template - `examples/ci/github-actions-bruno.yml` - `examples/ci/Jenkinsfile.bruno` ## Report Schema - `references/report-schema.md`
-
-
scripts
-
templates
-
bruno
-
environments
-
staging.bru 64 B · in bundle
-
-
requests
-
health.bru 251 B · in bundle
-
-
bruno.json 74 B
{ "version": "1", "name": "super-api-tests", "type": "collection" }
-
-
-
generate_bruno_requests.py 2.6 KB
#!/usr/bin/env python3 import argparse import json import re from pathlib import Path def _safe_slug(method: str, path: str, i: int) -> str: base = f"{i:03d}_{method}_{path}".lower() return re.sub(r"[^a-z0-9]+", "_", base).strip("_") def _to_bru(method: str, path: str, name: str) -> str: method_lower = method.lower() body_mode = "none" if method_lower in {"get", "delete", "head", "options"} else "json" body_block = "" if body_mode == "json": body_block = 'body:json {\n {\n "sample": true\n }\n}\n' return ( "meta {\n" f" name: {name}\n" " type: http\n" " seq: 1\n" "}\n\n" f"{method_lower} {{\n" f" url: {{baseUrl}}{path}\n" f" body: {body_mode}\n" "}\n\n" f"{body_block}\n" "headers {\n" " Content-Type: application/json\n" " Authorization: Bearer {{token}}\n" "}\n\n" "tests {\n" " test(\"status should be expected\", function() {\n" " const status = res.getStatus();\n" " expect([200, 201, 202, 204, 400, 401, 403, 404]).to.include(status);\n" " });\n" "}\n" ) def _bruno_json() -> str: return '{\n "version": "1",\n "name": "generated-bruno-collection",\n "type": "collection"\n}\n' def _env_bru() -> str: return ( "vars {\n" " baseUrl: https://api.example.com\n" " token: replace-me\n" "}\n" ) def main() -> None: parser = argparse.ArgumentParser(description="Generate Bruno requests from normalized endpoint JSON") parser.add_argument("--input", required=True, type=Path, help="Normalized endpoint JSON") parser.add_argument("--output-dir", required=True, type=Path, help="Output Bruno collection folder") args = parser.parse_args() payload = json.loads(args.input.read_text(encoding="utf-8")) endpoints = payload.get("endpoints", []) out_dir = args.output_dir req_dir = out_dir / "requests" env_dir = out_dir / "environments" req_dir.mkdir(parents=True, exist_ok=True) env_dir.mkdir(parents=True, exist_ok=True) (out_dir / "bruno.json").write_text(_bruno_json(), encoding="utf-8") (env_dir / "staging.bru").write_text(_env_bru(), encoding="utf-8") for i, ep in enumerate(endpoints, start=1): method = str(ep.get("method", "GET")).upper() path = str(ep.get("path", "/")) name = _safe_slug(method, path, i) content = _to_bru(method, path, name) (req_dir / f"{name}.bru").write_text(content, encoding="utf-8") print(str(out_dir)) if __name__ == "__main__": main() -
parse_api_sources.py 9.1 KB
#!/usr/bin/env python3 import argparse import json import re import zipfile from pathlib import Path from typing import Dict, List, Optional from xml.etree import ElementTree as ET try: import yaml # type: ignore except Exception: yaml = None METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"} def _safe_text(raw: bytes) -> str: return raw.decode("utf-8", errors="ignore") def _append_endpoint(out: List[Dict], method: str, path: str, source: str, **extra) -> None: method = method.upper().strip() path = (path or "").strip() if method not in METHODS or not path: return rec = {"method": method, "path": path, "source": source} rec.update({k: v for k, v in extra.items() if v is not None}) out.append(rec) def _parse_curl(text: str, source: str) -> List[Dict]: out: List[Dict] = [] for line in [x.strip() for x in text.splitlines() if "curl " in x]: m_method = re.search(r"-X\s+([A-Za-z]+)", line) method = m_method.group(1).upper() if m_method else "GET" m_url = re.search(r"(https?://[^\s'\"\\]+)", line) if not m_url: continue url = m_url.group(1) m_path = re.match(r"https?://[^/]+(.*)", url) path = m_path.group(1) if m_path else "/" if not path: path = "/" _append_endpoint(out, method, path, source, raw_url=url, format="curl") return out def _parse_postman(obj: Dict, source: str) -> List[Dict]: out: List[Dict] = [] def walk(items: List[Dict]) -> None: for it in items or []: if "request" in it: req = it.get("request", {}) method = str(req.get("method", "GET")).upper() url_obj = req.get("url", {}) path = "/" raw_url = None if isinstance(url_obj, dict): raw_url = url_obj.get("raw") path_parts = url_obj.get("path") if isinstance(path_parts, list) and path_parts: path = "/" + "/".join(str(p) for p in path_parts) elif isinstance(raw_url, str): m = re.match(r"https?://[^/]+(.*)", raw_url) path = m.group(1) if m and m.group(1) else "/" elif isinstance(url_obj, str): raw_url = url_obj m = re.match(r"https?://[^/]+(.*)", url_obj) path = m.group(1) if m and m.group(1) else "/" _append_endpoint(out, method, path, source, raw_url=raw_url, format="postman") if isinstance(it.get("item"), list): walk(it["item"]) walk(obj.get("item", [])) return out def _parse_openapi(obj: Dict, source: str) -> List[Dict]: out: List[Dict] = [] paths = obj.get("paths", {}) if not isinstance(paths, dict): return out for path, methods in paths.items(): if not isinstance(methods, dict): continue for m in methods.keys(): method = str(m).upper() if method in METHODS: _append_endpoint(out, method, str(path), source, format="openapi") return out def _parse_insomnia(obj: Dict, source: str) -> List[Dict]: out: List[Dict] = [] resources = obj.get("resources", []) if not isinstance(resources, list): return out for r in resources: if not isinstance(r, dict): continue if r.get("_type") == "request": method = str(r.get("method", "GET")).upper() url = str(r.get("url", "")) path = "/" m = re.match(r"https?://[^/]+(.*)", url) if m and m.group(1): path = m.group(1) elif url.startswith("/"): path = url _append_endpoint(out, method, path, source, raw_url=url, format="insomnia") return out def _parse_opencollection(obj: Dict, source: str) -> List[Dict]: out: List[Dict] = [] requests = obj.get("requests") if isinstance(requests, list): for r in requests: if not isinstance(r, dict): continue method = str(r.get("method", "GET")).upper() path = str(r.get("path") or r.get("url") or "/") _append_endpoint(out, method, path, source, format="opencollection") return out def _parse_wsdl(text: str, source: str) -> List[Dict]: out: List[Dict] = [] try: root = ET.fromstring(text.encode("utf-8")) except Exception: return out ns = { "wsdl": "http://schemas.xmlsoap.org/wsdl/", "soap": "http://schemas.xmlsoap.org/wsdl/soap/", } soap_address = root.find(".//soap:address", ns) location = soap_address.attrib.get("location") if soap_address is not None else None for op in root.findall(".//wsdl:operation", ns): name = op.attrib.get("name") if not name: continue path = f"/soap/{name}" _append_endpoint(out, "POST", path, source, raw_url=location, format="wsdl", operation=name) return out def _parse_bruno(text: str, source: str) -> List[Dict]: out: List[Dict] = [] method = None url = None for line in text.splitlines(): s = line.strip() block = re.match(r"^(get|post|put|patch|delete|head|options)\s*\{", s, re.IGNORECASE) if block: method = block.group(1).upper() continue m = re.match(r"method:\s*([A-Za-z]+)", s, re.IGNORECASE) if m: method = m.group(1).upper() continue u = re.match(r"url:\s*(.+)$", s, re.IGNORECASE) if u: url = u.group(1).strip() if method and url: m = re.match(r"https?://[^/]+(.*)", url) path = m.group(1) if m and m.group(1) else (url if url.startswith("/") else f"/{url}") _append_endpoint(out, method, path, source, raw_url=url, format="bruno") return out def _load_structured(path: Path, raw: str) -> Optional[Dict]: if path.suffix.lower() == ".json": try: return json.loads(raw) except Exception: return None if path.suffix.lower() in {".yaml", ".yml"} and yaml is not None: try: obj = yaml.safe_load(raw) return obj if isinstance(obj, dict) else None except Exception: return None return None def parse_file(path: Path, raw: str) -> List[Dict]: lower_name = path.name.lower() ext = path.suffix.lower() if ext == ".wsdl" or (ext == ".xml" and "<definitions" in raw and "wsdl" in raw.lower()): return _parse_wsdl(raw, str(path)) if ext == ".bru": return _parse_bruno(raw, str(path)) if ext in {".curl", ".sh", ".txt"}: return _parse_curl(raw, str(path)) obj = _load_structured(path, raw) if obj is not None: if "openapi" in obj or "swagger" in obj: return _parse_openapi(obj, str(path)) if lower_name.endswith(".postman_collection.json") or "info" in obj and "item" in obj: return _parse_postman(obj, str(path)) if "resources" in obj: return _parse_insomnia(obj, str(path)) if lower_name.endswith(".opencollection.json") or "requests" in obj: return _parse_opencollection(obj, str(path)) return [] return _parse_curl(raw, str(path)) def parse_zip(path: Path) -> List[Dict]: out: List[Dict] = [] with zipfile.ZipFile(path) as zf: for info in zf.infolist(): if info.is_dir(): continue p = Path(info.filename) raw = _safe_text(zf.read(info)) if p.suffix.lower() == ".zip": continue out.extend(parse_file(p, raw)) return out def parse_input(path: Path) -> List[Dict]: if path.is_dir(): out: List[Dict] = [] for f in path.rglob("*"): if f.is_file(): raw = f.read_text(encoding="utf-8", errors="ignore") out.extend(parse_file(f, raw)) return out if path.suffix.lower() == ".zip": return parse_zip(path) raw = path.read_text(encoding="utf-8", errors="ignore") return parse_file(path, raw) def dedupe(endpoints: List[Dict]) -> List[Dict]: seen = set() out = [] for e in endpoints: key = (e.get("method"), e.get("path"), e.get("source")) if key in seen: continue seen.add(key) out.append(e) return out def main() -> None: parser = argparse.ArgumentParser(description="Parse API sources into normalized endpoint inventory") parser.add_argument("--input", required=True, type=Path, help="Input file/folder/zip path") parser.add_argument("--output", type=Path, help="Output JSON path") args = parser.parse_args() endpoints = dedupe(parse_input(args.input)) payload = { "input": str(args.input), "count": len(endpoints), "endpoints": endpoints, } if args.output: args.output.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(str(args.output)) else: print(json.dumps(payload, ensure_ascii=False, indent=2)) if __name__ == "__main__": main() -
run-tests.sh 713 B
#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" COLLECTION_DIR="${1:-$SCRIPT_DIR/templates/bruno}" ENV_NAME="${2:-staging}" REPORT_DIR="$SCRIPT_DIR/../reports" mkdir -p "$REPORT_DIR" if ! command -v bru >/dev/null 2>&1; then echo "bru command not found. Please install Bruno CLI first." echo "npm i -g @usebruno/cli" exit 1 fi TS="$(date +%Y%m%d-%H%M%S)" REPORT_JSON="$REPORT_DIR/bru-report-${TS}.json" echo "Running Bruno collection: $COLLECTION_DIR (env=$ENV_NAME)" # Note: CLI flags may vary by Bruno version. # This command targets common CLI usage. bru run "$COLLECTION_DIR" --env "$ENV_NAME" --reporter-json "$REPORT_JSON" echo "Report generated: $REPORT_JSON" -
run.sh 582 B
#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" INPUT_PATH="${1:-$ROOT_DIR/examples}" TMP_JSON="$ROOT_DIR/.tmp.normalized.json" OUT_DIR="$ROOT_DIR/generated-bruno" python3 "$SCRIPT_DIR/parse_api_sources.py" --input "$INPUT_PATH" --output "$TMP_JSON" python3 "$SCRIPT_DIR/generate_bruno_requests.py" --input "$TMP_JSON" --output-dir "$OUT_DIR" if ! command -v bru >/dev/null 2>&1; then echo "bru not found. Collection generated at: $OUT_DIR" exit 0 fi "$SCRIPT_DIR/run-tests.sh" "$OUT_DIR" "staging"
-
-
README.md 808 B
# api-test-bruno (EN) ## Skill Overview Need API outputs that should land in Bruno structure or Bruno workflow; The project already uses Bruno or wants Bruno-ready organization. ## How to Use 1. Open `SKILL.md` in this folder and confirm this skill fits your task. 2. In your AI tool, call `@skill api-test-bruno`, then add your real project context and goal. 3. If you need a specific output format (table, checklist, report), include it directly in your request. ## One-Click Install Script Run from the repository root: ### macOS / Linux ```bash bash ./scripts/install-skills-mac.sh --tool codex --lang en --skill api-test-bruno ``` ### Windows PowerShell ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\install-skills-windows.ps1 -Tool codex -Lang en -Skill api-test-bruno ``` -
SKILL.md 2.6 KB
--- name: api-test-bruno description: Use this skill when you need to parse multi-format API definitions and generate Bruno collections for executable regression; triggers include Bruno collections and Bruno API testing. --- # api-test-bruno (EN) **Chinese version:** See the corresponding Chinese skill. ## When to Use - Need API outputs that should land in Bruno structure or Bruno workflow. - The project already uses Bruno or wants Bruno-ready organization. ## Workflow 1. Read and follow the main prompt listed under Progressive disclosure (coverage, structure, quality bar). 2. Add only project context that changes the result: scope, environment, constraints, risks, dependencies, expected deliverable. 3. If input is incomplete, return a usable first draft and explicitly mark assumptions and gaps. 4. Default to Markdown; switch formats only when the user asks. ## Core Constraints - Prioritize by risk / business impact — do not treat everything equally. - Separate confirmed facts from current assumptions. - Do not invent endpoints, fields, environments, or root causes the user did not provide. - Use placeholders or env-var semantics for auth/secrets; never hardcode real credentials. - Keep output executable: concrete scenarios, clear priority, clear next steps. ## Progressive Disclosure - Before producing output, read and follow `prompts/api-test-bruno.md` (minimum coverage, output structure, quality bar). - When a ready-made template fits: use matching files under `output-templates/`. - When the user wants examples or alignment with existing assets: read relevant `examples/`. - For deep framework/troubleshoot/schema notes: read only the relevant file(s) under `references/`, do not load the whole directory. - For format conversion or helper checks: prefer existing `scripts/` over reinventing. - For evaluating/regressing this skill: use `evals/` with skill-up. ## Pre-delivery Checklist - [ ] Followed the main prompt's output structure - [ ] Minimum coverage focus: collection structure, environment setup, auth handling, priority endpoints, positive scenarios, negative and boundary scenarios, data or variable strategy, assertion focus, ... (details in main prompt) - [ ] Covered the minimum checklist, or explained omissions - [ ] High-risk items have explicit priority - [ ] Did not invent details the user did not provide - [ ] Assumptions and gaps are marked ## Common Pitfalls - Do not pretend completeness when scope/context is missing. - Do not treat every item as equally important. - Do not skip assumptions and information gaps. - Do not dump generic theory unrelated to the current toolchain.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.