Enables dry-run checking before committing to case adoption. The preflight shares the same side-effect-free checks (steps 1–3) as the real take-ownership handler, so it cannot drift from what will actually succeed. Returns the same status codes and error shapes as the real endpoint (200 with wouldSucceed:true, or 409/404/422 if it would fail). Portal renders a "Vooraf controleren" button for legacy cases, surfaced through the existing actions block pattern. Confirmed in smoke.sh with two cases: one where preflight predicts success (and writes nothing), one where it predicts a named invariant failure (matching what the real call reproduces). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
175 lines
9.1 KiB
Bash
Executable File
175 lines
9.1 KiB
Bash
Executable File
#!/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
|