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:
Executable
+145
@@ -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
|
||||
Reference in New Issue
Block a user