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>
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 viaZgwTokenProvider), selected whenZgw:Enabled=true. Config is theZgwsection ofappsettings.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 defaultdotnet testand CI stay fast/offline. - Existing compose: repo root
docker-compose.ymlruns FE + BFF only (Swagger at :5000). Do not bolt OpenZaak onto it — OpenZaak is heavy (postgres + redis + celery). Use a separate compose file sodocker compose upstays light for everyone else. - Test project:
backend/tests/BigRegister.Tests/(xunit). It usesMicrosoft.AspNetCore.Mvc.Testing(TestWebApplicationFactory.cs) — the factory can be configured to override theZgwconfig 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.ymland itsimportfixtures 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 upstays 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
importfixture or a small bootstrap script) creates the client credentials (matchingZgwOptions), 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 shortbackend/openzaak/README.md(how to bring it up, credentials, teardown). - New:
backend/tests/BigRegister.Tests/OpenZaakIntegrationTests.cs(Category=Integration): configure the BFF (WebApplicationFactory) withZgw: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.ymlmirroring the e2e job's shape.
Steps
- Add the separate compose file bringing up OpenZaak + postgres + redis; pin image versions.
- 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). - 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. - 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 upyields a reachable OpenZaak with the seeded catalogus + zaak, and credentials matchingZgwOptions.- The Category=Integration test passes against it; the BFF returns the seeded zaak mapped to
ApplicationSummaryDtothrough the real HTTP + JWT path. - Default
dotnet testandnpm run ciare 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.sh → dotnet 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.lezenor zaaktype resolution 403s — cover in the setup script.
Deviations from the original plan
heeft_alle_autorisaties: trueinstead of granular scopes. The plan called outzaken.lezen/catalogi.lezenspecifically; in practice OpenZaak's scripted config (vng_api_common_applicaties_config, upstream's own documentedsetup_configurationYAML mechanism) exposes an all-scopes flag on the oneApplicatiethis 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.csnever sentContent-Crs/Accept-Crson any ZGW call. Every ZGW write 412s ("Content-Crs header ontbreekt") without it — a real OpenZaak enforces this; the stubHttpMessageHandlerevery prior Zgw test used never modelled header requirements, so nothing from WP-49/50 caught it before now. Fixed inZgwHttpClient.csalongside the harness (seedocs/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
selectielijstklassemust share aprocesTypewith the zaaktype's ownselectielijstProcestype— both cross-checked live against the public VNG selectielijst API (selectielijst.openzaak.nl).bootstrap-catalogus.shseeds 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=trueis required as a consequence: without a celery worker,NotificationsConfighas no client, and OpenZaak'snotify()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.