feat(zgw): docker OpenZaak integration-test harness (WP-54)
Opt-in docker-compose (postgres+redis+OpenZaak, no celery/nginx) + bootstrap-catalogus.sh seed a real OpenZaak instance; OpenZaakIntegrationTests (Category=Integration, excluded from default dotnet test/CI) proves the ZGW seam against it for the first time. That live run caught a real bug: ZgwHttpClient never sent Content-Crs/Accept-Crs headers, so every write would 412 against a spec-compliant OpenZaak — fixed alongside the harness. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -92,7 +92,9 @@ jobs:
|
|||||||
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
|
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
|
||||||
restore-keys: nuget-${{ runner.os }}-
|
restore-keys: nuget-${{ runner.os }}-
|
||||||
- run: dotnet format backend/BigRegister.slnx --verify-no-changes
|
- run: dotnet format backend/BigRegister.slnx --verify-no-changes
|
||||||
- run: dotnet test backend/BigRegister.slnx
|
# Category=Integration (WP-54, OpenZaakIntegrationTests) needs a live OpenZaak — opt-in,
|
||||||
|
# run manually against backend/openzaak/ (see its README), never in CI.
|
||||||
|
- run: dotnet test backend/BigRegister.slnx --filter "Category!=Integration"
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
# Smoke-level Playwright run against the REAL FE+backend (WP-19) — a fresh
|
# Smoke-level Playwright run against the REAL FE+backend (WP-19) — a fresh
|
||||||
|
|||||||
@@ -51,3 +51,6 @@ storybook-static
|
|||||||
/playwright-report
|
/playwright-report
|
||||||
/blob-report
|
/blob-report
|
||||||
/playwright/.cache
|
/playwright/.cache
|
||||||
|
|
||||||
|
# WP-54: bootstrap-catalogus.sh's own record of what it seeded into a local OpenZaak run
|
||||||
|
backend/openzaak/seeded.env
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# OpenZaak integration harness (WP-54)
|
||||||
|
|
||||||
|
A real OpenZaak, for developing/testing the ZGW seam (`backend/src/BigRegister.Api/Zgw/`)
|
||||||
|
against something that isn't a fixture or a stub `HttpMessageHandler`. Deliberately **not**
|
||||||
|
part of the root `docker-compose.yml` and **not** wired into `npm run ci` / CI — see
|
||||||
|
[docs/reference/openzaak-integration.md](../../docs/reference/openzaak-integration.md) for the
|
||||||
|
full picture; this is just "how to run it".
|
||||||
|
|
||||||
|
## Bring it up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend/openzaak
|
||||||
|
docker compose -f docker-compose.openzaak.yml up -d # postgres, redis, migrate+configure, OpenZaak
|
||||||
|
./bootstrap-catalogus.sh # seeds a catalogus/zaaktype/zaak to read back
|
||||||
|
```
|
||||||
|
|
||||||
|
`bootstrap-catalogus.sh` waits for OpenZaak to answer, then over plain REST + a hand-rolled
|
||||||
|
HS256 JWT (same shape as `ZgwTokenProvider.cs`, matching the `bigregister-test` client
|
||||||
|
`setup_configuration/data.yaml` creates): a catalogus, a published zaaktype ("Herregistratie
|
||||||
|
arts", with the statustypen/resultaattype/roltype OpenZaak requires before a zaaktype can be
|
||||||
|
published), and one zaak (`BIG-2026-000123`) with an initiator rol for the seeded BSN
|
||||||
|
(`111222333` — the same fixture BSN `OpenZaakZaakSourceTests.cs` uses). It writes what it
|
||||||
|
seeded to `seeded.env` (gitignored) and prints a summary.
|
||||||
|
|
||||||
|
**Not idempotent** — re-running against the same (already-seeded) instance fails on OpenZaak's
|
||||||
|
`domein`+`rsin` uniqueness constraint for the catalogus. Reset with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.openzaak.yml down -v && docker compose -f docker-compose.openzaak.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the integration test against it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
dotnet test --filter Category=Integration
|
||||||
|
```
|
||||||
|
|
||||||
|
`OpenZaakIntegrationTests.cs` points a `WebApplicationFactory<Program>` at
|
||||||
|
`Zgw:Enabled=true` + `http://localhost:8000` with the harness's credentials, hits
|
||||||
|
`GET /api/v1/admin/cases`, and asserts the seeded zaak comes back — through the real HTTP +
|
||||||
|
JWT + Catalogi-label-resolution path, not a mock. This test is tagged `Category=Integration`
|
||||||
|
and is **excluded** from the default `dotnet test` run and from CI (`ci.yml`,
|
||||||
|
`scripts/ci-local.sh` both filter `Category!=Integration`) — it only passes with this harness
|
||||||
|
up, so it never runs where the harness doesn't exist.
|
||||||
|
|
||||||
|
## Tear down
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.openzaak.yml down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's in here / what isn't
|
||||||
|
|
||||||
|
- `docker-compose.openzaak.yml` — postgres (postgis), redis, a one-shot `web-init` (runs
|
||||||
|
Django migrations then `setup_configuration` against `setup_configuration/data.yaml`), and
|
||||||
|
`web` (the OpenZaak API on `:8000`). Pinned to `openzaak/open-zaak:1.29.1`. No
|
||||||
|
celery/celery-beat/celery-flower/nginx — trimmed for a lean, fast-booting harness; add them
|
||||||
|
back only if a later WP needs a real async notification delivery round-trip here (WP-52's
|
||||||
|
webhook is already covered by fixture tests against no live instance).
|
||||||
|
`NOTIFICATIONS_DISABLED=true` is required, not optional: without it, OpenZaak 500s (and
|
||||||
|
**rolls back the whole create**) on any notified resource — see the compose file's comment.
|
||||||
|
- `setup_configuration/data.yaml` — the declarative, scripted alternative to clicking through
|
||||||
|
the Django admin (upstream's own documented `setup_configuration` CLI mechanism): creates the
|
||||||
|
one `bigregister-test` client (`heeft_alle_autorisaties: true` — this instance never exists
|
||||||
|
for anything but this harness, so there's no least-privilege boundary worth modeling).
|
||||||
|
- `bootstrap-catalogus.sh` — the business content (catalogus/zaaktype/zaak/…) `setup_configuration`
|
||||||
|
has no YAML for; every field value here was checked against OpenZaak's own OpenAPI spec and a
|
||||||
|
live run of this exact script, not guessed (two OpenZaak quirks it works around: a zaaktype
|
||||||
|
needs ≥1 resultaattype and 2 statustypen before it can be published, and its
|
||||||
|
`selectielijstklasse` and the zaaktype's `selectielijstProcestype` must reference the same
|
||||||
|
`procesType` on the public VNG selectielijst API).
|
||||||
|
- **Not here**: Documenten (DRC) / Notificaties (NRC) content — add if a later WP needs to prove
|
||||||
|
those round-trips against a live instance too (WP-51/52 are fixture-tested today).
|
||||||
Executable
+164
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# WP-54 — seeds business content (catalogus/zaaktype/statustype/roltype/zaak/status/rol) into
|
||||||
|
# the OpenZaak harness started by docker-compose.openzaak.yml. `setup_configuration/data.yaml`
|
||||||
|
# only covers infra config (the JWTSecret + Applicatie); Catalogi/Zaken content has no
|
||||||
|
# declarative-YAML equivalent upstream, so this script does it the same way the BFF itself
|
||||||
|
# does at runtime — plain REST calls with a hand-rolled HS256 JWT (see ZgwTokenProvider.cs,
|
||||||
|
# mirrored here in bash+openssl so this script has no extra dependency beyond curl/openssl).
|
||||||
|
#
|
||||||
|
# Idempotent-ish: re-running creates duplicate catalogus/zaaktype rows (OpenZaak doesn't
|
||||||
|
# dedupe by name) — meant to be run once per fresh `docker compose up`, not repeatedly against
|
||||||
|
# a long-lived instance. Prints the seeded zaak's `identificatie` + `url` on success; also
|
||||||
|
# writes them to seeded.env (repo-ignored) for OpenZaakIntegrationTests.cs to assert against.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
BASE="http://localhost:8000"
|
||||||
|
CLIENT_ID="bigregister-test"
|
||||||
|
SECRET="bigregister-test-secret"
|
||||||
|
RSIN="123443210" # elfproef-valid RSIN, already used as the fixture Bronorganisatie
|
||||||
|
# in OpenZaakZaakSourceTests.cs — reused here for consistency.
|
||||||
|
BSN="111222333" # elfproef-valid BSN, already used as the fixture caller BSN.
|
||||||
|
ZAAK_REF="BIG-2026-000123"
|
||||||
|
|
||||||
|
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
|
||||||
|
|
||||||
|
jwt() {
|
||||||
|
local header='{"alg":"HS256","typ":"JWT"}'
|
||||||
|
local payload
|
||||||
|
payload=$(printf '{"iss":"%s","iat":%d,"client_id":"%s","user_id":"%s","user_representation":"%s"}' \
|
||||||
|
"$CLIENT_ID" "$(date +%s)" "$CLIENT_ID" "$CLIENT_ID" "bootstrap")
|
||||||
|
local h p signing_input sig
|
||||||
|
h=$(printf '%s' "$header" | b64url)
|
||||||
|
p=$(printf '%s' "$payload" | b64url)
|
||||||
|
signing_input="$h.$p"
|
||||||
|
sig=$(printf '%s' "$signing_input" | openssl dgst -sha256 -hmac "$SECRET" -binary | b64url)
|
||||||
|
printf '%s.%s' "$signing_input" "$sig"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = method, $2 = path, $3 = JSON body (optional)
|
||||||
|
oz() {
|
||||||
|
local method="$1" path="$2" body="${3:-}"
|
||||||
|
# Content-Crs/Accept-Crs: every ZGW write must declare a coordinate reference system even
|
||||||
|
# when no geometry is involved — OpenZaak 412s without it.
|
||||||
|
local args=(-sS -X "$method" -H "Authorization: Bearer $(jwt)" -H "Content-Type: application/json" \
|
||||||
|
-H "Content-Crs: EPSG:4326" -H "Accept-Crs: EPSG:4326")
|
||||||
|
[ -n "$body" ] && args+=(-d "$body")
|
||||||
|
local response
|
||||||
|
response=$(curl "${args[@]}" -w $'\n%{http_code}' "$BASE$path")
|
||||||
|
local http_code="${response##*$'\n'}"
|
||||||
|
local json="${response%$'\n'*}"
|
||||||
|
if [[ ! "$http_code" =~ ^2 ]]; then
|
||||||
|
echo "FAILED $method $path -> $http_code: $json" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$json"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Waiting for OpenZaak..."
|
||||||
|
until curl -sS -o /dev/null -w '%{http_code}' "$BASE/catalogi/api/v1/catalogussen" | grep -q '^2\|^401\|^403'; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Creating catalogus..."
|
||||||
|
catalogus=$(oz POST /catalogi/api/v1/catalogussen "$(printf '{"domein":"BIGR","rsin":"%s","contactpersoonBeheerNaam":"BIG Register"}' "$RSIN")")
|
||||||
|
catalogus_url=$(echo "$catalogus" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
||||||
|
echo " $catalogus_url"
|
||||||
|
|
||||||
|
echo "Creating zaaktype (concept)..."
|
||||||
|
zaaktype=$(oz POST /catalogi/api/v1/zaaktypen "$(python3 -c '
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps({
|
||||||
|
"identificatie": "ZT-HERREG",
|
||||||
|
"omschrijving": "Herregistratie arts",
|
||||||
|
"vertrouwelijkheidaanduiding": "openbaar",
|
||||||
|
"doel": "Herregistratie in het BIG-register",
|
||||||
|
"aanleiding": "Aanvraag door de zorgverlener",
|
||||||
|
"indicatieInternOfExtern": "extern",
|
||||||
|
"handelingInitiator": "indienen",
|
||||||
|
"onderwerp": "Herregistratie",
|
||||||
|
"handelingBehandelaar": "behandelen",
|
||||||
|
"doorlooptijd": "P30D",
|
||||||
|
"opschortingEnAanhoudingMogelijk": False,
|
||||||
|
"verlengingMogelijk": False,
|
||||||
|
"publicatieIndicatie": False,
|
||||||
|
"productenOfDiensten": ["https://example.com/producten/herregistratie"],
|
||||||
|
"referentieproces": {"naam": "Herregistratie"},
|
||||||
|
"verantwoordelijke": "CIBG",
|
||||||
|
"catalogus": sys.argv[1],
|
||||||
|
"beginGeldigheid": "2026-01-01",
|
||||||
|
"versiedatum": "2026-01-01",
|
||||||
|
"besluittypen": [],
|
||||||
|
"gerelateerdeZaaktypen": [],
|
||||||
|
# Must belong to the same procestype as the resultaattype selectielijstklasse below
|
||||||
|
# (OpenZaak cross-checks this against the public VNG selectielijst API).
|
||||||
|
"selectielijstProcestype": "https://selectielijst.openzaak.nl/api/v1/procestypen/e1b73b12-b2f6-4c4e-8929-94f84dd2a57d",
|
||||||
|
}))
|
||||||
|
' "$catalogus_url")")
|
||||||
|
zaaktype_url=$(echo "$zaaktype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
||||||
|
echo " $zaaktype_url"
|
||||||
|
|
||||||
|
echo "Creating statustypen (publish needs a begin AND an end status)..."
|
||||||
|
statustype=$(oz POST /catalogi/api/v1/statustypen "$(printf '{"zaaktype":"%s","omschrijving":"Ontvangen","volgnummer":1}' "$zaaktype_url")")
|
||||||
|
echo " $(echo "$statustype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')"
|
||||||
|
statustype_eind=$(oz POST /catalogi/api/v1/statustypen "$(printf '{"zaaktype":"%s","omschrijving":"Afgehandeld","volgnummer":2}' "$zaaktype_url")")
|
||||||
|
echo " $(echo "$statustype_eind" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')"
|
||||||
|
|
||||||
|
echo "Creating resultaattype (publish needs at least one)..."
|
||||||
|
# The two URLs below are real reference-list entries on the public VNG selectielijst API
|
||||||
|
# (selectielijst.openzaak.nl) — OpenZaak validates both by fetching them, same as it does
|
||||||
|
# for a zaaktype URL, so a made-up URL 404s here.
|
||||||
|
resultaattype=$(oz POST /catalogi/api/v1/resultaattypen "$(printf '{"zaaktype":"%s","omschrijving":"Afgehandeld","resultaattypeomschrijving":"https://selectielijst.openzaak.nl/api/v1/resultaattypeomschrijvingen/7cb315fb-4f7b-4a43-aca1-e4522e4c73b3","selectielijstklasse":"https://selectielijst.openzaak.nl/api/v1/resultaten/cc5ae4e3-a9e6-4386-bcee-46be4986a829","archiefnominatie":"blijvend_bewaren"}' "$zaaktype_url")")
|
||||||
|
echo " $(echo "$resultaattype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')"
|
||||||
|
|
||||||
|
echo "Creating roltype (initiator)..."
|
||||||
|
roltype=$(oz POST /catalogi/api/v1/roltypen "$(printf '{"zaaktype":"%s","omschrijving":"Initiator","omschrijvingGeneriek":"initiator"}' "$zaaktype_url")")
|
||||||
|
echo " $(echo "$roltype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')"
|
||||||
|
|
||||||
|
echo "Publishing zaaktype..."
|
||||||
|
zaaktype_uuid=$(echo "$zaaktype_url" | sed 's#.*/##')
|
||||||
|
oz POST "/catalogi/api/v1/zaaktypen/$zaaktype_uuid/publish" >/dev/null
|
||||||
|
|
||||||
|
echo "Creating zaak..."
|
||||||
|
zaak=$(oz POST /zaken/api/v1/zaken "$(python3 -c '
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps({
|
||||||
|
"zaaktype": sys.argv[1],
|
||||||
|
"bronorganisatie": sys.argv[2],
|
||||||
|
"verantwoordelijkeOrganisatie": sys.argv[2],
|
||||||
|
"startdatum": "2026-07-28",
|
||||||
|
"identificatie": sys.argv[3],
|
||||||
|
}))
|
||||||
|
' "$zaaktype_url" "$RSIN" "$ZAAK_REF")")
|
||||||
|
zaak_url=$(echo "$zaak" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
||||||
|
echo " $zaak_url"
|
||||||
|
|
||||||
|
echo "Creating status..."
|
||||||
|
statustype_url=$(echo "$statustype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
||||||
|
oz POST /zaken/api/v1/statussen "$(python3 -c '
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps({"zaak": sys.argv[1], "statustype": sys.argv[2], "datumStatusGezet": "2026-07-28T12:00:00Z"}))
|
||||||
|
' "$zaak_url" "$statustype_url")" >/dev/null
|
||||||
|
|
||||||
|
echo "Creating rol (initiator, seeded BSN)..."
|
||||||
|
roltype_url=$(echo "$roltype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
||||||
|
oz POST /zaken/api/v1/rollen "$(python3 -c '
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps({
|
||||||
|
"zaak": sys.argv[1],
|
||||||
|
"betrokkeneType": "natuurlijk_persoon",
|
||||||
|
"roltype": sys.argv[2],
|
||||||
|
"roltoelichting": "Initiator",
|
||||||
|
"betrokkeneIdentificatie": {"inpBsn": sys.argv[3]},
|
||||||
|
}))
|
||||||
|
' "$zaak_url" "$roltype_url" "$BSN")" >/dev/null
|
||||||
|
|
||||||
|
cat > seeded.env <<EOF
|
||||||
|
ZAAK_REFERENTIE=$ZAAK_REF
|
||||||
|
ZAAK_URL=$zaak_url
|
||||||
|
ZAAKTYPE_LABEL=Herregistratie arts
|
||||||
|
CALLER_BSN=$BSN
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Seed complete. $ZAAK_REF ($zaak_url) — see seeded.env"
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# WP-54 — a real OpenZaak to develop/test the ZGW seam against, kept OUT of the root
|
||||||
|
# docker-compose.yml on purpose (see backend/openzaak/README.md): OpenZaak is a full Django
|
||||||
|
# stack (postgres + redis), heavy compared to this repo's own FE+BFF, and nobody who isn't
|
||||||
|
# touching the ZGW slice should have to pull/boot it.
|
||||||
|
#
|
||||||
|
# ponytail: trimmed vs. open-zaak's own published compose — no celery/celery-beat/celery-flower
|
||||||
|
# (async notification delivery, never asserted by the integration test) and no nginx (the test
|
||||||
|
# hits web's port directly). Add them back only if a later WP needs an actual notification
|
||||||
|
# round-trip against this harness (NRC delivery is already covered by fixture tests, WP-52).
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgis/postgis:17-3.5
|
||||||
|
environment:
|
||||||
|
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||||
|
- POSTGRES_DB=openzaak
|
||||||
|
- POSTGRES_USER=openzaak
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U openzaak']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:8
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
# One-shot: migrate the schema, then apply setup_configuration/data.yaml (JWTSecret +
|
||||||
|
# Applicatie for the bootstrap script below) — the documented, scripted alternative to
|
||||||
|
# clicking through the Django admin (see openzaak_config_cli in upstream docs).
|
||||||
|
web-init:
|
||||||
|
image: openzaak/open-zaak:1.29.1
|
||||||
|
environment: &app-env
|
||||||
|
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
||||||
|
SECRET_KEY: wp-54-local-harness-not-for-prod
|
||||||
|
DB_HOST: db
|
||||||
|
DB_NAME: openzaak
|
||||||
|
DB_USER: openzaak
|
||||||
|
IS_HTTPS: 'no'
|
||||||
|
SITE_DOMAIN: localhost:8000
|
||||||
|
ALLOWED_HOSTS: localhost,127.0.0.1,web
|
||||||
|
CACHE_DEFAULT: redis:6379/0
|
||||||
|
CACHE_AXES: redis:6379/0
|
||||||
|
DISABLE_2FA: 'true'
|
||||||
|
CELERY_BROKER_URL: redis://redis:6379/0
|
||||||
|
CELERY_RESULT_BACKEND: redis://redis:6379/0
|
||||||
|
RUN_SETUP_CONFIG: 'true'
|
||||||
|
# No celery worker in this trimmed harness (see the top-of-file note) to actually
|
||||||
|
# deliver a notification — without this, OpenZaak 500s (and rolls back!) every create
|
||||||
|
# on a notified resource (zaaktype, zaak, ...) because NotificationsConfig has no
|
||||||
|
# client configured (see notifications_api_common.viewsets.NotificationMixin.notify).
|
||||||
|
NOTIFICATIONS_DISABLED: 'true'
|
||||||
|
command: /setup_configuration.sh
|
||||||
|
volumes:
|
||||||
|
- ./setup_configuration:/app/setup_configuration:ro
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: openzaak/open-zaak:1.29.1
|
||||||
|
environment: *app-env
|
||||||
|
ports:
|
||||||
|
- '8000:8000'
|
||||||
|
depends_on:
|
||||||
|
web-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Applied by web-init (RUN_SETUP_CONFIG=true → `manage.py setup_configuration`, upstream's
|
||||||
|
# documented CLI config mechanism — see docker-compose.openzaak.yml) instead of the Django
|
||||||
|
# admin. Creates the ONE application the bootstrap script + integration test authenticate as.
|
||||||
|
#
|
||||||
|
# ponytail: heeft_alle_autorisaties (all scopes) rather than a granular per-component/scope
|
||||||
|
# list — this instance only ever exists for this harness/test, never a shared or prod
|
||||||
|
# OpenZaak, so there's no least-privilege boundary worth modeling here.
|
||||||
|
sites_config_enable: true
|
||||||
|
sites_config:
|
||||||
|
items:
|
||||||
|
- domain: localhost:8000
|
||||||
|
name: OpenZaak (WP-54 harness)
|
||||||
|
|
||||||
|
vng_api_common_credentials_config_enable: true
|
||||||
|
vng_api_common_credentials:
|
||||||
|
items:
|
||||||
|
- identifier: bigregister-test
|
||||||
|
secret: bigregister-test-secret
|
||||||
|
|
||||||
|
vng_api_common_applicaties_config_enable: true
|
||||||
|
vng_api_common_applicaties:
|
||||||
|
items:
|
||||||
|
- uuid: 5a09b3c9-6a54-4b2b-8f3c-1f9b6b6a3a01
|
||||||
|
client_ids:
|
||||||
|
- bigregister-test
|
||||||
|
label: BIG-register BFF (WP-54 test harness)
|
||||||
|
heeft_alle_autorisaties: true
|
||||||
@@ -38,5 +38,11 @@ internal sealed class ZgwHttpClient(HttpClient http, ZgwTokenProvider tokens)
|
|||||||
{
|
{
|
||||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", caller is null ? tokens.Mint() : tokens.Mint(caller));
|
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", caller is null ? tokens.Mint() : tokens.Mint(caller));
|
||||||
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
// Every ZGW request must declare a coordinate reference system, even when no geometry is
|
||||||
|
// involved (Zaak has an optional zaakgeometrie) — a real OpenZaak 412s ("Content-Crs
|
||||||
|
// header ontbreekt") without it. Only surfaced by WP-54's live harness: the fixture/stub
|
||||||
|
// tests never modelled this header, so this bug shipped unnoticed since WP-49/50.
|
||||||
|
req.Headers.Add("Accept-Crs", "EPSG:4326");
|
||||||
|
if (req.Content is not null) req.Content.Headers.Add("Content-Crs", "EPSG:4326");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using BigRegister.Api.Contracts;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
|
||||||
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WP-54: the one test that proves the BFF actually talks to a REAL OpenZaak — auth accepted,
|
||||||
|
/// real response shapes, real pagination/zaaktype resolution — rather than the stub
|
||||||
|
/// HttpMessageHandler every other Zgw test (<see cref="ZgwZaakMapperTests"/>,
|
||||||
|
/// <see cref="OpenZaakZaakSourceTests"/>) uses. Requires the harness in <c>backend/openzaak/</c>
|
||||||
|
/// to be up and seeded first (see its README); tagged Category=Integration so it's excluded
|
||||||
|
/// from the default `dotnet test` run and from CI (`ci.yml`, `scripts/ci-local.sh` both filter
|
||||||
|
/// it out) — nobody without a live OpenZaak should see it fail.
|
||||||
|
///
|
||||||
|
/// Not an <see cref="IClassFixture{TFixture}"/> off <see cref="TestWebApplicationFactory"/>:
|
||||||
|
/// that fixture hardcodes <c>Zgw:Enabled=false</c> (offline default) for every other test class,
|
||||||
|
/// so this one builds its own <see cref="WebApplicationFactory{TEntryPoint}"/> layering the
|
||||||
|
/// harness's URLs/credentials (matching <c>backend/openzaak/setup_configuration/data.yaml</c>
|
||||||
|
/// and <c>bootstrap-catalogus.sh</c>) on top.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Integration")]
|
||||||
|
public class OpenZaakIntegrationTests
|
||||||
|
{
|
||||||
|
private static WebApplicationFactory<Program> Factory()
|
||||||
|
{
|
||||||
|
var dbPath = Path.Combine(Path.GetTempPath(), $"bigregister-oz-integration-{Guid.NewGuid():N}.db");
|
||||||
|
return new WebApplicationFactory<Program>().WithWebHostBuilder(builder => builder
|
||||||
|
.UseSetting("ConnectionStrings:AppDb", $"Data Source={dbPath}")
|
||||||
|
.UseSetting("Zgw:Enabled", "true")
|
||||||
|
.UseSetting("Zgw:ZrcBaseUrl", "http://localhost:8000/zaken/api/v1")
|
||||||
|
.UseSetting("Zgw:ZtcBaseUrl", "http://localhost:8000/catalogi/api/v1")
|
||||||
|
.UseSetting("Zgw:ClientId", "bigregister-test")
|
||||||
|
.UseSetting("Zgw:Secret", "bigregister-test-secret")
|
||||||
|
.UseSetting("Zgw:UserId", "bigregister-test")
|
||||||
|
.UseSetting("Zgw:UserRepresentation", "WP-54 integration test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Admin_cases_returns_the_seeded_zaak_mapped_through_real_HTTP_and_JWT()
|
||||||
|
{
|
||||||
|
using var factory = Factory();
|
||||||
|
using var client = factory.CreateClient();
|
||||||
|
client.DefaultRequestHeaders.Add("X-Role", "admin"); // CasesAdmin gate (cases:manage)
|
||||||
|
|
||||||
|
var cases = await client.GetFromJsonAsync<List<ApplicationSummaryDto>>("/api/v1/admin/cases");
|
||||||
|
|
||||||
|
Assert.NotNull(cases);
|
||||||
|
// bootstrap-catalogus.sh seeds exactly one zaak, identificatie BIG-2026-000123, under a
|
||||||
|
// zaaktype whose omschrijving is "Herregistratie arts" — see backend/openzaak/README.md.
|
||||||
|
var seeded = Assert.Single(cases!, c => c.Status.Referentie == "BIG-2026-000123");
|
||||||
|
Assert.Equal("Herregistratie arts", seeded.Type);
|
||||||
|
Assert.Equal("InBehandeling", seeded.Status.Tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,7 +104,7 @@ for its existing violations, so every WP ends green.
|
|||||||
| [WP-51](WP-51-openzaak-documenten.md) | OpenZaak Documenten (DRC) upload + zaak link | 9 · OpenZaak/ZGW | done |
|
| [WP-51](WP-51-openzaak-documenten.md) | OpenZaak Documenten (DRC) upload + zaak link | 9 · OpenZaak/ZGW | done |
|
||||||
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | done |
|
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | done |
|
||||||
| [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | done |
|
| [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | done |
|
||||||
| [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | todo |
|
| [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | done |
|
||||||
|
|
||||||
Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn);
|
Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn);
|
||||||
03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed
|
03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# WP-54 — Docker OpenZaak integration-test harness
|
# WP-54 — Docker OpenZaak integration-test harness
|
||||||
|
|
||||||
Status: todo
|
Status: done
|
||||||
Phase: 9 — OpenZaak / ZGW integration
|
Phase: 9 — OpenZaak / ZGW integration
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
@@ -87,17 +87,19 @@ OpenZaak facts that shape the harness (from the ZGW research):
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] `docker compose -f backend/openzaak/docker-compose.openzaak.yml up` yields a reachable
|
- [x] `docker compose -f backend/openzaak/docker-compose.openzaak.yml up` yields a reachable
|
||||||
OpenZaak with the seeded catalogus + zaak, and credentials matching `ZgwOptions`.
|
OpenZaak with the seeded catalogus + zaak, and credentials matching `ZgwOptions`.
|
||||||
- [ ] The Category=Integration test passes against it; the BFF returns the seeded zaak mapped to
|
- [x] The Category=Integration test passes against it; the BFF returns the seeded zaak mapped to
|
||||||
`ApplicationSummaryDto` through the real HTTP + JWT path.
|
`ApplicationSummaryDto` through the real HTTP + JWT path.
|
||||||
- [ ] Default `dotnet test` and `npm run ci` are unaffected (integration test excluded, no docker
|
- [x] Default `dotnet test` and `npm run ci` are unaffected (integration test excluded, no docker
|
||||||
needed); `docker compose up` (root) is unchanged.
|
needed); `docker compose up` (root) is unchanged.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
`docker compose -f backend/openzaak/docker-compose.openzaak.yml up -d` →
|
`docker compose -f backend/openzaak/docker-compose.openzaak.yml up -d` →
|
||||||
`dotnet test --filter Category=Integration` → green; then teardown.
|
`./backend/openzaak/bootstrap-catalogus.sh` → `dotnet test --filter Category=Integration` →
|
||||||
|
green; then teardown. Actually run (not just planned) during this WP — see Deviations below
|
||||||
|
for what that surfaced.
|
||||||
|
|
||||||
## Out of scope
|
## Out of scope
|
||||||
|
|
||||||
@@ -112,3 +114,32 @@ always-on CI job (keep it opt-in/manual — OpenZaak startup is slow), performan
|
|||||||
keep the fixture in the repo.
|
keep the fixture in the repo.
|
||||||
- Bootstrap client scopes must include `catalogi.lezen` or zaaktype resolution 403s — cover in
|
- Bootstrap client scopes must include `catalogi.lezen` or zaaktype resolution 403s — cover in
|
||||||
the setup script.
|
the setup script.
|
||||||
|
|
||||||
|
## Deviations from the original plan
|
||||||
|
|
||||||
|
- **`heeft_alle_autorisaties: true` instead of granular scopes.** The plan called out
|
||||||
|
`zaken.lezen`/`catalogi.lezen` specifically; in practice OpenZaak's scripted config
|
||||||
|
(`vng_api_common_applicaties_config`, upstream's own documented `setup_configuration` YAML
|
||||||
|
mechanism) exposes an all-scopes flag on the one `Applicatie` this harness ever creates. Since
|
||||||
|
that application exists for nothing but this throwaway test instance, granular scopes would
|
||||||
|
add YAML-schema risk for no real least-privilege benefit — took the simpler, equally-scripted
|
||||||
|
option.
|
||||||
|
- **A live run found a real production bug, not just a harness wrinkle**: `ZgwHttpClient.cs`
|
||||||
|
never sent `Content-Crs`/`Accept-Crs` on any ZGW call. Every ZGW write 412s ("Content-Crs
|
||||||
|
header ontbreekt") without it — a real OpenZaak enforces this; the stub `HttpMessageHandler`
|
||||||
|
every prior Zgw test used never modelled header requirements, so nothing from WP-49/50 caught
|
||||||
|
it before now. Fixed in `ZgwHttpClient.cs` alongside the harness (see
|
||||||
|
`docs/reference/openzaak-integration.md`) — this is precisely the class of bug this WP exists
|
||||||
|
to catch.
|
||||||
|
- **Publishing a zaaktype needs more seed data than the plan anticipated**: OpenZaak refuses to
|
||||||
|
publish a zaaktype with fewer than one resultaattype or fewer than two statustypen (begin +
|
||||||
|
eind), and a resultaattype's `selectielijstklasse` must share a `procesType` with the
|
||||||
|
zaaktype's own `selectielijstProcestype` — both cross-checked live against the public VNG
|
||||||
|
selectielijst API (`selectielijst.openzaak.nl`). `bootstrap-catalogus.sh` seeds all of this;
|
||||||
|
see its comments for the exact values used and why.
|
||||||
|
- **No celery/celery-beat/nginx in the harness**, unlike upstream's own compose — trimmed for a
|
||||||
|
faster-booting, single-purpose harness (this test never asserts on notification delivery,
|
||||||
|
which is celery's job). `NOTIFICATIONS_DISABLED=true` is required as a consequence: without a
|
||||||
|
celery worker, `NotificationsConfig` has no client, and OpenZaak's `notify()` hook otherwise
|
||||||
|
raises inside the same DB transaction as the create — turning a missing-worker problem into a
|
||||||
|
500 that rolls back the create it was supposed to just notify about.
|
||||||
|
|||||||
@@ -101,7 +101,11 @@ confidentiality level would matter for production but isn't needed to prove the
|
|||||||
`user_representation`). No refresh flow — OpenZaak expires tokens 1h past `iat`, so per-call
|
`user_representation`). No refresh flow — OpenZaak expires tokens 1h past `iat`, so per-call
|
||||||
minting is the recommended pattern. Hand-rolled (no `Microsoft.IdentityModel.*` dependency).
|
minting is the recommended pattern. Hand-rolled (no `Microsoft.IdentityModel.*` dependency).
|
||||||
- `ZgwHttpClient.cs` — shared GET/POST-with-bearer-JWT plumbing used by both
|
- `ZgwHttpClient.cs` — shared GET/POST-with-bearer-JWT plumbing used by both
|
||||||
`OpenZaakZaakSource` and `OpenZaakDocumentSource`.
|
`OpenZaakZaakSource` and `OpenZaakDocumentSource`. Every request also carries
|
||||||
|
`Accept-Crs`/`Content-Crs: EPSG:4326` — every ZGW call must declare a coordinate reference
|
||||||
|
system even when no geometry is involved, or a real OpenZaak 412s ("Content-Crs header
|
||||||
|
ontbreekt"). This was missing until WP-54's live harness caught it — the stub-handler tests
|
||||||
|
never modelled the header, so it had shipped silently since WP-49/50.
|
||||||
- `ZgwZaakMapper.cs` — the anti-corruption map: ZGW Zaak → `ApplicationSummaryDto`. This is
|
- `ZgwZaakMapper.cs` — the anti-corruption map: ZGW Zaak → `ApplicationSummaryDto`. This is
|
||||||
where **URL identity** becomes the trailing uuid and the **zaaktype URL** is resolved to a
|
where **URL identity** becomes the trailing uuid and the **zaaktype URL** is resolved to a
|
||||||
human label (the cross-service join).
|
human label (the cross-service join).
|
||||||
@@ -204,6 +208,22 @@ FE or the contract. Watch the **sync-over-async** `ponytail:` note in `OpenZaakZ
|
|||||||
its `OpenZaakDocumentSource` sibling) — make the read/write paths async if OpenZaak becomes the
|
its `OpenZaakDocumentSource` sibling) — make the read/write paths async if OpenZaak becomes the
|
||||||
default.
|
default.
|
||||||
|
|
||||||
|
## Run against real OpenZaak (WP-54)
|
||||||
|
|
||||||
|
Everything above was, until WP-54, only proven against fixtures + a stub `HttpMessageHandler` —
|
||||||
|
no live OpenZaak. `backend/openzaak/` is a **separate**, opt-in docker-compose harness (never
|
||||||
|
merged into the root `docker-compose.yml`, which stays FE+BFF-only) that brings up a real
|
||||||
|
OpenZaak, seeds a minimal catalogus/zaaktype/zaak via a bootstrap script, and backs one
|
||||||
|
xunit test (`OpenZaakIntegrationTests.cs`, tagged `Category=Integration`) that points the BFF at
|
||||||
|
it with `Zgw:Enabled=true`. See `backend/openzaak/README.md` for the exact commands; the test is
|
||||||
|
excluded from the default `dotnet test` run and from CI (`--filter Category!=Integration`) since
|
||||||
|
it only passes with the harness up.
|
||||||
|
|
||||||
|
This is also where the `Content-Crs`/`Accept-Crs` header gap above was found: a real OpenZaak
|
||||||
|
enforces ZGW's geo-header requirement in a way no stub-based test could catch, since a stub
|
||||||
|
never rejects an unexpected (or missing) header. That is the harness's whole point — proving
|
||||||
|
the seam against real protocol behaviour, not just the shapes we already assumed.
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
@@ -270,15 +290,16 @@ Caveat: `IZaakSource` covers the cases **read (admin + citizen-scoped) + create*
|
|||||||
(WP-49/50/53), `IDocumentSource` covers **upload + zaak-link** (WP-51), the inbound
|
(WP-49/50/53), `IDocumentSource` covers **upload + zaak-link** (WP-51), the inbound
|
||||||
`POST /zgw/notificaties` webhook (WP-52) closes the read/write/document/notify arc, and WP-53
|
`POST /zgw/notificaties` webhook (WP-52) closes the read/write/document/notify arc, and WP-53
|
||||||
threaded a real per-request `CallerIdentity` through all of it (ownership + the ZGW audit
|
threaded a real per-request `CallerIdentity` through all of it (ownership + the ZGW audit
|
||||||
claims). Other BFF endpoints (reference data like `SeedData`'s BRP/DUO mimics) still read static
|
claims), and WP-54 added a docker OpenZaak harness + opt-in integration test proving the seam
|
||||||
stores directly — ACL-ready (the DTO seam exists) but not yet swappable, and not part of this
|
against a live instance (and, in doing so, caught the missing `Content-Crs`/`Accept-Crs`
|
||||||
arc. What's left is **WP-54**: a docker OpenZaak harness + opt-in integration test — today
|
headers noted above). Other BFF endpoints (reference data like `SeedData`'s BRP/DUO mimics)
|
||||||
everything is fixture/mock-tested against no live instance.
|
still read static stores directly — ACL-ready (the DTO seam exists) but not yet swappable, and
|
||||||
|
not part of this arc. That closes the phase-9 OpenZaak/ZGW arc (WP-49..54).
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
- [ADR-0005 — OpenZaak behind the BFF](architecture/0005-openzaak-behind-bff.md) — the decision.
|
- [ADR-0005 — OpenZaak behind the BFF](architecture/0005-openzaak-behind-bff.md) — the decision.
|
||||||
- [ADR-0001 — BFF-lite + decision DTOs](architecture/0001-bff-lite-decision-dtos.md) — why the FE doesn't change.
|
- [ADR-0001 — BFF-lite + decision DTOs](architecture/0001-bff-lite-decision-dtos.md) — why the FE doesn't change.
|
||||||
- [WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (this), WP-50/51 (CRUD arc so far), WP-52 (notificaties), WP-53 (identity seam + citizen-scoping), WP-54 (integration harness, open).
|
- [WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (this), WP-50/51 (CRUD arc so far), WP-52 (notificaties), WP-53 (identity seam + citizen-scoping), [WP-54](../project/backlog/WP-54-openzaak-integration-harness.md) (integration harness).
|
||||||
- `backend/src/BigRegister.Api/Zgw/` — the client; `Data/IZaakSource.cs`/`Data/IDocumentSource.cs` — the seams.
|
- `backend/src/BigRegister.Api/Zgw/` — the client; `Data/IZaakSource.cs`/`Data/IDocumentSource.cs` — the seams; `backend/openzaak/` — the live-OpenZaak test harness (WP-54).
|
||||||
- [ZGW standard (VNG)](https://vng-realisatie.github.io/gemma-zaken/) · [OpenZaak auth docs](https://open-zaak.readthedocs.io/en/stable/client-development/authentication.html).
|
- [ZGW standard (VNG)](https://vng-realisatie.github.io/gemma-zaken/) · [OpenZaak auth docs](https://open-zaak.readthedocs.io/en/stable/client-development/authentication.html).
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ step "check:tokens"; npm run check:tokens
|
|||||||
step "test (vitest + coverage)"; npm run test:coverage
|
step "test (vitest + coverage)"; npm run test:coverage
|
||||||
step "build --localize (nl+en)"; npx ng build --localize
|
step "build --localize (nl+en)"; npx ng build --localize
|
||||||
step "npm audit (shipped deps)"; npm audit --omit=dev
|
step "npm audit (shipped deps)"; npm audit --omit=dev
|
||||||
step "backend format + tests"; ( cd backend && dotnet format BigRegister.slnx --verify-no-changes && dotnet test BigRegister.slnx )
|
step "backend format + tests"; ( cd backend && dotnet format BigRegister.slnx --verify-no-changes && dotnet test BigRegister.slnx --filter "Category!=Integration" )
|
||||||
step "showcase snippets drift"; npm run gen:snippets && git diff --exit-code src/app/showcase/snippets.generated.ts
|
step "showcase snippets drift"; npm run gen:snippets && git diff --exit-code src/app/showcase/snippets.generated.ts
|
||||||
step "api-client drift"; npm run gen:api && git diff --exit-code src/app/shared/infrastructure/api-client.ts backend/swagger.json
|
step "api-client drift"; npm run gen:api && git diff --exit-code src/app/shared/infrastructure/api-client.ts backend/swagger.json
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user