diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 7eabeb2..ca47c29 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -207,6 +207,9 @@ jobs: - name: Objecten API up + token authenticates + trusts Objecttypen id: objecten run: OBJECTEN_TIMEOUT=120 make verify-objecten + - name: RegisterRecord objecttype registered + published + id: registerrecord + run: REGISTERRECORD_TIMEOUT=120 make verify-registerrecord - name: ACL ↔ OpenZaak integration tests id: acl run: make verify-acl @@ -241,6 +244,7 @@ jobs: OBS: ${{ steps.obs.outcome }} OBJECTTYPEN: ${{ steps.objecttypen.outcome }} OBJECTEN: ${{ steps.objecten.outcome }} + REGISTERRECORD: ${{ steps.registerrecord.outcome }} ACL: ${{ steps.acl.outcome }} NRC: ${{ steps.nrc.outcome }} PROJECTION: ${{ steps.projection.outcome }} @@ -261,6 +265,7 @@ jobs: echo "| Observability backplane | $(icon "$OBS") |" echo "| Objecttypen API + token | $(icon "$OBJECTTYPEN") |" echo "| Objecten API + token | $(icon "$OBJECTEN") |" + echo "| RegisterRecord objecttype | $(icon "$REGISTERRECORD") |" echo "| ACL ↔ OpenZaak | $(icon "$ACL") |" echo "| OpenZaak → NRC | $(icon "$NRC") |" echo "| NRC → Event Subscriber → projection | $(icon "$PROJECTION") |" diff --git a/Makefile b/Makefile index a399cbf..2b7199f 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK) endif endif -.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-notifications smoke up down local verify-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 frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-notifications smoke up down local verify-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, frontend, verify (mirrors Gitea Actions) ## `verify` is the live-stack stage (full stack up once → ACL + notification checks). @@ -196,6 +196,11 @@ verify-objecttypen: verify-objecten: bash infra/run-objecten-check.sh +## verify-registerrecord: assert the RegisterRecord objecttype is registered + published in the +## Objecttypen API (S-18c), against the already-running stack. +verify-registerrecord: + bash infra/run-registerrecord-check.sh + ## verify: local mirror of the CI verify-stack job — full stack up once, all checks, ## tear down (always). For fast single-concern local iteration use `integration` ## (oz-only) or `verify-notifications` (oz+nrc) instead. diff --git a/infra/registerrecord-check.py b/infra/registerrecord-check.py new file mode 100644 index 0000000..6e02dca --- /dev/null +++ b/infra/registerrecord-check.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""S-18c (#141): prove the RegisterRecord objecttype is registered + published in the Objecttypen API. + +Assert the objecttype named "RegisterRecord" exists, has a **published** version, and that version's +jsonSchema carries the public-safe fields (id, status, reference) — i.e. registerrecord-init ran and +seeded the schema S-19 will write records against. Stdlib only so it runs in a bare python:3-slim +container on the compose network. +""" +import json +import os +import sys +import time +import urllib.error +import urllib.request + +BASE = os.environ["OBJECTTYPEN"] # http://:8000 +TOKEN = os.environ["OBJECTTYPEN_TOKEN"] +TIMEOUT = int(os.environ.get("REGISTERRECORD_TIMEOUT", "60")) +NAME = "RegisterRecord" +EXPECTED_FIELDS = {"id", "status", "reference"} + + +def get(path): + req = urllib.request.Request(f"{BASE}{path}", headers={"Authorization": f"Token {TOKEN}"}) + with urllib.request.urlopen(req, timeout=10) as r: + return json.load(r) + + +def check(): + """Return (ok, detail). Raises on transport errors so the caller can retry.""" + ots = get("/api/v2/objecttypes").get("results", []) + match = next((o for o in ots if o.get("name") == NAME), None) + if not match: + return False, f"no objecttype named {NAME!r} (have: {[o.get('name') for o in ots]})" + if not match.get("versions"): + return False, f"{NAME} exists but has no versions" + # The versions list holds URLs; fetch each to find a published one. + for ver_url in match["versions"]: + ver = get(ver_url[len(BASE):] if ver_url.startswith(BASE) else ver_url) + if ver.get("status") != "published": + continue + props = set((ver.get("jsonSchema") or {}).get("properties", {})) + if not EXPECTED_FIELDS <= props: + return False, f"published version missing fields: {EXPECTED_FIELDS - props}" + return True, f"{NAME} v{ver.get('version')} published, fields={sorted(props)}" + return False, f"{NAME} has versions but none are published" + + +def main(): + deadline = time.time() + TIMEOUT + detail = "no attempt" + while time.time() < deadline: + try: + ok, detail = check() + if ok: + print(f"OK — {detail}") + return 0 + except (urllib.error.URLError, ConnectionError, TimeoutError) as e: + detail = f"transport: {e}" + time.sleep(3) + print(f"FAIL — {detail}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/infra/run-registerrecord-check.sh b/infra/run-registerrecord-check.sh new file mode 100755 index 0000000..3f4d546 --- /dev/null +++ b/infra/run-registerrecord-check.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# S-18c (#141): assert the RegisterRecord objecttype is registered + published in the Objecttypen +# API, against an ALREADY-RUNNING stack. Runs the check in a python:3-slim container on the stack +# network (the service is reached by container IP; the runner can't reach published ports — +# gitea-actions-gotchas.md §5/§6). Does NOT manage the stack lifecycle. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# The dev token provisioned by infra/objecttypen/setup_configuration/data.yaml. +TOKEN="${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}" + +ot="$(docker ps -q --filter 'name=objecttypen' --filter 'health=healthy' | head -1)" +[ -n "$ot" ] || ot="$(docker ps -q --filter 'name=[-_]objecttypen[-_]' | head -1)" +[ -n "$ot" ] || { echo "ERROR: no running objecttypen container — bring the stack up first" >&2; exit 1; } +net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$ot" | head -1)" +ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$ot")" +echo ">> network=$net objecttypen=$ip" + +cid="$(docker create --network "$net" \ + -e "OBJECTTYPEN=http://$ip:8000" -e "OBJECTTYPEN_TOKEN=$TOKEN" \ + -e "REGISTERRECORD_TIMEOUT=${REGISTERRECORD_TIMEOUT:-60}" \ + python:3-slim python /registerrecord-check.py)" +docker cp "$here/registerrecord-check.py" "$cid:/registerrecord-check.py" >/dev/null +rc=0; docker start -a "$cid" || rc=$? +docker rm -f "$cid" >/dev/null +exit $rc