Files
atomic-design-poc/backend/openzaak/README.md
T
ehoandClaude Opus 5 6cfd70eeeb fix(backend): resolve besluit endpoint's id via Referentie, not local PK
POST /beoordeling/{id}/besluit always 404'd against a real OpenZaak: {id} is the
FE-facing case id from IZaakSource.ListCases, which under OpenZaakZaakSource is the
ZGW zaak's own uuid, not ApplicationStore's primary key. Resolve the case through
ListCases first (same seam the GET sibling already uses), then to the local Aanvraag
via its Referentie — the one identifier stable across both sources.

Adds ApplicationStore.GetByReferentie and a regression test that reproduces the
divergence with a decorating IZaakSource test double instead of a live OpenZaak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:20:36 +02:00

13 KiB

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 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 (WP-56) — 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

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 (WP-58)

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 (WP-60 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 (WP-55)

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-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; layer docker-compose.openzaak.notificaties.yml (WP-58) on top for a real async notification delivery round-trip. 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 with heeft_alle_autorisaties: false — this YAML mechanism (vng_api_common's ApplicatieConfigurationModel) has no field for granular scopes at all, so the client starts with zero Autorisaties; bootstrap-catalogus.sh grants the exact ones it needs (WP-57).
  • 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). Idempotent (WP-56) — see "Bring it up" above. Also grants bigregister-test's Autorisaties via manage.py shell (WP-57, see the script's top comment): ztc scopes (catalogi.lezen/catalogi.schrijven, this script's own content-creation needs) up front, zrc scopes (zaken.aanmaken/zaken.bijwerken/ zaken.lezen, scoped to the one zaaktype the BFF and this script both use) once that zaaktype exists. No documenten/DRC grant — Zgw:InformatieobjecttypeUrls is empty in this harness's appsettings.json, so OpenZaakDocumentSource isn't reachable here yet; add the grant (scoped to a real informatieobjecttype, 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 WP needs to prove that round-trip against a live instance too (WP-51 is fixture-tested today). A real NRC is a separate application (open-notificaties) this harness deliberately doesn't stand up — WP-58's notifications-enabled profile (below) proves live delivery without one, since this harness only ever has one subscriber.
  • docker-compose.openzaak.notificaties.yml (WP-58) — 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 the api container by name (tried host.docker.internal:host-gateway first; this environment's rootless Podman doesn't route container→host-port traffic through it). See "Notifications-enabled profile" below.
  • bootstrap-notificaties.sh (WP-58) — points OpenZaak's NotificationsConfig at the BFF's webhook via a zgw_consumers.Service (update_or_create, idempotent) instead of provisioning a real NRC abonnement; 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 (WP-58) — the runnable end-to-end check: PATCHes the seeded zaak, polls the BFF's own /admin/audit (WP-41) for the resulting zgw:notificatie/allow row.
  • docker-compose.openzaak.prod.yml (WP-55) — production overrides layered on top of docker-compose.openzaak.yml: real SECRET_KEY/DB password/site domain/allowed-hosts from required env vars (fails fast if unset), password DB auth instead of trust, IS_HTTPS: 'yes'. Adds no image/service of its own — see "Production" above for the full flow.
  • setup_configuration/data.prod.yaml.template (WP-55) — the prod counterpart of data.yaml with no secret in it (${OPENZAAK_CLIENT_SECRET} etc. as placeholders); render-prod-secrets.sh fills it in to the gitignored data.prod.yaml, which the prod compose override mounts over the container's data.yaml.