From 09de500fb81c77e38f69f9e584913298cd032077 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Mon, 29 Jun 2026 11:43:38 +0200 Subject: [PATCH] =?UTF-8?q?arch(acl):=20ADR-0006=20=E2=80=94=20provision?= =?UTF-8?q?=20the=20ACL=20integration=20test=20against=20the=20compose=20s?= =?UTF-8?q?tack=20(refs=20#46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records why the integration test targets the running compose stack rather than a Testcontainers graph (no .NET compose support; not hermetic anyway due to the Selectielijst dependency), the opt-in publish seed, and the chunked-body bug the test caught. Proposed in #53. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../adr-0006-integration-test-provisioning.md | 84 +++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 85 insertions(+) create mode 100644 docs/architecture/adr-0006-integration-test-provisioning.md diff --git a/docs/architecture/adr-0006-integration-test-provisioning.md b/docs/architecture/adr-0006-integration-test-provisioning.md new file mode 100644 index 0000000..a10da6d --- /dev/null +++ b/docs/architecture/adr-0006-integration-test-provisioning.md @@ -0,0 +1,84 @@ +# 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 — 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; CI runs it as a dedicated job (Docker + dotnet + + python3), separate from the fast lanes. + +## 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. diff --git a/mkdocs.yml b/mkdocs.yml index 33f4935..8173c11 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,6 +27,7 @@ nav: - "ADR-0003: ACL default-fill": architecture/adr-0003-default-fill.md - "ADR-0004: BDD framework": architecture/adr-0004-bdd-framework.md - "ADR-0005: Mutation testing": architecture/adr-0005-mutation-testing.md + - "ADR-0006: ACL integration test provisioning": architecture/adr-0006-integration-test-provisioning.md - Working in Gitea: gitea-workflow.md - Runbooks: - CI: runbooks/ci.md