| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- #!/usr/bin/env bash
- # m13a_smoke.sh — End-to-end smoke for the M13a auth gate.
- #
- # Walks through:
- # 1. authd /health and /metrics
- # 2. login (super_admin) → access + refresh
- # 3. /v1/users/me with Bearer → user info
- # 4. refresh → new pair (rotated)
- # 5. re-use OLD refresh → session_killed
- # 6. invite → magic-link token (will not email; we just verify
- # the route is wired)
- # 7. admind /v1/dlq (gated): no token → 401, valid token → 200
- # 8. ingestd /v1/admin/ingest (gated): no token → 401, valid
- # token → 200 (we don't check the full ingest pipeline here,
- # just that the gate lets the request through)
- #
- # Requires:
- # - authd running on $BA_AUTHD_HTTP (default http://127.0.0.1:8804)
- # - admind running on $BA_ADMIND_HTTP (default http://127.0.0.1:8803)
- # - ingestd running on $BA_INGESTD_HTTP (default http://127.0.0.1:8800)
- # - $BA_AUTHD_JWT_SECRET set
- # - super_admin user in Postgres with a known password
- # (created by scripts/bootstrap-super-admin.sh)
- #
- # Run:
- # bash scripts/m13a_smoke.sh
- #
- # Exits 0 if all steps pass, non-zero with a summary table on failure.
- set -euo pipefail
- cd "$(dirname "$0")/.."
- AUTHD="${BA_AUTHD_HTTP:-http://127.0.0.1:8804}"
- ADMIND="${BA_ADMIND_HTTP:-http://127.0.0.1:8803}"
- INGESTD="${BA_INGESTD_HTTP:-http://127.0.0.1:8800}"
- SUPER_EMAIL="${BA_SMOKE_SUPER_EMAIL:-super@broad-announce.test}"
- SUPER_PASSWORD="${BA_SMOKE_SUPER_PASSWORD:-test-password-123}"
- TENANT_SLUG="${BA_SMOKE_TENANT_SLUG:-acme}"
- INVITE_EMAIL="${BA_SMOKE_INVITE_EMAIL:-invite-$(date +%s)@acme.test}"
- PASS=0
- FAIL=0
- RESULTS=()
- check() {
- local name="$1"
- local actual="$2"
- local want="$3"
- if [[ "$actual" == "$want" ]]; then
- PASS=$((PASS+1))
- RESULTS+=("OK $name")
- else
- FAIL=$((FAIL+1))
- RESULTS+=("FAIL $name (got $actual, want $want)")
- fi
- }
- # ---------------------------------------------------------------------------
- # 1. authd /health
- # ---------------------------------------------------------------------------
- status=$(curl -s -o /dev/null -w "%{http_code}" "$AUTHD/health")
- check "1. authd /health" "$status" "200"
- # ---------------------------------------------------------------------------
- # 2. login
- # ---------------------------------------------------------------------------
- login_resp=$(curl -s -X POST "$AUTHD/v1/auth/login" \
- -H 'Content-Type: application/json' \
- -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
- login_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/login" \
- -H 'Content-Type: application/json' \
- -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
- check "2. login (super_admin)" "$login_code" "200"
- ACCESS=$(echo "$login_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))")
- REFRESH=$(echo "$login_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('refresh_token',''))")
- if [[ -z "$ACCESS" || -z "$REFRESH" ]]; then
- echo "FATAL: login response missing tokens" >&2
- echo "$login_resp" >&2
- exit 1
- fi
- # ---------------------------------------------------------------------------
- # 3. /v1/users/me with Bearer
- # ---------------------------------------------------------------------------
- me_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $ACCESS" "$AUTHD/v1/users/me")
- check "3. /v1/users/me (with Bearer)" "$me_code" "200"
- me_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" "$AUTHD/v1/users/me")
- check "3a. /v1/users/me (no Bearer)" "$me_no_auth_code" "401"
- # ---------------------------------------------------------------------------
- # 4. refresh → new pair
- # ---------------------------------------------------------------------------
- refresh_resp=$(curl -s -X POST "$AUTHD/v1/auth/refresh" \
- -H 'Content-Type: application/json' \
- -d "{\"refresh_token\":\"$REFRESH\"}")
- refresh_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/refresh" \
- -H 'Content-Type: application/json' \
- -d "{\"refresh_token\":\"$REFRESH\"}")
- check "4. refresh" "$refresh_code" "200"
- NEW_ACCESS=$(echo "$refresh_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))")
- NEW_REFRESH=$(echo "$refresh_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('refresh_token',''))")
- if [[ "$NEW_ACCESS" == "$ACCESS" || "$NEW_REFRESH" == "$REFRESH" ]]; then
- FAIL=$((FAIL+1))
- RESULTS+=("FAIL 4a. refresh rotated (got same tokens)")
- else
- PASS=$((PASS+1))
- RESULTS+=("OK 4a. refresh rotated (new JTI + new refresh)")
- fi
- # ---------------------------------------------------------------------------
- # 5. re-use OLD refresh → session_killed
- # ---------------------------------------------------------------------------
- reuse_resp=$(curl -s -X POST "$AUTHD/v1/auth/refresh" \
- -H 'Content-Type: application/json' \
- -d "{\"refresh_token\":\"$REFRESH\"}")
- reuse_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/refresh" \
- -H 'Content-Type: application/json' \
- -d "{\"refresh_token\":\"$REFRESH\"}")
- check "5. re-use old refresh" "$reuse_code" "401"
- if echo "$reuse_resp" | grep -q "session_killed"; then
- PASS=$((PASS+1))
- RESULTS+=("OK 5a. reuse → session_killed (family killed)")
- else
- FAIL=$((FAIL+1))
- RESULTS+=("FAIL 5a. reuse → expected 'session_killed', got: $reuse_resp")
- fi
- # After kill, NEW_REFRESH is also revoked
- post_kill_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/refresh" \
- -H 'Content-Type: application/json' \
- -d "{\"refresh_token\":\"$NEW_REFRESH\"}")
- check "5b. new refresh after family kill" "$post_kill_code" "401"
- # ---------------------------------------------------------------------------
- # 6. invite
- # ---------------------------------------------------------------------------
- invite_resp=$(curl -s -X POST "$AUTHD/v1/users/invite" \
- -H "Authorization: Bearer $ACCESS" \
- -H 'Content-Type: application/json' \
- -d "{\"tenant_slug\":\"$TENANT_SLUG\",\"email\":\"$INVITE_EMAIL\",\"role\":\"tenant_admin\"}")
- invite_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/users/invite" \
- -H "Authorization: Bearer $ACCESS" \
- -H 'Content-Type: application/json' \
- -d "{\"tenant_slug\":\"$TENANT_SLUG\",\"email\":\"another-$(date +%s)@acme.test\",\"role\":\"viewer\"}")
- check "6. invite (super_admin)" "$invite_code" "200"
- invite_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/users/invite" \
- -H 'Content-Type: application/json' \
- -d '{"email":"x@y.com","role":"viewer"}')
- check "6a. invite (no Bearer)" "$invite_no_auth_code" "401"
- # ---------------------------------------------------------------------------
- # 7. admind /v1/dlq (gated)
- # ---------------------------------------------------------------------------
- dlq_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" "$ADMIND/v1/dlq")
- check "7. admind /v1/dlq (no auth)" "$dlq_no_auth_code" "401"
- # Need a fresh token (the family was killed above)
- login_resp=$(curl -s -X POST "$AUTHD/v1/auth/login" \
- -H 'Content-Type: application/json' \
- -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
- ACCESS=$(echo "$login_resp" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
- dlq_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $ACCESS" "$ADMIND/v1/dlq")
- check "7a. admind /v1/dlq (super_admin Bearer)" "$dlq_code" "200"
- # ---------------------------------------------------------------------------
- # 8. ingestd /v1/admin/ingest (gated)
- # ---------------------------------------------------------------------------
- ingest_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$INGESTD/v1/admin/ingest")
- check "8. ingestd /v1/admin/ingest (no auth)" "$ingest_no_auth_code" "401"
- ingest_auth_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$INGESTD/v1/admin/ingest" \
- -H "Authorization: Bearer $ACCESS" \
- -H 'Content-Type: application/json' \
- -d '{}')
- # We expect 4xx (invalid alert) or 5xx (deps nil), not 401. The point is
- # the gate let us through.
- if [[ "$ingest_auth_code" == "401" ]]; then
- FAIL=$((FAIL+1))
- RESULTS+=("FAIL 8a. ingestd /v1/admin/ingest (auth Bearer) — gate rejected the token")
- else
- PASS=$((PASS+1))
- RESULTS+=("OK 8a. ingestd /v1/admin/ingest (auth Bearer) — gate passed ($ingest_auth_code)")
- fi
- # ---------------------------------------------------------------------------
- # Summary
- # ---------------------------------------------------------------------------
- echo
- echo "=== M13a smoke summary ==="
- for r in "${RESULTS[@]}"; do
- echo " $r"
- done
- echo
- echo " $PASS passed, $FAIL failed"
- echo
- if [[ $FAIL -gt 0 ]]; then
- exit 1
- fi
- echo "all M13a smoke checks passed"
|