test(portals): assert each portal proxies only its own endpoint group (refs #166)
The four portal proxy configs are near-identical, so a copy-paste slip is cheap to introduce and expensive to find: proxying another portal's endpoint group hands a browser an endpoint its token is not for, and the failure surfaces as a 401 three services away. Asserts each portal proxies exactly its own groups to the BFF and keeps the SPA fallback for Angular's client-side routes. Red: the Caddyfiles it reads do not exist yet.
This commit is contained in:
@@ -76,6 +76,7 @@ build:
|
|||||||
unit:
|
unit:
|
||||||
dotnet test $(SLN) -c Release --filter "Category!=Integration" --logger trx --results-directory TestResults
|
dotnet test $(SLN) -c Release --filter "Category!=Integration" --logger trx --results-directory TestResults
|
||||||
python3 infra/test_playwright_summary.py
|
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)
|
## 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`
|
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
|
||||||
|
|||||||
@@ -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 <path> { 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")
|
||||||
Reference in New Issue
Block a user