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:
@@ -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
|
||||
Reference in New Issue
Block a user