The hosted Gitea runner starts the OpenZaak stack as sibling containers via the host daemon, so a process on the runner can't reach the published ports — the seed and dotnet test get Connection refused on localhost:8000. Drop the (non-working) integration CI job; make integration stays the local / host-runner gate. Document the limitation in gitea-actions-gotchas.md §5 and the CI runbook, and track running it inside the compose network in #55. ADR-0006 updated accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.8 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:
- Testcontainers .NET has no docker-compose support. OpenZaak needs PostGIS + Redis +
a
setup_configurationone-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"). - 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 plainHttpClient, 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 publishedBIG-REGISTRATIEzaaktype via the Catalogi API and exercises the realOpenZaakGatewayagainst it. - The lane is kept out of the fast checks.
make unitruns with--filter "Category!=Integration"; Stryker is pinned toAcl.Tests(test-projects), so neither the unit nor the mutation lane needs a live stack. A newmake integrationtarget brings the stack up, seeds, runs the lane, and always tears down. This matchesmakebeing the single source of truth (ADR-0005). - Publishing is opt-in in the seed.
infra/openzaak/seed_catalogus.pygains anOZ_PUBLISH=1path 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; onlymake integrationflips 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
JsonContentwithout aContent-Length, so .NET framed it asTransfer-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 aContent-Lengthis 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_SELECTIELIJSToverrides the base URL. - Cost: the lane needs the stack up first, so it is separate from the fast lanes.
- Not yet a hosted-runner job.
make integrationpasses locally and on a host-executing runner, but on Gitea's hosted runner a process on the runner cannot reach the stack's published ports — Compose starts sibling containers via the host daemon, so the ports bind to the host, not the runner (gitea-actions-gotchas.md §5, the same split as §1). Running the test and seed inside the compose network (via a built image, not bind mounts) to gate it in CI is tracked in #55. Until then the lane runs locally and is not a merge gate.
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.