Compare commits
6 Commits
feat/47-ac
...
855a5565fe
| Author | SHA1 | Date | |
|---|---|---|---|
| 855a5565fe | |||
| 09de500fb8 | |||
| 4322c607cb | |||
| d0582cef65 | |||
| f2e575b427 | |||
| fd5fa5ac3c |
@@ -63,6 +63,25 @@ 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
|
||||||
|
|
||||||
|
integration:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
- uses: https://github.com/actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
# `make integration` brings the OpenZaak stack up, seeds a PUBLISHED BIG
|
||||||
|
# zaaktype (OZ_PUBLISH=1) and runs the Integration-category tests, then tears
|
||||||
|
# down. Needs Docker + python3 (both on ubuntu-latest) and outbound access to
|
||||||
|
# selectielijst.openzaak.nl from the OpenZaak container (see ADR-0006).
|
||||||
|
- 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
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
21
Makefile
21
Makefile
@@ -43,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: ci lint build unit mutation smoke up down local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help
|
.PHONY: ci lint build unit mutation integration smoke up down local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help
|
||||||
|
|
||||||
## ci: run the full pipeline — lint, build, unit, mutation, smoke (mirrors Gitea Actions)
|
## ci: run the full pipeline — lint, build, unit, mutation, smoke (mirrors Gitea Actions)
|
||||||
ci: lint build unit mutation smoke
|
ci: lint build unit mutation smoke
|
||||||
@@ -56,9 +56,9 @@ lint:
|
|||||||
build:
|
build:
|
||||||
dotnet build $(SLN) -c Release
|
dotnet build $(SLN) -c Release
|
||||||
|
|
||||||
## unit: run unit tests
|
## unit: run unit tests (excludes the container-backed Integration lane)
|
||||||
unit:
|
unit:
|
||||||
dotnet test $(SLN) -c Release
|
dotnet test $(SLN) -c Release --filter "Category!=Integration"
|
||||||
|
|
||||||
## mutation: run the Stryker.NET ratchet on the ACL (fails below the recorded baseline)
|
## mutation: run the Stryker.NET ratchet on the ACL (fails below the recorded baseline)
|
||||||
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
|
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
|
||||||
@@ -106,6 +106,21 @@ 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
|
||||||
|
## the stack up, seeds a PUBLISHED BIG zaaktype (OZ_PUBLISH=1, so OpenZaak accepts a
|
||||||
|
## real zaak POST), runs the Integration-category tests, then always tears down.
|
||||||
|
## Kept out of `unit`/`mutation` because it needs the live stack. See ADR-0006.
|
||||||
|
integration: openzaak-up
|
||||||
|
@bash -c 'set -e; \
|
||||||
|
for i in $$(seq 1 60); do \
|
||||||
|
c=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/catalogi/api/v1/ || true); \
|
||||||
|
[ "$$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:
|
||||||
$(SEED) oz
|
$(SEED) oz
|
||||||
|
|||||||
84
docs/architecture/adr-0006-integration-test-provisioning.md
Normal file
84
docs/architecture/adr-0006-integration-test-provisioning.md
Normal file
@@ -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.
|
||||||
@@ -15,8 +15,9 @@ and CI cannot drift:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `lint` | `make lint` → `dotnet format … --verify-no-changes` | .NET 10 SDK |
|
| `lint` | `make lint` → `dotnet format … --verify-no-changes` | .NET 10 SDK |
|
||||||
| `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` | .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` → `openzaak-up` → seed a **published** BIG zaaktype (`OZ_PUBLISH=1`) → `dotnet test … --filter "Category=Integration"` → tear down | .NET 10 SDK + container engine + python3 + egress to `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 |
|
||||||
|
|
||||||
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`,
|
||||||
@@ -88,12 +89,17 @@ the containerized CI runner). Keep the two files in sync.
|
|||||||
Until the runner exists, run the full pipeline yourself before pushing:
|
Until the runner exists, run the full pipeline yourself before pushing:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make ci # lint + build + unit + mutation + smoke — what the pipeline runs
|
make ci # lint + build + unit + mutation + smoke — the fast pipeline lanes
|
||||||
make lint # or a single stage
|
make lint # or a single stage
|
||||||
make mutation # Stryker.NET ratchet on the ACL
|
make mutation # Stryker.NET ratchet on the ACL
|
||||||
make smoke # compose up --wait, curl /health, tear down
|
make smoke # compose up --wait, curl /health, tear down
|
||||||
|
make integration # ACL ↔ real OpenZaak (its own CI job; not part of `make ci`)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> `make integration` is a separate, heavier lane (it stands the OpenZaak stack up and
|
||||||
|
> seeds a published zaaktype), so it is **not** folded into `make ci`. Run it before
|
||||||
|
> pushing changes that touch the ACL gateway or the OpenZaak seed. See ADR-0006.
|
||||||
|
|
||||||
**Prerequisites:** .NET 10 SDK, a container engine with Compose v2, and `curl`.
|
**Prerequisites:** .NET 10 SDK, a container engine with Compose v2, and `curl`.
|
||||||
|
|
||||||
On a **rootless Podman** box (the default dev setup here), the `smoke` target needs
|
On a **rootless Podman** box (the default dev setup here), the `smoke` target needs
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ SECRET = os.environ.get("OZ_SECRET", "insecure-dev-secret-change-me")
|
|||||||
ZTC = f"{BASE}/catalogi/api/v1"
|
ZTC = f"{BASE}/catalogi/api/v1"
|
||||||
RSIN = "517439943" # elfproef-valid test RSIN
|
RSIN = "517439943" # elfproef-valid test RSIN
|
||||||
|
|
||||||
|
# Opt-in: also publish the zaaktype so OpenZaak's Zaken API accepts a zaak against
|
||||||
|
# it (a concept zaaktype is rejected with `not-published`). Off by default — the
|
||||||
|
# S-01 compose seed keeps it a concept (ADR-0002). The ACL integration test
|
||||||
|
# (S-04a, #46) sets OZ_PUBLISH=1. Publishing requires ≥2 statustypen, ≥1 roltype
|
||||||
|
# and ≥1 resultaattype; the resultaattype is validated against the external
|
||||||
|
# Selectielijst reference API, so this path needs outbound access to it. See ADR-0006.
|
||||||
|
PUBLISH = os.environ.get("OZ_PUBLISH", "").lower() in ("1", "true", "yes")
|
||||||
|
SELECTIELIJST = os.environ.get(
|
||||||
|
"OZ_SELECTIELIJST", "https://selectielijst.openzaak.nl/api/v1").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
def token():
|
def token():
|
||||||
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
|
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
|
||||||
@@ -52,6 +62,68 @@ def find(path):
|
|||||||
return body.get("results", [])
|
return body.get("results", [])
|
||||||
|
|
||||||
|
|
||||||
|
def selectielijst(path):
|
||||||
|
"""GET the external Selectielijst reference API (no auth). Used only when publishing."""
|
||||||
|
req = urllib.request.Request(f"{SELECTIELIJST}{path}", headers={"Accept": "application/json"})
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return json.loads(r.read())
|
||||||
|
|
||||||
|
|
||||||
|
def publish_zaaktype(zt):
|
||||||
|
"""Add the relations OpenZaak requires to publish, then publish (idempotent).
|
||||||
|
|
||||||
|
Publish validation (verified against OpenZaak 1.28.2) demands: ≥2 statustypen
|
||||||
|
(begin + eind), ≥1 roltype, ≥1 resultaattype. A resultaattype needs a
|
||||||
|
Selectielijst `selectielijstklasse` whose procestype matches the zaaktype's
|
||||||
|
`selectielijstProcestype`, plus a `resultaattypeomschrijving`.
|
||||||
|
"""
|
||||||
|
have_st = {s.get("volgnummer") for s in find(f"/statustypen?zaaktype={zt['url']}&status=alles")}
|
||||||
|
for volgnummer, omschrijving in [(1, "Ontvangen"), (2, "Afgehandeld")]:
|
||||||
|
if volgnummer not in have_st:
|
||||||
|
st, body = api("POST", "/statustypen", {
|
||||||
|
"omschrijving": omschrijving, "zaaktype": zt["url"], "volgnummer": volgnummer})
|
||||||
|
if st != 201:
|
||||||
|
sys.exit(f"create statustype {volgnummer} -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
print(f"create statustype {volgnummer} ({omschrijving})")
|
||||||
|
|
||||||
|
if find(f"/roltypen?zaaktype={zt['url']}&status=alles"):
|
||||||
|
print("skip roltype Aanvrager")
|
||||||
|
else:
|
||||||
|
st, body = api("POST", "/roltypen", {
|
||||||
|
"zaaktype": zt["url"], "omschrijving": "Aanvrager", "omschrijvingGeneriek": "initiator"})
|
||||||
|
if st != 201:
|
||||||
|
sys.exit(f"create roltype -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
print("create roltype Aanvrager")
|
||||||
|
|
||||||
|
if find(f"/resultaattypen?zaaktype={zt['url']}&status=alles"):
|
||||||
|
print("skip resultaattype Geregistreerd")
|
||||||
|
else:
|
||||||
|
resultaat = selectielijst("/resultaten?pageSize=1")["results"][0]
|
||||||
|
omschrijvingen = selectielijst("/resultaattypeomschrijvingen")
|
||||||
|
oms = (omschrijvingen if isinstance(omschrijvingen, list) else omschrijvingen["results"])[0]["url"]
|
||||||
|
# The selectielijstklasse and the zaaktype must share a procestype.
|
||||||
|
st, body = api("PATCH", zt["url"], {"selectielijstProcestype": resultaat["procesType"]})
|
||||||
|
if st != 200:
|
||||||
|
sys.exit(f"set procestype -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
st, body = api("POST", "/resultaattypen", {
|
||||||
|
"zaaktype": zt["url"], "omschrijving": "Geregistreerd",
|
||||||
|
"resultaattypeomschrijving": oms, "selectielijstklasse": resultaat["url"],
|
||||||
|
"archiefnominatie": "blijvend_bewaren",
|
||||||
|
"brondatumArchiefprocedure": {"afleidingswijze": "afgehandeld"},
|
||||||
|
})
|
||||||
|
if st != 201:
|
||||||
|
sys.exit(f"create resultaattype -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
print("create resultaattype Geregistreerd")
|
||||||
|
|
||||||
|
if zt.get("concept", True):
|
||||||
|
st, body = api("POST", f"{zt['url']}/publish")
|
||||||
|
if st != 200:
|
||||||
|
sys.exit(f"publish zaaktype -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
print(f"publish zaaktype BIG-REGISTRATIE ({zt['url']})")
|
||||||
|
else:
|
||||||
|
print("skip publish (already published)")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# 1. Catalogus
|
# 1. Catalogus
|
||||||
existing = [c for c in find(f"/catalogussen?domein=BIG") if c.get("domein") == "BIG"]
|
existing = [c for c in find(f"/catalogussen?domein=BIG") if c.get("domein") == "BIG"]
|
||||||
@@ -121,16 +193,24 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print("warn zaaktype already published; cannot add bsn eigenschap")
|
print("warn zaaktype already published; cannot add bsn eigenschap")
|
||||||
|
|
||||||
# Intentionally NOT published. Publishing requires roltypen, resultaattypen
|
# 4. Optionally publish. By default the zaaktype stays a concept: publishing
|
||||||
# and statustypen, which go beyond the "lean / schema-mandatory" zaaktype this
|
# requires roltypen, resultaattypen and statustypen, beyond the "lean /
|
||||||
# slice asks for; they arrive with the workflow/zaak slices. See ADR-0002.
|
# schema-mandatory" zaaktype S-01 asks for (ADR-0002). Set OZ_PUBLISH=1 to add
|
||||||
|
# those relations and publish — needed so a real zaak POST is accepted, which
|
||||||
|
# the ACL integration test (S-04a, #46) exercises. See ADR-0006.
|
||||||
|
if PUBLISH:
|
||||||
|
# Re-fetch: the bsn-eigenschap branch above may hold a stale concept flag.
|
||||||
|
zt = next(z for z in find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
|
||||||
|
if z.get("identificatie") == "BIG-REGISTRATIE")
|
||||||
|
publish_zaaktype(zt)
|
||||||
|
|
||||||
# 4. Verify the JWT client can list the zaaktype (concepts included).
|
# 5. Verify the JWT client can list the zaaktype (concepts included).
|
||||||
zaaktypen = find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
|
zaaktypen = find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
|
||||||
names = [z.get("identificatie") for z in zaaktypen]
|
names = [z.get("identificatie") for z in zaaktypen]
|
||||||
print(f"zaaktypen in BIG: {names}")
|
print(f"zaaktypen in BIG: {names}")
|
||||||
assert "BIG-REGISTRATIE" in names, "BIG-REGISTRATIE not listed"
|
assert "BIG-REGISTRATIE" in names, "BIG-REGISTRATIE not listed"
|
||||||
print("OK — BIG catalogus seeded (BIG-REGISTRATIE concept + bsn eigenschap)")
|
state = "published" if PUBLISH else "concept"
|
||||||
|
print(f"OK — BIG catalogus seeded (BIG-REGISTRATIE {state} + bsn eigenschap)")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ nav:
|
|||||||
- "ADR-0003: ACL default-fill": architecture/adr-0003-default-fill.md
|
- "ADR-0003: ACL default-fill": architecture/adr-0003-default-fill.md
|
||||||
- "ADR-0004: BDD framework": architecture/adr-0004-bdd-framework.md
|
- "ADR-0004: BDD framework": architecture/adr-0004-bdd-framework.md
|
||||||
- "ADR-0005: Mutation testing": architecture/adr-0005-mutation-testing.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
|
- Working in Gitea: gitea-workflow.md
|
||||||
- Runbooks:
|
- Runbooks:
|
||||||
- CI: runbooks/ci.md
|
- CI: runbooks/ci.md
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<Project Path="services/acl/Acl.Api/Acl.Api.csproj" />
|
<Project Path="services/acl/Acl.Api/Acl.Api.csproj" />
|
||||||
<Project Path="services/acl/Acl.Application/Acl.Application.csproj" />
|
<Project Path="services/acl/Acl.Application/Acl.Application.csproj" />
|
||||||
<Project Path="services/acl/Acl.Infrastructure/Acl.Infrastructure.csproj" />
|
<Project Path="services/acl/Acl.Infrastructure/Acl.Infrastructure.csproj" />
|
||||||
|
<Project Path="services/acl/Acl.IntegrationTests/Acl.IntegrationTests.csproj" />
|
||||||
<Project Path="services/acl/Acl.Tests/Acl.Tests.csproj" />
|
<Project Path="services/acl/Acl.Tests/Acl.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/services/bff/">
|
<Folder Name="/services/bff/">
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
|
|||||||
// ZRC is a geo API; it requires the CRS headers.
|
// ZRC is a geo API; it requires the CRS headers.
|
||||||
message.Headers.Add("Accept-Crs", "EPSG:4326");
|
message.Headers.Add("Accept-Crs", "EPSG:4326");
|
||||||
message.Content.Headers.Add("Content-Crs", "EPSG:4326");
|
message.Content.Headers.Add("Content-Crs", "EPSG:4326");
|
||||||
|
// OpenZaak runs behind uwsgi, which rejects a chunked request body with 400.
|
||||||
|
// JsonContent streams without a known length (→ Transfer-Encoding: chunked),
|
||||||
|
// so buffer it first to send a Content-Length instead. Only a real OpenZaak
|
||||||
|
// surfaces this — a stubbed HttpMessageHandler accepts either framing.
|
||||||
|
await message.Content.LoadIntoBufferAsync(ct);
|
||||||
|
|
||||||
using var response = await http.SendAsync(message, ct);
|
using var response = await http.SendAsync(message, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<!-- Integration tests: they talk to a real OpenZaak (the compose stack), so they
|
||||||
|
are gated behind [Trait("Category","Integration")] and excluded from the fast
|
||||||
|
`make unit` / mutation lanes. `make integration` brings the stack up, seeds a
|
||||||
|
published BIG zaaktype (OZ_PUBLISH=1) and runs this project. See ADR-0006. -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Acl.Application\Acl.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\Acl.Infrastructure\Acl.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
97
services/acl/Acl.IntegrationTests/OpenZaakFixture.cs
Normal file
97
services/acl/Acl.IntegrationTests/OpenZaakFixture.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Acl.Infrastructure;
|
||||||
|
|
||||||
|
namespace Acl.IntegrationTests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared connection to the running OpenZaak compose stack (ADR-0006). Reads the
|
||||||
|
/// same endpoint + JWT-client config the seed uses, and locates the published
|
||||||
|
/// BIG-REGISTRATIE zaaktype the ACL opens zaken against. Defaults match
|
||||||
|
/// `infra/openzaak/seed_catalogus.py`; override via OZ_BASE / OZ_CLIENT_ID / OZ_SECRET.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OpenZaakFixture : IDisposable
|
||||||
|
{
|
||||||
|
private static string Env(string key, string fallback) =>
|
||||||
|
Environment.GetEnvironmentVariable(key) is { Length: > 0 } v ? v : fallback;
|
||||||
|
|
||||||
|
public Uri BaseUrl { get; } = new(Env("OZ_BASE", "http://localhost:8000"));
|
||||||
|
public string ClientId { get; } = Env("OZ_CLIENT_ID", "big-reference-seed");
|
||||||
|
public string Secret { get; } = Env("OZ_SECRET", "insecure-dev-secret-change-me");
|
||||||
|
|
||||||
|
public HttpClient Http { get; } = new();
|
||||||
|
|
||||||
|
public OpenZaakOptions Options => new()
|
||||||
|
{
|
||||||
|
BaseUrl = BaseUrl,
|
||||||
|
ClientId = ClientId,
|
||||||
|
Secret = Secret,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URL of the published BIG-REGISTRATIE zaaktype, or null when none is
|
||||||
|
/// published yet (a concept-only stack). `status=definitief` returns published
|
||||||
|
/// zaaktypen only — a concept zaaktype is deliberately excluded.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Uri?> FindPublishedBigZaaktypeAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = new Uri(BaseUrl,
|
||||||
|
"/catalogi/api/v1/zaaktypen?identificatie=BIG-REGISTRATIE&status=definitief");
|
||||||
|
using var message = new HttpRequestMessage(HttpMethod.Get, query);
|
||||||
|
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", MintToken());
|
||||||
|
|
||||||
|
using var response = await Http.SendAsync(message, ct);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync(ct));
|
||||||
|
var results = document.RootElement.GetProperty("results");
|
||||||
|
return results.GetArrayLength() == 0
|
||||||
|
? null
|
||||||
|
: new Uri(results[0].GetProperty("url").GetString()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>GETs a previously-created zaak to prove it was really persisted.</summary>
|
||||||
|
public async Task<JsonElement> GetZaakAsync(Uri zaakUrl, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var message = new HttpRequestMessage(HttpMethod.Get, zaakUrl);
|
||||||
|
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", MintToken());
|
||||||
|
message.Headers.Add("Accept-Crs", "EPSG:4326");
|
||||||
|
|
||||||
|
using var response = await Http.SendAsync(message, ct);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var json = await response.Content.ReadAsStringAsync(ct);
|
||||||
|
return JsonDocument.Parse(json).RootElement.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ZGW (vng-api-common) HS256 JWT, mirroring the seed's client. Minted here
|
||||||
|
// rather than reusing Acl.Infrastructure's internal minter to keep that internal.
|
||||||
|
private string MintToken()
|
||||||
|
{
|
||||||
|
static string B64(byte[] b) =>
|
||||||
|
Convert.ToBase64String(b).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||||
|
|
||||||
|
var header = B64(JsonSerializer.SerializeToUtf8Bytes(new { alg = "HS256", typ = "JWT" }));
|
||||||
|
var payload = B64(JsonSerializer.SerializeToUtf8Bytes(new
|
||||||
|
{
|
||||||
|
iss = ClientId,
|
||||||
|
iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
|
client_id = ClientId,
|
||||||
|
user_id = "acl-integration-test",
|
||||||
|
user_representation = "acl-integration-test",
|
||||||
|
}));
|
||||||
|
var signingInput = $"{header}.{payload}";
|
||||||
|
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Secret));
|
||||||
|
var signature = B64(hmac.ComputeHash(Encoding.UTF8.GetBytes(signingInput)));
|
||||||
|
return $"{signingInput}.{signature}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Http.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[CollectionDefinition(Name)]
|
||||||
|
public sealed class OpenZaakCollection : ICollectionFixture<OpenZaakFixture>
|
||||||
|
{
|
||||||
|
public const string Name = "OpenZaak";
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using Acl.Application;
|
||||||
|
using Acl.Infrastructure;
|
||||||
|
|
||||||
|
namespace Acl.IntegrationTests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S-04a (#46): the deferred S-04 acceptance criterion — the ACL's OpenZaakGateway
|
||||||
|
/// opening a zaak against a *real* OpenZaak, exercising real ZGW JWT auth and the
|
||||||
|
/// real POST /zaken/api/v1/zaken contract (CRS headers, default-fill, the created
|
||||||
|
/// zaak URL) that the stubbed-HttpMessageHandler unit tests cannot. See ADR-0006.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Integration")]
|
||||||
|
[Collection(OpenZaakCollection.Name)]
|
||||||
|
public sealed class OpenZaakGatewayIntegrationTests(OpenZaakFixture stack)
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Opens_a_real_zaak_against_the_published_big_zaaktype_and_returns_its_url()
|
||||||
|
{
|
||||||
|
var zaaktype = await stack.FindPublishedBigZaaktypeAsync();
|
||||||
|
Assert.True(zaaktype is not null,
|
||||||
|
"No published BIG-REGISTRATIE zaaktype found in OpenZaak — bring the stack up and " +
|
||||||
|
"seed it with OZ_PUBLISH=1 (`make integration` does this).");
|
||||||
|
|
||||||
|
var gateway = new OpenZaakGateway(stack.Http, stack.Options);
|
||||||
|
var request = new ZaakRequest(
|
||||||
|
Bronorganisatie: "517439943",
|
||||||
|
VerantwoordelijkeOrganisatie: "517439943",
|
||||||
|
Vertrouwelijkheidaanduiding: "openbaar",
|
||||||
|
Zaaktype: zaaktype!,
|
||||||
|
Startdatum: DateOnly.FromDateTime(DateTime.UtcNow));
|
||||||
|
|
||||||
|
var zaakUrl = await gateway.OpenZaakAsync(request);
|
||||||
|
|
||||||
|
// The gateway returns the canonical zaak URL on OpenZaak's Zaken API...
|
||||||
|
Assert.StartsWith(
|
||||||
|
new Uri(stack.BaseUrl, "/zaken/api/v1/zaken/").ToString(),
|
||||||
|
zaakUrl.ToString());
|
||||||
|
|
||||||
|
// ...and that zaak is really persisted with the default-filled fields.
|
||||||
|
var zaak = await stack.GetZaakAsync(zaakUrl);
|
||||||
|
Assert.Equal(zaaktype.ToString(), zaak.GetProperty("zaaktype").GetString());
|
||||||
|
Assert.Equal("517439943", zaak.GetProperty("bronorganisatie").GetString());
|
||||||
|
Assert.Equal("openbaar", zaak.GetProperty("vertrouwelijkheidaanduiding").GetString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,10 @@ public class OpenZaakGatewayTests
|
|||||||
return new StubHandler(async req =>
|
return new StubHandler(async req =>
|
||||||
{
|
{
|
||||||
c.Seen = req;
|
c.Seen = req;
|
||||||
|
// Capture the length BEFORE reading the body: ReadAsStringAsync buffers the
|
||||||
|
// content and would set ContentLength as a side effect, masking the gateway's
|
||||||
|
// own buffering. Read here to assert the gateway sent a length (not chunked).
|
||||||
|
c.ContentLength = req.Content?.Headers.ContentLength;
|
||||||
c.Body = req.Content is null ? null : await req.Content.ReadAsStringAsync();
|
c.Body = req.Content is null ? null : await req.Content.ReadAsStringAsync();
|
||||||
return new HttpResponseMessage(HttpStatusCode.Created)
|
return new HttpResponseMessage(HttpStatusCode.Created)
|
||||||
{
|
{
|
||||||
@@ -43,6 +47,7 @@ public class OpenZaakGatewayTests
|
|||||||
{
|
{
|
||||||
public HttpRequestMessage? Seen;
|
public HttpRequestMessage? Seen;
|
||||||
public string? Body;
|
public string? Body;
|
||||||
|
public long? ContentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -75,6 +80,20 @@ public class OpenZaakGatewayTests
|
|||||||
Assert.Equal("EPSG:4326", Assert.Single(capture.Seen.Content!.Headers.GetValues("Content-Crs")));
|
Assert.Equal("EPSG:4326", Assert.Single(capture.Seen.Content!.Headers.GetValues("Content-Crs")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Sends_the_body_with_a_content_length_so_it_is_not_chunked()
|
||||||
|
{
|
||||||
|
// OpenZaak's uwsgi rejects a chunked request body (400). The gateway buffers
|
||||||
|
// the body so a Content-Length is sent. JsonContent has no length until
|
||||||
|
// buffered, so this guards the fix the real-OpenZaak integration test found.
|
||||||
|
var handler = Created(out var capture);
|
||||||
|
|
||||||
|
await Gateway(handler).OpenZaakAsync(SampleRequest());
|
||||||
|
|
||||||
|
Assert.NotNull(capture.ContentLength);
|
||||||
|
Assert.True(capture.ContentLength > 0);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Mints_a_hs256_jwt_carrying_the_acl_identity_claims()
|
public async Task Mints_a_hs256_jwt_carrying_the_acl_identity_claims()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
<Project Path="Acl.Api/Acl.Api.csproj" />
|
<Project Path="Acl.Api/Acl.Api.csproj" />
|
||||||
<Project Path="Acl.Application/Acl.Application.csproj" />
|
<Project Path="Acl.Application/Acl.Application.csproj" />
|
||||||
<Project Path="Acl.Infrastructure/Acl.Infrastructure.csproj" />
|
<Project Path="Acl.Infrastructure/Acl.Infrastructure.csproj" />
|
||||||
|
<Project Path="Acl.IntegrationTests/Acl.IntegrationTests.csproj" />
|
||||||
<Project Path="Acl.Tests/Acl.Tests.csproj" />
|
<Project Path="Acl.Tests/Acl.Tests.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"stryker-config": {
|
"stryker-config": {
|
||||||
"solution": "Acl.slnx",
|
"solution": "Acl.slnx",
|
||||||
|
"test-projects": ["Acl.Tests/Acl.Tests.csproj"],
|
||||||
"reporters": ["progress", "html"],
|
"reporters": ["progress", "html"],
|
||||||
"thresholds": {
|
"thresholds": {
|
||||||
"high": 95,
|
"high": 95,
|
||||||
|
|||||||
Reference in New Issue
Block a user