Files
register-referentie/docs/architecture/adr-0006-integration-test-provisioning.md
Niek Otten d49443353e
All checks were successful
CI / lint (pull_request) Successful in 51s
CI / build (pull_request) Successful in 40s
CI / unit (pull_request) Successful in 48s
CI / mutation (pull_request) Successful in 1m36s
CI / verify-stack (pull_request) Successful in 4m37s
refactor(ci): one verify-stack stage for all live-stack checks (closes #58) (refs #46 #56)
On the single self-hosted runner CI jobs run sequentially, so booting OpenZaak once
beats once-per-job. Replace the integration + notifications + compose-smoke jobs with
one verify-stack job that brings the full stack up once and runs, as clearly-named
steps: health (make verify-up, the DoD smoke) → ACL ↔ OpenZaak (verify-acl) →
OpenZaak → NRC delivery (verify-nrc) → teardown (always) + log dump on failure.

The check logic moves into stack-agnostic runners (run-acl-integration.sh,
run-notification-check.sh) that operate on whatever stack is already up, reaching
services by container IP. The local single-concern wrappers (make integration oz-only,
make verify-notifications oz+nrc) keep working by delegating to the same runners, so
nothing is duplicated. make ci now runs the consolidated 'verify' stage.

Verified locally: make verify boots the full stack once, ACL integration passes and
the NRC notification is delivered, then tears down.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:48:38 +02:00

6.0 KiB

ADR-0006: Provision the ACL integration test against the compose stack

  • Status: Accepted
  • Date: 2026-06-29
  • Deciders: Respellion engineering
  • Relates to: S-04a (#46); proposed in #53; builds on ADR-0001 (loose coupling), ADR-0002 (catalogus design), ADR-0003 (default-fill); supports CLAUDE.md §11 (integration tests via real containers)

Context

S-04 delivered the ACL's one operation — OpenZaakGateway.OpenZaakAsync — with unit tests against a stubbed HttpMessageHandler and a Reqnroll scenario over an in-memory stand-in. The deferred S-04 acceptance criterion (S-04a) is the one a stub cannot meet:

Integration test using Testcontainers against real OpenZaak passes.

The test must drive the gateway against a real OpenZaak — real ZGW JWT auth, the real POST /zaken/api/v1/zaken contract, real CRS handling — and assert a zaak comes back.

Two ways to stand OpenZaak up were considered (the issue's open question): (a) a full Testcontainers graph started by the test, or (b) target the running compose stack the repo already defines (infra/openzaak/docker-compose.yml, make openzaak-up).

Investigation reversed the initially-favoured Testcontainers option:

  1. Testcontainers .NET has no docker-compose support. OpenZaak needs PostGIS + Redis + a setup_configuration one-shot (the JWT client) + the API. Honouring "full graph" would mean re-implementing that five-service stack — init ordering, the config volume, health gating — by hand in C#, duplicating the maintained compose file and rotting with it. That rubs against CLAUDE.md §13 ("if a test is hard to write, the design is wrong").
  2. The test cannot be hermetic anyway. OpenZaak's Zaken API rejects a zaak against a concept zaaktype (not-published), and a published zaaktype requires ≥1 resultaattype, which OpenZaak validates by fetching the external Selectielijst reference API (selectielijst.openzaak.nl). So a real zaak POST already depends on outbound internet from the OpenZaak container — the self-containment that motivated Testcontainers is lost regardless of how the containers are started.

Decision

The ACL integration test targets the running compose stack; it does not start containers itself. No new test dependency is added.

  • A gated test project Acl.IntegrationTests ([Trait("Category","Integration")]) talks to OpenZaak with a plain HttpClient, reusing the same endpoint + JWT-client config the seed uses (OZ_BASE / OZ_CLIENT_ID / OZ_SECRET, defaulting to the local stack). It locates the published BIG-REGISTRATIE zaaktype via the Catalogi API and exercises the real OpenZaakGateway against it.
  • The lane is kept out of the fast checks. make unit runs with --filter "Category!=Integration"; Stryker is pinned to Acl.Tests (test-projects), so neither the unit nor the mutation lane needs a live stack. A make integration target (infra/run-integration.sh) brings up a throwaway OpenZaak and runs the lane locally. In CI the check runs as the verify-acl step of the consolidated verify-stack job (issue #58) — one shared full-stack bring-up. This matches make being the single source of truth (ADR-0005).
  • Publishing is opt-in in the seed. infra/openzaak/seed_catalogus.py gains an OZ_PUBLISH=1 path that adds the relations OpenZaak's publish requires — two statustypen (begin/eind), a roltype, and a resultaattype whose Selectielijst procestype is matched onto the zaaktype — then publishes. The default seed (S-01 / ADR-0002) still leaves the zaaktype a concept; only make integration flips the switch.

Consequences

  • Positive: a small, honest test over the real ZGW contract with no bespoke orchestration to maintain; the compose stack is exercised exactly as operators run it; no new dependency.
  • It caught a real bug. The gateway sent the zaak body via JsonContent without a Content-Length, so .NET framed it as Transfer-Encoding: chunked, which OpenZaak's uwsgi rejects with 400. A stubbed handler accepts either framing, so only a real OpenZaak surfaced it. Fixed by buffering the body (LoadIntoBufferAsync); guarded in the fast lane by a unit test asserting a Content-Length is set. This is the concrete justification for §11's integration tier.
  • External dependency: the integration job needs the OpenZaak container to reach selectielijst.openzaak.nl. It is a stable public reference API (the same one OpenZaak uses in production) but it is a network touchpoint, and a CI environment without egress would need a local Selectielijst service or a recorded fixture. OZ_SELECTIELIJST overrides the base URL.
  • Cost: the lane needs the stack up first, so it is separate from the fast lanes.
  • Runs on the hosted runner. A process on the runner can't reach the stack's published ports (Compose starts sibling containers via the host daemon — gitea-actions-gotchas.md §5, same split as §1), so infra/run-integration.sh runs both the seed and the test as containers joined to the OpenZaak network, reaching it by container IP (a single-label host like openzaak isn't URL-valid for OpenZaak's own URLValidator; an IPv4 literal is). Code is delivered by image build / docker cp, never bind mounts. The CI job therefore needs only Docker — no setup-dotnet. (This closed the follow-up that was originally split out as #55.)

Alternatives considered

  • Full Testcontainers graph — rejected: re-implements the compose stack in C# (brittle, duplicative) for no hermeticity gain, since the Selectielijst dependency remains.
  • Single OpenZaak container (sqlite/locmem) — rejected: diverges from the real PostGIS-backed, Redis-cached deployment; the Zaken API is a geo API and the divergence would undermine the contract the test exists to verify.
  • Mock OpenZaak / record-replay — rejected: that is what the existing stubbed-handler unit tests already do; it cannot exercise the real contract, and would not have caught the chunked body bug.