# 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 new `make integration` target brings the stack up, seeds, runs the lane, and always tears down. 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. - **Not yet a hosted-runner job.** `make integration` passes 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.