diff --git a/Makefile b/Makefile index d9efbc2..0d0ff64 100644 --- a/Makefile +++ b/Makefile @@ -76,6 +76,7 @@ build: unit: dotnet test $(SLN) -c Release --filter "Category!=Integration" --logger trx --results-directory TestResults python3 infra/test_playwright_summary.py + python3 infra/test_portal_caddyfiles.py ## mutation: run the Stryker.NET ratchet on each service with branching logic (fails below baseline) # Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore` diff --git a/infra/test_portal_caddyfiles.py b/infra/test_portal_caddyfiles.py new file mode 100644 index 0000000..b72172f --- /dev/null +++ b/infra/test_portal_caddyfiles.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Self-check for the portals' Caddyfiles — stdlib asserts, no framework. + +Run: python3 infra/test_portal_caddyfiles.py (also runs in `make unit`). + +Each portal serves its Angular app and reverse-proxies *its own* BFF endpoint group +same-origin, so the browser never sees CORS and the DigiD token rides along (ADR-0010). +The four files are near-identical, which makes a copy-paste slip cheap to introduce and +expensive to find: proxying another portal's group hands a behandelaar's browser an +endpoint its token isn't for, and the failure shows up as a 401 three services away. + +What is asserted per portal: it proxies exactly its own groups to the BFF service, and it +falls back to index.html so Angular's client-side routes survive a deep link / refresh. +""" +import os +import re + +APPS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "apps") + +# The self-service portal also renders the public register (S-09), so it proxies both. +EXPECTED = { + "self-service": {"/self-service/*", "/openbaar/*"}, + "openbaar": {"/openbaar/*"}, + "behandel": {"/behandel/*"}, + "beheer": {"/beheer/*"}, +} +ALL_GROUPS = {g for groups in EXPECTED.values() for g in groups} + + +def caddyfile(app): + with open(os.path.join(APPS, app, "Caddyfile")) as fh: + return fh.read() + + +def proxied_groups(text): + """The path groups routed to the BFF: `handle { reverse_proxy bff:8080 }`.""" + return { + m.group(1) + for m in re.finditer(r"handle\s+(\S+)\s*\{[^}]*reverse_proxy\s+bff:8080", text) + } + + +def test_each_portal_proxies_exactly_its_own_endpoint_groups(): + for app, expected in EXPECTED.items(): + got = proxied_groups(caddyfile(app)) + assert got == expected, f"{app}: proxies {got or '{}'}, expected {expected}" + + +def test_no_portal_proxies_another_portals_group(): + for app, expected in EXPECTED.items(): + strays = proxied_groups(caddyfile(app)) & (ALL_GROUPS - expected) + assert not strays, f"{app}: proxies another portal's group {strays}" + + +def test_every_portal_falls_back_to_index_html(): + """Angular routes client-side: an unknown path must serve the app, not a 404.""" + for app in EXPECTED: + text = caddyfile(app) + assert "try_files {path} /index.html" in text, f"{app}: no SPA fallback" + assert "file_server" in text, f"{app}: nothing serves the built app" + + +if __name__ == "__main__": + for name, fn in sorted(globals().items()): + if name.startswith("test_") and callable(fn): + fn() + print(f" ok {name}") + print("portal Caddyfile self-check passed")