Files
atomic-design-poc/docs/project/archive/backlog/WP-54-openzaak-integration-harness.md
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +02:00

9.0 KiB

WP-54 — Docker OpenZaak integration-test harness

Status: done Phase: 9 — OpenZaak / ZGW integration

Why

Everything ZGW so far is verified against fixtures + a stub HttpMessageHandler — nothing exercises a real OpenZaak. That is fine for unit-testing the mapper/JWT/pagination, but it does not prove the BFF actually talks to OpenZaak (auth accepted, real response shapes, real pagination, zaaktype resolution). This WP stands up a local OpenZaak via docker, seeds a minimal catalog + a client credential matching ZgwOptions, and adds an opt-in integration test that points the BFF at it with Zgw:Enabled=true. It closes the "no live instance" gap called out in the roadmap and makes WP-50/51/52 developable against something real.

Context — current state (read before designing; self-contained handoff)

  • What exists to test: backend/src/BigRegister.Api/Zgw/OpenZaakZaakSource.cs (lists zaken, follows {count,next,previous,results}, resolves zaaktype labels from Catalogi, Bearer auth via ZgwTokenProvider), selected when Zgw:Enabled=true. Config is the Zgw section of appsettings.json (ZrcBaseUrl, ZtcBaseUrl, ClientId, Secret, UserId, UserRepresentation) → Zgw/ZgwOptions.cs.
  • Current tests (the pattern to extend, not replace): ZgwZaakMapperTests (inline JSON), OpenZaakZaakSourceTests (stub handler). These stay as fast unit tests; the new integration test is a separate, opt-in category so the default dotnet test and CI stay fast/offline.
  • Existing compose: repo root docker-compose.yml runs FE + BFF only (Swagger at :5000). Do not bolt OpenZaak onto it — OpenZaak is heavy (postgres + redis + celery). Use a separate compose file so docker compose up stays light for everyone else.
  • Test project: backend/tests/BigRegister.Tests/ (xunit). It uses Microsoft.AspNetCore.Mvc.Testing (TestWebApplicationFactory.cs) — the factory can be configured to override the Zgw config section to point at the compose instance.
  • CLAUDE.md / backlog GREEN: the default local gate and CI must remain runnable without docker/OpenZaak. Anything requiring the harness is explicitly separate (like e2e is today — a distinct job, not chained into npm run ci).

OpenZaak facts that shape the harness (from the ZGW research):

  • OpenZaak is the reference impl of the 5 ZGW APIs; the published docker-compose.yml (github.com/open-zaak/open-zaak) brings up openzaak (web + celery), postgres, redis and (optionally) Open Notificaties. A one-time bootstrap is required: create a superuser, register an Application in the Autorisaties API with a Client ID/Secret + scopes (zaken.lezen, catalogi.lezen, …), and import/create a Catalogus with a zaaktype (published) so there is something to read.
  • Auth = the HS256 JWT the BFF already mints; the harness's client credentials must match ZgwOptions.ClientId/Secret.
  • Reading a zaak needs read scope on both Zaken and Catalogi (zaaktype resolution).

Read first

  • openzaak-integration.md (the seam + config keys).
  • ADR-0005.
  • WP-19 (WP-19-e2e-smoke.md) — the precedent for a heavy, separate, opt-in test job (mirror its "not chained into the default gate" structure).
  • Upstream: OpenZaak docker docs (https://open-zaak.readthedocs.io/) + the repo's docker-compose.yml and its import fixtures for a demo catalogus.

Decisions (pre-made, don't relitigate)

  • Separate compose file (e.g. backend/openzaak/docker-compose.openzaak.yml), never merged into the root compose. docker compose up stays FE+BFF only.
  • Opt-in test category. Tag the integration test [Trait("Category", "Integration")]; exclude it from the default run (dotnet test --filter Category!=Integration) and from the standard CI jobs. Provide a documented command / optional manual CI job to run it.
  • Bootstrap is scripted, not manual. A checked-in setup (compose import fixture or a small bootstrap script) creates the client credentials (matching ZgwOptions), a catalogus, and one published zaaktype + one zaak — so the test is deterministic and repeatable.
  • Provider stays stubbed (WP-53) — this WP is about the ZGW round-trip, not real user auth.

Files

  • New: backend/openzaak/docker-compose.openzaak.yml + bootstrap fixture/script + a short backend/openzaak/README.md (how to bring it up, credentials, teardown).
  • New: backend/tests/BigRegister.Tests/OpenZaakIntegrationTests.cs (Category=Integration): configure the BFF (WebApplicationFactory) with Zgw:Enabled=true + the compose URLs/creds, hit /admin/cases, assert the seeded zaak comes back mapped.
  • Edit: docs/reference/openzaak-integration.md (add a "Run against real OpenZaak" section); optionally a manual/gated CI job in .github/workflows/ci.yml mirroring the e2e job's shape.

Steps

  1. Add the separate compose file bringing up OpenZaak + postgres + redis; pin image versions.
  2. Script the bootstrap: superuser, Autorisaties Application (Client ID/Secret = ZgwOptions), a catalogus + one published zaaktype + one zaak with a natuurlijk-persoon rol (the seeded BSN).
  3. Write the opt-in integration test: point the BFF at the compose instance, assert the mapped zaak (id = uuid, type = zaaktype label, status) via /admin/cases.
  4. Document docker compose -f … up + the run command; optionally add a manual CI job.

Acceptance criteria

  • docker compose -f backend/openzaak/docker-compose.openzaak.yml up yields a reachable 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 ApplicationSummaryDto through the real HTTP + JWT path.
  • Default dotnet test and npm run ci are unaffected (integration test excluded, no docker needed); docker compose up (root) is unchanged.

Verification

docker compose -f backend/openzaak/docker-compose.openzaak.yml up -d./backend/openzaak/bootstrap-catalogus.shdotnet 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

Documenten/DRC + Notificaties services in the harness (add when WP-51/52 land), a permanent always-on CI job (keep it opt-in/manual — OpenZaak startup is slow), performance testing.

Risks

  • OpenZaak startup is slow + resource-heavy → keep it opt-in; a permanent CI job would blow the same runner budget WP's storybook cap just fixed.
  • ZGW API/version drift vs the pinned image can change response shapes → pin image versions and keep the fixture in the repo.
  • Bootstrap client scopes must include catalogi.lezen or zaaktype resolution 403s — cover in 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.