# 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](../../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. **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: ```bash 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: ```bash 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: ```bash 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 ```bash cd backend dotnet test --filter Category=Integration ``` `OpenZaakIntegrationTests.cs` points a `WebApplicationFactory` 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](../../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: ```bash docker compose run --rm -d --name atomic-design-poc-api-1 --service-ports \ -e Zgw__NotificatieAuthorization='' 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='' ./bootstrap-notificaties.sh BFF_AUTH='' ./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: ```bash 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 ```bash 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: ```bash 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` 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. - `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 — see "Bring it up" above. Also grants `bigregister-test`'s Autorisaties via `manage.py shell` (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 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 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` — 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` — the runnable end-to-end check: PATCHes the seeded zaak, polls the BFF's own `/admin/audit` for the resulting `zgw:notificatie`/`allow` row. - `docker-compose.openzaak.prod.yml` — 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` — 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`.