The backend half of the sweep RD-18 did for the front end. git blame holds the provenance and stays correct when the code moves; the comment names a closed ticket and tells the reader nothing the sentence around it does not. public/letter.css and LetterHtml.golden.html change together, because the renderer inlines the CSS and the golden file snapshots the result. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
14 KiB
OpenZaak integration harness
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 for the
full picture; this is just "how to run it".
Bring it up
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.
Idempotent — every resource is looked up by its natural key (the same field(s)
OpenZaak enforces identity on: catalogus by domein+rsin, zaaktype by catalogus+
identificatie, statustype by zaaktype+volgnummer, roltype by zaaktype+
omschrijvingGeneriek, zaak by identificatie) before creating it, so re-running against an
already-seeded instance reuses what's there instead of erroring. Safe to run repeatedly
against a long-lived instance, not just once per fresh volume — a full reset is still
available if you want a truly clean slate:
docker compose -f docker-compose.openzaak.yml down -v && docker compose -f docker-compose.openzaak.yml up -d
Check it is running
docker compose up at the repo root does not start OpenZaak. OpenZaak is a separate
stack. Bring it up with the command above, or with scripts/openzaak-ui-up.sh below.
Check the containers:
docker compose -f docker-compose.openzaak.yml ps
Look for four containers: db, redis, web-init, web. db and redis show a health
status. web-init runs once and exits. Its exit code must be 0. web has no health
status. Check it directly instead:
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8000/catalogi/api/v1/catalogussen
A 200, 401, or 403 response means OpenZaak is up and answering. This is the same
check bootstrap-catalogus.sh uses to wait for OpenZaak before it seeds anything.
To confirm the BFF (not just OpenZaak) is wired up and can write to it, see "Testing the
Angular UI against this harness" below — scripts/openzaak-ui-up.sh submits a real test
case and confirms OpenZaak received it.
This content has no setup_configuration (declarative-YAML) equivalent: reading the
django_setup_configuration steps installed inside the openzaak/open-zaak:1.29.1 image
itself confirms the only app-registered steps are sites/credentials/applicaties (already used
by setup_configuration/data.yaml) and Selectielijst API config — nothing for Catalogi/Zaken
content. Hence this stays a script, made safe to rerun instead.
Run the integration test against it
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 + zaaktype→aanvraag-type mapping 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.
Notifications-enabled profile
The base harness above never delivers a real notification (NOTIFICATIONS_DISABLED: 'true',
no celery worker) — fine for the read/write ZGW seam, not for proving a live webhook round-trip.
An opt-in overlay adds the one celery worker needed, flips that flag, and points OpenZaak
straight at this repo's own BFF webhook (no real Notificaties API/NRC in this harness — see
docs/reference/openzaak-integration.md's
"Notifications-enabled profile" section for why and how). Needs the repo root's own
docker compose up (or an equivalent api container) running too, since the celery worker
reaches the BFF by container name on that network:
docker compose run --rm -d --name atomic-design-poc-api-1 --service-ports \
-e Zgw__NotificatieAuthorization='<a secret>' api # repo root
cd backend/openzaak
docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.notificaties.yml up -d
./bootstrap-catalogus.sh
BFF_AUTH='<the same secret>' ./bootstrap-notificaties.sh
BFF_AUTH='<the same secret>' ./verify-notificatie.sh # proves a real delivery, end to end
To go back to the fast, no-notifications default: docker compose -f docker-compose.openzaak.yml up -d --remove-orphans (drops the celery worker, restores NOTIFICATIONS_DISABLED: 'true').
Testing the Angular UI against this harness
The base harness above and the app's own root docker-compose.yml are independent projects on
purpose (see the top of this file) — this is the opt-in bridge between them, for when you want
to click through the real UI and see an aanvraag land in a real OpenZaak instead of just
running dotnet test. One command from the repo root:
scripts/openzaak-ui-up.sh
It brings up the root app (so its docker network exists), brings up this harness plus
docker-compose.openzaak.bff.yml (gives this harness's web service a dotted alias,
openzaak.local, on its own network — the root project's api container joins THIS network,
in the opposite direction from the notifications overlay below, to avoid a real alias
collision: the root project's frontend service is also called web. The alias needs a dot
because Django's URLValidator rejects a bare hostname in a URL field; this environment's
rootless Podman also can't route container→host-port traffic through host.docker.internal,
so container-to-container is the only reliable path either way — see that file's header
comment for the full, empirically-confirmed reasoning), seeds the catalogus, additively
replaces the zrc authorization grant to match the alias (ZGW authorization is scoped by the
exact zaaktype URL string, not just the resource; see scripts/openzaak-ui-up.sh's own
comment for why this is a replace, not an add), and brings the root app back up pointed at
OpenZaak (docker-compose.openzaak.yml at the repo root). A final self-check submits a
throwaway aanvraag and confirms it actually lands in OpenZaak, restarting api (up to 5
times) if not — see that script for a caveat about an intermittent per-container networking
flake this environment can hit under memory pressure (the script now warns if host swap is
already high going in; ZGW_DEBUG_HTTP=1 on api, see docker-compose.openzaak.yml, logs
diagnostics to help nail the cause next time it reproduces). Prints the URLs to check
afterward and the teardown commands.
Two caveats, both non-fatal (the BFF catches and flags rather than surfacing an error):
only herregistratie has a seeded zaaktype here, so submit that wizard to prove a real
write; and no Documenten content is seeded, so a document upload's ZGW half no-ops (pick
"per post" in the wizard's document step, or ignore it).
Tear down
docker compose -f docker-compose.openzaak.yml down -v
Production
This dev harness stays dev-only: hardcoded SECRET_KEY, POSTGRES_HOST_AUTH_METHOD=trust,
IS_HTTPS: 'no', a client secret checked into setup_configuration/data.yaml. A real
deployment layers docker-compose.openzaak.prod.yml on top instead of replacing anything:
export OPENZAAK_SECRET_KEY=... # Django SECRET_KEY — generate, don't reuse the dev value
export OPENZAAK_DB_PASSWORD=... # postgres password (switches auth off `trust`)
export OPENZAAK_SITE_DOMAIN=... # e.g. open-zaak.example.org — no scheme/port
export OPENZAAK_ALLOWED_HOSTS=... # Django ALLOWED_HOSTS, usually the same domain
export OPENZAAK_CLIENT_ID=... # the BFF's OpenZaak client id (ZgwOptions:ClientId)
export OPENZAAK_CLIENT_SECRET=... # the BFF's JWT signing secret (ZgwOptions:Secret)
export OPENZAAK_APPLICATIE_UUID=$(uuidgen)
./render-prod-secrets.sh # writes the gitignored setup_configuration/data.prod.yaml
docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.prod.yml up -d
Every one of those env vars is ${VAR:?...}-checked — compose (and render-prod-secrets.sh
for the client secret) refuses to start rather than silently falling back to a dev-looking
default. There is no env var that "means insecure default"; if it's unset, it's a hard error.
TLS: OpenZaak itself does no certificate handling. Put a reverse proxy/ingress (the same
one fronting the BFF) in front of web's :8000, terminate TLS there, and forward to
http://web:8000 over the compose network. IS_HTTPS: 'yes' in the prod override only tells
Django it's being served over HTTPS (secure cookies, SECURE_* redirects) — it does not open
a TLS listener itself.
The BFF side needs no code change: ZgwOptions already binds ClientId/Secret/the base
URLs from IConfiguration, so pointing it at a production OpenZaak is a config change
(Zgw:ClientId/Zgw:Secret/Zgw:ZrcBaseUrl etc. via env vars or a secrets manager), not an
app change.
What's in here / what isn't
docker-compose.openzaak.yml— postgres (postgis), redis, a one-shotweb-init(runs Django migrations thensetup_configurationagainstsetup_configuration/data.yaml), andweb(the OpenZaak API on:8000). Pinned toopenzaak/open-zaak:1.29.1. No celery/celery-beat/celery-flower/nginx — trimmed for a lean, fast-booting harness; layerdocker-compose.openzaak.notificaties.ymlon top for a real async notification delivery round-trip.NOTIFICATIONS_DISABLED=trueis 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 documentedsetup_configurationCLI mechanism): creates the onebigregister-testclient withheeft_alle_autorisaties: false— this YAML mechanism (vng_api_common'sApplicatieConfigurationModel) has no field for granular scopes at all, so the client starts with zero Autorisaties;bootstrap-catalogus.shgrants the exact ones it needs.bootstrap-catalogus.sh— the business content (catalogus/zaaktype/zaak/…)setup_configurationhas 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 itsselectielijstklasseand the zaaktype'sselectielijstProcestypemust reference the sameprocesTypeon the public VNG selectielijst API). Idempotent — see "Bring it up" above. Also grantsbigregister-test's Autorisaties viamanage.py shell(see the script's top comment):ztcscopes (catalogi.lezen/catalogi.schrijven, this script's own content-creation needs) up front,zrcscopes (zaken.aanmaken/zaken.bijwerken/zaken.lezen, scoped to the one zaaktype the BFF and this script both use) once that zaaktype exists. Nodocumenten/DRC grant —Zgw:InformatieobjecttypeUrlsis empty in this harness'sappsettings.json, soOpenZaakDocumentSourceisn't reachable here yet; add the grant (scoped to a realinformatieobjecttype, which this script would also need to seed) when a later WP wires DRC content into this harness.- Not here: Documenten (DRC) content, or a real Notificaties API (NRC) — add DRC content if a
later change needs to prove that round-trip against a live instance too (fixture-tested
today). A real NRC is a separate application (
open-notificaties) this harness deliberately doesn't stand up — the notifications-enabled profile (below) proves live delivery without one, since this harness only ever has one subscriber. docker-compose.openzaak.notificaties.yml— opt-in overlay: one celery worker for OpenZaak (async notification delivery needs it) +NOTIFICATIONS_DISABLED: 'false', joined to the repo root's own compose network so it can reach theapicontainer by name (triedhost.docker.internal:host-gatewayfirst; this environment's rootless Podman doesn't route container→host-port traffic through it). See "Notifications-enabled profile" below.bootstrap-notificaties.sh— points OpenZaak'sNotificationsConfigat the BFF's webhook via azgw_consumers.Service(update_or_create, idempotent) instead of provisioning a real NRCabonnement; preflights that the BFF is reachable with the right secret first (a misconfigured target here means every write to a notified resource 500s and rolls back).verify-notificatie.sh— the runnable end-to-end check: PATCHes the seeded zaak, polls the BFF's own/admin/auditfor the resultingzgw:notificatie/allowrow.docker-compose.openzaak.prod.yml— production overrides layered on top ofdocker-compose.openzaak.yml: realSECRET_KEY/DB password/site domain/allowed-hosts from required env vars (fails fast if unset), password DB auth instead oftrust,IS_HTTPS: 'yes'. Adds no image/service of its own — see "Production" above for the full flow.setup_configuration/data.prod.yaml.template— the prod counterpart ofdata.yamlwith no secret in it (${OPENZAAK_CLIENT_SECRET}etc. as placeholders);render-prod-secrets.shfills it in to the gitignoreddata.prod.yaml, which the prod compose override mounts over the container'sdata.yaml.