A complete REST collection for the European Health Data Space integration
hub. Every endpoint in ui/src/app/api/* and the Neo4j proxy in
services/neo4j-proxy/* is exercised here.
This README covers:
- Why Bruno
- Prerequisites
- Open the collection
- Pick an environment
- Authenticate against Azure-Dev
- Send your first request
- Tests, asserts, and chaining
- Automate with the Bruno CLI
- GitHub Actions integration
- Coverage
- Adding a new endpoint
- Troubleshooting
Bruno collections are plain .bru files that live in git alongside the
code. Unlike Postman, there is no cloud sync, no proprietary export, and
the collection diffs cleanly in PRs. The same files work in the Bruno
desktop app, the VS Code extension, and the bru CLI for CI runs.
The auth/[...nextauth] route is intentionally not in the collection: it
is a NextAuth internal handler, not a REST endpoint.
| Tool | Version | Why |
|---|---|---|
| Bruno (desktop) | v3.3.0 or later | Interactive request authoring |
| Node.js | v20 or later | Forge script and Bruno CLI |
@usebruno/cli (bru) |
latest | Run the collection in CI |
| Project repo | this checkout | The forge script lives in ui/scripts/ |
Install Bruno desktop from usebruno.com or
brew install --cask bruno on macOS. Install the CLI with:
npm install -g @usebruno/cliFrom the repo root:
open -a Bruno bruno/MVHDv2 # macOS
xdg-open bruno/MVHDv2 # Linux
explorer.exe bruno\MVHDv2 # WindowsIf Bruno opens to a previous workspace instead, click Open Collection
in the Quick Actions panel and pick bruno/MVHDv2/.
Top-right dropdown of any open request:
| Environment | Base URL | Auth required | Use when |
|---|---|---|---|
| Local | http://localhost:3000 |
Yes (browser) | Full local Docker stack is up |
| Static-mock | https://ma3u.github.io/MinimumViableHealthDataspacev2 |
No | Quick UI / shape testing, no live data |
| Azure-Dev | https://ehds.mabu.red |
Yes (cookie) | Live shared dev environment |
The cleanest path for first-time users is Static-mock: every GET
endpoint serves a static JSON fixture from ui/public/mock/*.json and
returns 200 without authentication.
Note: Azure-Dev is on an ACA off-hours scale-down schedule (ADR-016 / ADR-023), reachable Mon–Fri 07:00–20:00 Europe/Berlin. Outside that window, expect 502s from cold-starting services.
The Azure deployment gates every /api/* route behind a NextAuth session
cookie. Bruno does not share a cookie jar with your browser, so the usual
"sign in via the UI first" pattern does not carry over. Two paths:
cd ui
node scripts/forge-bruno-session.mjs regulatorThe script prints something like:
COOKIE_NAME=__Secure-next-auth.session-token
COOKIE_VALUE=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..…
In Bruno:
- Open Environments → Azure-Dev (top-right environment dropdown, click the small edit icon).
- Find or add a variable named
sessionToken. - Paste the long
COOKIE_VALUEstring into its Value field. Save. - Re-send any request. The collection-level Cookie header
(
Cookie: __Secure-next-auth.session-token={{sessionToken}}) picks it up automatically.
The token is valid for 8 hours. Re-run the script when it expires.
Available personas (each maps to a Keycloak role set):
| Persona | Role |
|---|---|
edcadmin |
EDC_ADMIN (full admin) |
regulator |
HDAB_AUTHORITY (Health Data Authority) |
clinicuser |
DATA_HOLDER (German clinic) |
lmcuser |
DATA_HOLDER (Dutch clinic) |
researcher |
DATA_USER (pharma researcher) |
patient1 |
PATIENT |
- Sign in at https://ehds.mabu.red/auth/signin with one of the demo
personas (
username = password, e.g.regulator/regulator). - Open browser DevTools → Application → Cookies →
https://ehds.mabu.red. - Copy the value of
__Secure-next-auth.session-token. - Paste into the Bruno
sessionTokenvariable as in Path A.
This works without running anything, but requires manual repetition every 8 hours, and you cannot easily script it.
Pick GET EEHRxF (or GET Compliance Status) in the sidebar. The
URL is {{baseUrl}}/api/eehrxf. Hit Send.
- On
Static-mock: immediate 200 with the FHIR profile catalogue. - On
Azure-DevwithsessionTokenset: live data (currently 6 categories, 14 profiles, ~93k resources). - On
Azure-DevwithoutsessionToken: 401 plus a Bruno test failure with the hint "run the forge script".
Things to verify in your own window:
| Check | Where |
|---|---|
| Environment selector top-right | Should read Azure-Dev |
| Response status | 200 OK (red 401 Unauthorized means sessionToken is empty or expired) |
Headers tab on the request |
Two collection-level rows: Accept: application/json and Cookie: __Secure-next-auth.session-token={{sessionToken}} |
Vars tab on the request |
sessionToken shows a long string starting eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0.. |
| Response body | Real participant DIDs (did:web:alpha-klinik.de:participant, …) — not a mock object |
If you see 401 after setting sessionToken, Bruno may not have re-read
the environment file. Use the in-app Environments → Azure-Dev panel
to set the value (instead of editing the file from outside) — those
edits take effect immediately.
Every request inherits the collection-level guards in collection.bru:
test("response is not a server error", function () {
expect(res.getStatus()).to.be.below(500);
});Plus a 401-against-Azure-Dev hint that surfaces the forge command right next to the failing request.
Add per-request asserts in the Asserts tab, e.g.:
res.status: eq 200
res.body.summary.totalCategories: gte 6
Chain requests by writing values to environment variables in the Tests tab:
test("save first patient id", function () {
const data = res.getBody();
bru.setVar("patientId", data.patients[0].id);
});Subsequent requests reference {{patientId}} in the URL, headers, or
body.
The Bruno CLI (bru) runs a collection without the desktop UI. Useful
for smoke tests and CI.
cd bruno/MVHDv2
bru run --env Static-mock --reporter-html report.html
open report.html# 1. Forge a token
cd ui
TOKEN=$(node scripts/forge-bruno-session.mjs regulator | awk -F= '/^COOKIE_VALUE/{print $2}')
# 2. Run the collection, mapping the token onto the sessionToken variable
cd ../bruno/MVHDv2
bru run --env Azure-Dev --env-var sessionToken="$TOKEN" --reporter-html report.htmlbru run flags worth knowing:
| Flag | Purpose |
|---|---|
--env <name> |
Pick environment (Local / Static-mock / Azure-Dev) |
--env-var k=v |
Override or set a variable for this run |
-r / --recursive |
Include sub-folders |
--reporter-html f |
Write HTML report |
--reporter-junit f |
Write JUnit XML for CI test reporters |
--bail |
Stop on first failing request |
--insecure |
Disable TLS verify (only for self-signed local dev) |
Exit code is non-zero if any request fails its asserts or returns 5xx.
( cd ui && TOKEN=$(node scripts/forge-bruno-session.mjs regulator \
| awk -F= '/^COOKIE_VALUE/{print $2}') ) && \
( cd bruno/MVHDv2 && bru run --env Azure-Dev \
--env-var sessionToken="$TOKEN" --bail )A workflow at .github/workflows/bruno-smoke.yml runs the collection on
push and on demand. The relevant pieces:
- name: Forge session token
env:
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
run: |
cd ui
TOKEN=$(node scripts/forge-bruno-session.mjs regulator \
| awk -F= '/^COOKIE_VALUE/{print $2}')
echo "::add-mask::$TOKEN"
echo "BRUNO_TOKEN=$TOKEN" >> "$GITHUB_ENV"
- name: Run Bruno collection
run: |
npm install -g @usebruno/cli
cd bruno/MVHDv2
bru run --env Azure-Dev \
--env-var sessionToken="$BRUNO_TOKEN" \
--reporter-html ../../bruno-report.html \
--reporter-junit ../../bruno-report.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: bruno-report
path: bruno-report.*| Secret | Value |
|---|---|
NEXTAUTH_SECRET |
Same value as on the live Azure deployment |
The add-mask line in the workflow ensures the token never appears in
the run logs.
The whole secret-and-trigger flow can be done from the terminal without
opening the GitHub UI. The gh CLI handles secret management and
workflow dispatch.
# Verify gh is authenticated against the right account
gh auth status
# Set (or rotate) the NEXTAUTH_SECRET repository secret
gh secret set NEXTAUTH_SECRET \
--repo ma3u/MinimumViableHealthDataspacev2 \
--body 'mvhd-azure-secret-change-me'
# For longer / multiline secrets, read from a file or stdin
gh secret set NEXTAUTH_SECRET \
--repo ma3u/MinimumViableHealthDataspacev2 < secret.txt
# List repository secrets (names + last-updated only — values aren't readable)
gh secret list --repo ma3u/MinimumViableHealthDataspacev2
# Delete a secret
gh secret delete NEXTAUTH_SECRET --repo ma3u/MinimumViableHealthDataspacev2The value to set must match the NEXTAUTH_SECRET env var on the live
Azure Container App. Read it with:
az containerapp show -n mvhd-ui -g rg-mvhd-dev \
--query "properties.template.containers[0].env[?name=='NEXTAUTH_SECRET'].value" \
-o tsvIf the live secret is rotated, rotate the GitHub secret in the same window or the workflow's forge step will produce tokens that the live API rejects.
# Default run (Static-mock, no auth needed)
gh workflow run bruno-smoke.yml --repo ma3u/MinimumViableHealthDataspacev2
# Pick environment and persona explicitly
gh workflow run bruno-smoke.yml \
--repo ma3u/MinimumViableHealthDataspacev2 \
-f environment=Azure-Dev \
-f persona=edcadmin
# RBAC-focused smoke (regulator persona — admin write routes return 403)
gh workflow run bruno-smoke.yml \
--repo ma3u/MinimumViableHealthDataspacev2 \
-f environment=Azure-Dev \
-f persona=regulator
# List recent runs of this workflow
gh run list --workflow=bruno-smoke.yml \
--repo ma3u/MinimumViableHealthDataspacev2 --limit 10
# Watch the latest run interactively
gh run watch --repo ma3u/MinimumViableHealthDataspacev2
# View one run's step results
RUN_ID=$(gh run list --workflow=bruno-smoke.yml \
--repo ma3u/MinimumViableHealthDataspacev2 \
--limit 1 --json databaseId --jq '.[0].databaseId')
gh run view "$RUN_ID" --repo ma3u/MinimumViableHealthDataspacev2
# Download the report artefact (HTML + JUnit) of the most recent run
gh run download "$RUN_ID" \
--repo ma3u/MinimumViableHealthDataspacev2 \
--name "bruno-report-Azure-Dev"
open bruno-report.htmlPersona options accepted by the -f persona= input: edcadmin,
regulator, clinicuser, lmcuser, researcher, patient1. See the
table under Authenticate against Azure-Dev
for the role each persona maps to.
| Folder | Routes | Notes |
|---|---|---|
| Health | 1 | Liveness probe |
| Catalog | 3 | HealthDCAT-AP datasets — list / create / delete |
| Graph | 4 | 5-layer Neo4j graph queries |
| Patient | 6 | FHIR profile, insights, research consent (GDPR Art. 7) |
| Compliance | 2 | EHDS / GDPR / DSP status + TCK results |
| Credentials | 4 | DCP v1.0 W3C VCs |
| Negotiations | 3 | DSP 2025-1 contract negotiations |
| Transfers | 3 | DCore data plane transfers |
| Assets | 2 | EDC asset registration |
| Participants | 5 | DID:web participant CRUD |
| Tasks | 1 | Aggregated contract / transfer pipeline tasks |
| Trust Center | 1 | DID resolution + attestation chain |
| Federated | 1 | Cross-site cohort query (k-anonymity) |
| NLQ | 2 | Text2Cypher templates + run |
| EEHRxF | 1 | EEHRxF profile catalog (Layer 2b) |
| Analytics | 1 | OMOP-derived dashboard |
| ODRL Scope | 1 | Effective ODRL scope for current participant |
| Admin | 8 | Tenants, audit, components, topology, policies (CRUD) |
| Total | 49 | + collection.bru, bruno.json, 3 environments |
- Drop a new
.brufile under the appropriate folder. - Set
seq:to the next number in that folder. - Use
{{baseUrl}}and any of the env vars ({{participantId}},{{patientId}},{{studyId}},{{proxyUrl}}). - For request bodies, use
body:json { ... }; the block contents are valid JSON. - Commit the file. PR diffs are readable.
The collection-level Cookie header is already attached, so admin /
gated routes work without per-request setup once sessionToken is in
the active environment.
The sessionToken variable is empty or expired. Re-run the forge script
and paste the new value into Environments → Azure-Dev → sessionToken.
Working as intended. Forge a token with the edcadmin persona instead,
or use regulator for the read-only paths (/api/admin/policies,
/api/admin/audit).
The Azure proxy has a 30 s timeout. Some federated queries (/api/nlq,
/api/graph with no filter) take longer on a cold Neo4j. Re-send after
warming up with a small request first. Outside Mon–Fri 07:00–20:00
Europe/Berlin, scaled-down services produce the same symptom.
Bruno failed to substitute the variable. Check that the environment is
actually selected in the top-right dropdown, and that the variable name
matches exactly (sessionToken, no spaces).
You ran the script from the wrong directory. The ui/scripts/ location
is required so Node ESM can resolve the next-auth package from
ui/node_modules/.
cd ui # from the repo root
node scripts/forge-bruno-session.mjs regulatorBruno CLI v1.x had a bug where --env-var with = in the value
required quoting. Use single quotes around the whole arg:
bru run --env Azure-Dev --env-var 'sessionToken=eyJhbGc…'ui/src/app/api/**/route.ts: source of truth for request and response shapes.ui/public/mock/*.json: fixture responses used by the static export.ui/scripts/forge-bruno-session.mjs: NextAuth session forge script..claude/rules/api-conventions.md: protocol versions, role matrix, data models.docs/ADRs/ADR-008-testing-strategy.md: how Bruno fits alongside Vitest and Playwright.
