All checks were successful
CI / lint (pull_request) Successful in 50s
CI / build (pull_request) Successful in 42s
CI / unit (pull_request) Successful in 47s
CI / mutation (pull_request) Successful in 1m31s
CI / integration (pull_request) Successful in 3m43s
CI / compose-smoke (pull_request) Successful in 4m1s
The hosted runner can't reach the stack's published ports (sibling containers), so run 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, but an IPv4 literal is. Code is delivered via image build / docker cp (bind mounts don't reach the daemon either). - infra/run-integration.sh: up -> wait healthy (docker inspect) -> seed published zaaktype (python container on the net) -> build + run the test image on the net -> always tear down. Plain docker primitives only (portable docker/podman). - services/acl/Dockerfile.integration: builds + runs Acl.IntegrationTests; dotnet lives in the image, so the CI job needs only Docker (no setup-dotnet). - make integration now delegates to the script; re-added the Gitea Actions job. Supersedes the local-only gap documented earlier; #55 is no longer needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
5.9 KiB
Markdown
92 lines
5.9 KiB
Markdown
# 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
|
|
(`infra/run-integration.sh`) brings the stack up, seeds, runs the lane, and always tears down
|
|
— mirrored by a Gitea Actions `integration` job. 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.
|