test(acl): ACL integration test against real OpenZaak (closes #46) #54

Merged
not merged 7 commits from test/46-acl-openzaak-integration into main 2026-06-29 10:48:01 +00:00
7 changed files with 138 additions and 35 deletions
Showing only changes of commit 4474585606 - Show all commits

View File

@@ -63,11 +63,22 @@ jobs:
path: services/acl/StrykerOutput/**/reports/mutation-report.html path: services/acl/StrykerOutput/**/reports/mutation-report.html
if-no-files-found: warn if-no-files-found: warn
# NOTE: there is deliberately no `integration` job here yet. `make integration` integration:
# runs the ACL ↔ real-OpenZaak test locally / on a host-executing runner, but on runs-on: ubuntu-latest
# the hosted runner a process on the runner cannot reach the stack's published steps:
# ports (sibling containers — see gitea-actions-gotchas.md §5). Running it inside - uses: https://github.com/actions/checkout@v4
# the compose network is tracked in #55. # No setup-dotnet: `make integration` runs the seed and the test as containers
# *inside* the OpenZaak compose network (reaching it by container IP), so dotnet
# lives in the test image and the runner needs only Docker. This sidesteps the
# runner being unable to reach published ports (gitea-actions-gotchas.md §5).
# Needs egress to pull base images + nuget + selectielijst.openzaak.nl.
- run: make integration
- name: dump OpenZaak logs on failure
if: failure()
run: docker compose -f infra/openzaak/docker-compose.yml logs --no-color --tail=80 oz-init openzaak 2>&1 || true
- name: tear down on failure
if: failure()
run: docker compose -f infra/openzaak/docker-compose.yml down --volumes 2>&1 || true
compose-smoke: compose-smoke:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -106,20 +106,14 @@ local-down:
changelog: changelog:
git-cliff --output CHANGELOG.md git-cliff --output CHANGELOG.md
## integration: ACL integration tests against a real OpenZaak (S-04a, #46). Brings ## integration: ACL integration tests against a real OpenZaak (S-04a, #46). The
## the stack up, seeds a PUBLISHED BIG zaaktype (OZ_PUBLISH=1, so OpenZaak accepts a ## seed and the test run inside the compose network (reaching http://openzaak:8000),
## real zaak POST), runs the Integration-category tests, then always tears down. ## so this works on the hosted CI runner where a runner process can't reach the
## Kept out of `unit`/`mutation` because it needs the live stack. See ADR-0006. ## stack's published ports. Brings the stack up, seeds a PUBLISHED BIG zaaktype,
integration: openzaak-up ## runs the Integration-category tests, then always tears down. Kept out of
@bash -c 'set -e; \ ## `unit`/`mutation` because it needs the live stack. See infra/run-integration.sh + ADR-0006.
for i in $$(seq 1 60); do \ integration:
c=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/catalogi/api/v1/ || true); \ bash infra/run-integration.sh
[ "$$c" = "200" ] && break; sleep 3; done; echo "OpenZaak ready ($$c)"; \
OZ_PUBLISH=1 python3 infra/openzaak/seed_catalogus.py; \
rc=0; dotnet test $(SLN) -c Release --filter "Category=Integration" || rc=$$?; \
docker compose -f $(OZ_COMPOSE) down --volumes >/dev/null 2>&1 || true; \
docker volume rm -f rr-oz-config >/dev/null 2>&1 || true; \
exit $$rc'
## openzaak-up: start the OpenZaak stack (migrations run on first start) ## openzaak-up: start the OpenZaak stack (migrations run on first start)
openzaak-up: openzaak-up:

View File

@@ -47,8 +47,9 @@ itself. No new test dependency is added.**
- **The lane is kept out of the fast checks.** `make unit` runs with - **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 `--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 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 (`infra/run-integration.sh`) brings the stack up, seeds, runs the lane, and always tears down
the single source of truth (ADR-0005). — 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 - **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 `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 (begin/eind), a roltype, and a resultaattype whose Selectielijst procestype is matched onto
@@ -70,12 +71,13 @@ itself. No new test dependency is added.**
in production) but it is a network touchpoint, and a CI environment without egress would need 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. 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. - **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 - **Runs on the hosted runner.** A process *on* the runner can't reach the stack's published
runner, but on Gitea's hosted runner a process *on the runner* cannot reach the stack's ports (Compose starts sibling containers via the host daemon — gitea-actions-gotchas.md §5,
published ports — Compose starts sibling containers via the host daemon, so the ports bind to same split as §1), so `infra/run-integration.sh` runs both the seed and the test as containers
the host, not the runner (gitea-actions-gotchas.md §5, the same split as §1). Running the test *joined to the OpenZaak network*, reaching it by **container IP** (a single-label host like
and seed *inside* the compose network (via a built image, not bind mounts) to gate it in CI is `openzaak` isn't URL-valid for OpenZaak's own `URLValidator`; an IPv4 literal is). Code is
tracked in **#55**. Until then the lane runs locally and is not a merge gate. 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 ## Alternatives considered

View File

@@ -17,13 +17,13 @@ and CI cannot drift:
| `build` | `make build``dotnet build … -c Release` | .NET 10 SDK | | `build` | `make build``dotnet build … -c Release` | .NET 10 SDK |
| `unit` | `make unit``dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK | | `unit` | `make unit``dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK |
| `mutation` | `make mutation``dotnet tool restore``dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK | | `mutation` | `make mutation``dotnet tool restore``dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
| `integration` | `make integration``infra/run-integration.sh`: OpenZaak up → seed a **published** BIG zaaktype + run `Acl.IntegrationTests` **as containers inside the compose network** → tear down | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
| `compose-smoke` | `make smoke` → seed config volumes → `up -d` (full stack) → `up --wait` durable services → `down` | container engine + compose v2 | | `compose-smoke` | `make smoke` → seed config volumes → `up -d` (full stack) → `up --wait` durable services → `down` | container engine + compose v2 |
> **`make integration` is not a hosted-runner job yet.** The ACL ↔ real-OpenZaak > **The `integration` job needs no `setup-dotnet`.** dotnet runs inside the test
> test (`make integration`, ADR-0006) passes locally and on a host-executing runner, > image, and both the seed and the test join the OpenZaak network and reach it by
> but a process on the hosted runner can't reach the stack's published ports > container IP — so the runner never has to reach a published port
> (sibling containers — see [gitea-actions-gotchas.md §5](gitea-actions-gotchas.md)). > (see [gitea-actions-gotchas.md §5](gitea-actions-gotchas.md)).
> Run it inside the compose network to gate it in CI — tracked in **#55**.
All `uses:` references are absolute, tag-pinned URLs (`https://github.com/actions/checkout@v4`, All `uses:` references are absolute, tag-pinned URLs (`https://github.com/actions/checkout@v4`,
`https://github.com/actions/setup-dotnet@v4`) per CLAUDE.md §8.7 and §15 — Gitea `https://github.com/actions/setup-dotnet@v4`) per CLAUDE.md §8.7 and §15 — Gitea

View File

@@ -152,6 +152,8 @@ service by name (`http://openzaak:8000`). For a test/seed that needs the repo's
code, deliver it via a **built image** (not a bind mount — §1), then code, deliver it via a **built image** (not a bind mount — §1), then
`docker run --network <stack>_cg …`. `docker run --network <stack>_cg …`.
**Status**`make integration` (the ACL ↔ real-OpenZaak test, ADR-0006) therefore **Applied**`make integration` (the ACL ↔ real-OpenZaak test, ADR-0006) does
runs locally / on a host-executing runner only. Gating it on the hosted runner via exactly this: `infra/run-integration.sh` runs the seed and the test as containers on
the compose network is tracked in **#55**. the OpenZaak network and reaches it by **container IP** (a single-label service name
like `openzaak` isn't URL-valid — OpenZaak echoes the request host into the URLs it
returns and then rejects them with Django's `URLValidator`; an IPv4 literal passes).

68
infra/run-integration.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
#
# Run the ACL ↔ real-OpenZaak integration test (S-04a / #46) end to end.
#
# Everything that talks to OpenZaak runs *inside* the compose network and reaches
# it by service name (http://openzaak:8000) — the hosted CI runner can't reach the
# stack's published ports (sibling containers) and bind mounts don't reach the
# daemon either (gitea-actions-gotchas.md §1/§5). So we use only plain docker
# primitives (run / create / cp / build) — portable across docker compose (CI) and
# podman-compose (local), exactly like infra/seed-config.sh. See ADR-0006.
#
# Steps: bring OpenZaak up → wait for it healthy → seed a PUBLISHED BIG zaaktype
# (a seed container on the network) → build + run the test container on the network
# → always tear down.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$here/.." && pwd)"
OZ_COMPOSE="$here/openzaak/docker-compose.yml"
cleanup() {
docker compose -f "$OZ_COMPOSE" down --volumes >/dev/null 2>&1 || true
docker volume rm -f rr-oz-config >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo ">> bringing OpenZaak up"
bash "$here/seed-config.sh" oz
docker compose -f "$OZ_COMPOSE" up -d
echo ">> waiting for the OpenZaak API container to be healthy"
# Match the API container under both docker compose (openzaak-openzaak-1) and
# podman-compose (openzaak_openzaak_1) naming; the regex excludes oz-db / oz-redis.
api=""
for _ in $(seq 1 140); do
api="$(docker ps -q --filter 'name=openzaak[-_]openzaak' | head -1)"
if [ -n "$api" ]; then
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$api" 2>/dev/null || true)"
[ "$status" = "healthy" ] && break
fi
sleep 3
done
[ -n "$api" ] || { echo "ERROR: OpenZaak API container never appeared" >&2; exit 1; }
[ "${status:-}" = "healthy" ] || { echo "ERROR: OpenZaak not healthy (status=${status:-none})" >&2; exit 1; }
# The network the API container is attached to — joined by the seed + test below.
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$api" | head -1)"
# Reach OpenZaak by container IP, not by the service name. OpenZaak echoes its
# request Host into the self-referential URLs it returns, then validates those URLs
# with Django's URLValidator — which rejects a single-label host like `openzaak`
# ("Voer een geldige URL in") while accepting an IPv4 literal (and `localhost`).
oz_ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$api")"
oz_base="http://${oz_ip}:8000"
echo ">> OpenZaak healthy on network $net at $oz_base"
echo ">> seeding a published BIG zaaktype (OZ_PUBLISH=1, inside the network)"
sid="$(docker create --network "$net" \
-e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 \
python:3-slim python /seed_catalogus.py)"
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed_catalogus.py"
docker start -a "$sid"
docker rm -f "$sid" >/dev/null
echo ">> building the integration test image"
docker build -f "$root/services/acl/Dockerfile.integration" -t rr-acl-integration "$root/services/acl"
echo ">> running the integration tests (inside the network)"
docker run --rm --network "$net" -e "OZ_BASE=$oz_base" rr-acl-integration

View File

@@ -0,0 +1,26 @@
# Runs the ACL integration tests (Category=Integration) from *inside* the compose
# network, so they reach OpenZaak at http://openzaak:8000 by service name. On the
# hosted CI runner a process on the runner can't reach the stack's published ports
# (sibling containers — gitea-actions-gotchas.md §5), so the test runs as a
# container joined to that network instead. See ADR-0006 / #55.
#
# Build context is services/acl (like the service Dockerfile). dotnet lives in this
# image, so the CI `integration` job needs only Docker — no setup-dotnet step.
FROM mcr.microsoft.com/dotnet/sdk:10.0
WORKDIR /src
# Restore first (cached unless the .csproj files change). The integration test
# project pulls in Acl.Application + Acl.Infrastructure via its ProjectReferences.
COPY Acl.Application/Acl.Application.csproj Acl.Application/
COPY Acl.Infrastructure/Acl.Infrastructure.csproj Acl.Infrastructure/
COPY Acl.IntegrationTests/Acl.IntegrationTests.csproj Acl.IntegrationTests/
RUN dotnet restore Acl.IntegrationTests/Acl.IntegrationTests.csproj
COPY Acl.Application/ Acl.Application/
COPY Acl.Infrastructure/ Acl.Infrastructure/
COPY Acl.IntegrationTests/ Acl.IntegrationTests/
# OZ_BASE is supplied at run time (the OpenZaak container IP — see run-integration.sh,
# which passes `-e OZ_BASE=http://<ip>:8000`; a single-label host is not URL-valid).
ENTRYPOINT ["dotnet", "test", "Acl.IntegrationTests/Acl.IntegrationTests.csproj", \
"-c", "Release", "--filter", "Category=Integration"]