feat: implement strangler-fig-demo Session 1 (backend + smoke script)

Builds the four-seam, three-write-path reference demo backend: case-framework
(seam D stand-in), legacy-backend/frontend (SQL Server, seams A/B/C targets),
and new-backend (Domain/Application/Infrastructure.*/Api implementing the
source resolver, take/release-ownership, write-through translator, and owned
assessment flow), wired together via docker-compose with a plain placeholder
frontend standing in for the Angular portal until Session 2.

All 11 Architecture.Tests pass and scripts/smoke.sh passes end-to-end against
a fresh `docker compose up`, covering acceptance criteria 1-3 and 7-22.

Fixes two real domain bugs found only once the stack ran for real: the BSN
eleven-proof checksum trivially passes all-zero digits, and the adoption
mapper silently treated a partial legacy address as absent instead of failing
loudly. Also fixes several environment-specific integration issues (rootless
Podman/SELinux bind-mount permissions, a buildah NuGet layer-caching bug,
SqlClient's invariant-globalization incompatibility, and an nginx path-prefix
mismatch for the legacy frontend).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-31 07:57:26 +02:00
co-authored by Claude Sonnet 5
parent 09b27173a7
commit a6a1abbe9c
129 changed files with 6379 additions and 1 deletions
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
# Smoke-tests the strangler-fig-demo stack against the acceptance criteria in
# the design notes (§13). Run against a FRESH `docker compose up` - it
# depends on the seed data being untouched (12 legacy rows 1001-1012, 5 owned
# REG-2026-0001..0005 at fixed ids). Exits non-zero on the first failure.
set -euo pipefail
BASE="${SMOKE_BASE_URL:-http://localhost:8080}"
FAILURES=0
pass() { echo " OK $1"; }
fail() { echo " FAIL $1"; FAILURES=$((FAILURES + 1)); }
check_status() {
local desc="$1" expected="$2" actual="$3"
if [ "$actual" = "$expected" ]; then pass "$desc ($actual)"; else fail "$desc (expected $expected, got $actual)"; fi
}
json_field() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d$1)"; }
echo "== Infrastructure =="
STATUS=$(docker compose ps --format json 2>/dev/null | python3 -c "
import sys, json
ok = True
for line in sys.stdin:
line = line.strip()
if not line:
continue
d = json.loads(line)
svc = d.get('Service')
state = d.get('State')
health = d.get('Health', '')
if state != 'running':
print(f'{svc}: state={state}'); ok = False
if health and health != 'healthy':
print(f'{svc}: health={health}'); ok = False
print('ALL_OK' if ok else 'SOME_FAILED')
")
if echo "$STATUS" | grep -q ALL_OK; then pass "all 9 containers running/healthy"; else fail "container status: $STATUS"; fi
if curl -sS --max-time 2 http://localhost:8081 >/dev/null 2>&1; then
fail "legacy-backend must NOT be reachable from the host (port 8081 responded)"
else
pass "legacy-backend not reachable from host"
fi
if curl -sS --max-time 2 http://localhost:8082 >/dev/null 2>&1; then
fail "case-framework must NOT be reachable from the host (port 8082 responded)"
else
pass "case-framework not reachable from host"
fi
echo "== Seam A: unified read =="
WORKLIST=$(curl -sS "$BASE/api/worklist")
TOTAL=$(echo "$WORKLIST" | json_field "['totalCount']")
if [ "$TOTAL" = "17" ]; then pass "GET /api/worklist returns 17 items"; else fail "expected 17 items, got $TOTAL"; fi
DETAIL_1001=$(curl -sS "$BASE/api/worklist/legacy/1001")
SEAM_AANVRAGER=$(echo "$DETAIL_1001" | json_field "['seams']['aanvrager']")
check_status "A-1001 seam inspector names legacy-backend" "legacy-backend" "$SEAM_AANVRAGER"
echo "== Write path 1: redirect (seam C) =="
MODE=$(echo "$DETAIL_1001" | json_field "['actions']['recordAssessment']['mode']")
check_status "A-1001 recordAssessment action is a redirect" "redirect" "$MODE"
echo "== Write path 2: write-through (seam B) =="
HTTP=$(curl -sS -o /dev/null -w '%{http_code}' -X PUT "$BASE/api/worklist/legacy/1001/details" \
-H "Content-Type: application/json" \
-d '{"surname":"de Vries","initials":"A.","address":{"street":"Kerkweg","number":"12","postalCode":"3512JK","city":"Utrecht"},"email":"anna.devries@example.nl","phone":"+31612345678","preferredChannel":"Post"}')
check_status "valid write-through details edit on A-1001" "204" "$HTTP"
ERRORS=$(curl -sS -X PUT "$BASE/api/worklist/legacy/1001/details" \
-H "Content-Type: application/json" \
-d '{"surname":"","initials":"A.","address":{"street":"Kerkweg","number":"","postalCode":"12AB","city":"Utrecht"},"email":null,"phone":null,"preferredChannel":"Post"}')
ERROR_COUNT=$(echo "$ERRORS" | json_field "['errors'].__len__()" 2>/dev/null || echo "$ERRORS" | python3 -c "import sys,json;print(len(json.load(sys.stdin)['errors']))")
check_status "3-field-invalid write-through returns 3 mapped errors" "3" "$ERROR_COUNT"
echo "== Write path 3: take ownership =="
TAKE=$(curl -sS -o /tmp/take_1002.json -w '%{http_code}' -X POST "$BASE/api/worklist/legacy/1002/take-ownership")
check_status "take ownership of A-1002" "201" "$TAKE"
REG_1002=$(python3 -c "import json; print(json.load(open('/tmp/take_1002.json'))['registrationApplicationId'])")
DETAIL_1002=$(curl -sS "$BASE/api/worklist/owned/$REG_1002")
SEAM_AFTER=$(echo "$DETAIL_1002" | json_field "['seams']['aanvrager']")
check_status "adopted case's seam inspector now reads owned" "owned" "$SEAM_AFTER"
BEFORE=$(curl -sS "$BASE/api/diagnostics/legacy-call-count" | json_field "['count']")
curl -sS -o /dev/null -X PUT "$BASE/api/worklist/owned/$REG_1002/details" \
-H "Content-Type: application/json" \
-d '{"surname":"Jansen","initials":"P.","address":null,"email":null,"phone":null,"preferredChannel":"Post"}'
AFTER=$(curl -sS "$BASE/api/diagnostics/legacy-call-count" | json_field "['count']")
check_status "editing the adopted case makes no legacy calls" "$BEFORE" "$AFTER"
DIRECT=$(curl -sS -o /dev/null -w '%{http_code}' -X PUT "$BASE/api/worklist/legacy/1002/details" \
-H "Content-Type: application/json" -d '{"surname":"x","initials":"x","address":null,"email":null,"phone":null,"preferredChannel":"Post"}')
check_status "direct legacy write to an adopted case is blocked" "409" "$DIRECT"
for pair in "1003:ContactDetails.EmailRequiredForEmailChannel" "1005:Bsn.ElevenProof" "1006:Assessment.MotivationTooShort" "1007:Address.AllPartsRequired"; do
ID="${pair%%:*}"; EXPECTED_INVARIANT="${pair##*:}"
RESULT=$(curl -sS -o /tmp/adopt_fail.json -w '%{http_code}' -X POST "$BASE/api/worklist/legacy/$ID/take-ownership")
check_status "adoption of A-$ID fails" "422" "$RESULT"
ACTUAL_INVARIANT=$(python3 -c "import json; print(json.load(open('/tmp/adopt_fail.json')).get('invariant'))")
check_status "A-$ID fails on $EXPECTED_INVARIANT" "$EXPECTED_INVARIANT" "$ACTUAL_INVARIANT"
done
RELEASE_1002=$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE "$BASE/api/worklist/owned/$REG_1002/ownership")
check_status "releasing an edited owned case is blocked" "409" "$RELEASE_1002"
echo "== Owned assessment + conformist boundary (seam D) =="
INVALID_ASSESSMENT=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
"$BASE/api/worklist/owned/00000000-0000-0000-0000-000000000002/assessment" \
-H "Content-Type: application/json" \
-d '{"verifiedItems":[],"exceptionReason":null,"outcome":"Approved","rejectionCategory":null,"motivation":"too short"}')
check_status "direct invalid assessment payload is rejected" "422" "$INVALID_ASSESSMENT"
ASSESSMENT=$(curl -sS -X POST "$BASE/api/worklist/owned/00000000-0000-0000-0000-000000000002/assessment" \
-H "Content-Type: application/json" \
-d '{"verifiedItems":["document","land","datum"],"exceptionReason":null,"outcome":"Approved","rejectionCategory":null,"motivation":"Alle bewijsstukken zijn gecontroleerd en akkoord bevonden."}')
CLOSURE_PENDING=$(echo "$ASSESSMENT" | json_field "['closurePending']")
check_status "REG-2026-0002 assessment reports closure pending (open task)" "True" "$CLOSURE_PENDING"
OUTCOME_AFTER=$(curl -sS "$BASE/api/worklist/owned/00000000-0000-0000-0000-000000000002" | json_field "['assessment']['outcome']")
check_status "REG-2026-0002 outcome recorded, not rolled back" "Approved" "$OUTCOME_AFTER"
echo
echo "== Architecture tests =="
if (cd "$(dirname "$0")/../new" && dotnet test tests/Architecture.Tests/Architecture.Tests.csproj -c Release --nologo 2>&1 | tail -5 | grep -q "Failed: 0"); then
pass "all Architecture.Tests pass"
else
fail "Architecture.Tests reported failures"
fi
echo
if [ "$FAILURES" -eq 0 ]; then
echo "All checks passed."
exit 0
else
echo "$FAILURES check(s) failed."
exit 1
fi