#!/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 10 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 "== Portal frontend (Session 2) ==" HTTP=$(curl -sS -o /dev/null -w '%{http_code}' "$BASE/portal/") check_status "GET /portal/ serves the Angular app" "200" "$HTTP" HTTP=$(curl -sS -o /dev/null -w '%{http_code}' "$BASE/portal/legacy/1001") check_status "deep link /portal/legacy/1001 falls back to index.html" "200" "$HTTP" # Regression check: nginx's implicit redirect construction uses its own # internal `listen` port, not the host's published port - a naive # `return 301 /portal/` silently drops :8080 from the Location header. BARE_REDIRECT_STATUS=$(curl -sS -o /dev/null -w '%{http_code}' "$BASE/portal") check_status "GET /portal (no slash) redirects" "301" "$BARE_REDIRECT_STATUS" FOLLOWED=$(curl -sSL -o /dev/null -w '%{http_code}' "$BASE/portal") check_status "following that redirect lands on a 200, on the same host:port" "200" "$FOLLOWED" 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 preflight: shadow check before take ownership ==" PREFLIGHT_1004=$(curl -sS -o /tmp/preflight_1004.json -w '%{http_code}' "$BASE/api/worklist/legacy/1004/take-ownership/preflight") check_status "preflight predicts A-1004 would adopt cleanly" "200" "$PREFLIGHT_1004" WOULD_SUCCEED=$(python3 -c "import json; print(json.load(open('/tmp/preflight_1004.json'))['wouldSucceed'])") check_status "preflight body reports wouldSucceed" "True" "$WOULD_SUCCEED" SEAM_1004_AFTER_PREFLIGHT=$(curl -sS "$BASE/api/worklist/legacy/1004" | json_field "['seams']['aanvrager']") check_status "preflight on A-1004 wrote nothing (still legacy-backend)" "legacy-backend" "$SEAM_1004_AFTER_PREFLIGHT" PREFLIGHT_1005=$(curl -sS -o /tmp/preflight_1005.json -w '%{http_code}' "$BASE/api/worklist/legacy/1005/take-ownership/preflight") check_status "preflight predicts A-1005 would fail adoption" "422" "$PREFLIGHT_1005" PREFLIGHT_INVARIANT=$(python3 -c "import json; print(json.load(open('/tmp/preflight_1005.json'))['invariant'])") check_status "preflight names the invariant the real call below also fails on" "Bsn.ElevenProof" "$PREFLIGHT_INVARIANT" 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