m13a_smoke.sh 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #!/usr/bin/env bash
  2. # m13a_smoke.sh — End-to-end smoke for the M13a auth gate.
  3. #
  4. # Walks through:
  5. # 1. authd /health and /metrics
  6. # 2. login (super_admin) → access + refresh
  7. # 3. /v1/users/me with Bearer → user info
  8. # 4. refresh → new pair (rotated)
  9. # 5. re-use OLD refresh → session_killed
  10. # 6. invite → magic-link token (will not email; we just verify
  11. # the route is wired)
  12. # 7. admind /v1/dlq (gated): no token → 401, valid token → 200
  13. # 8. ingestd /v1/admin/ingest (gated): no token → 401, valid
  14. # token → 200 (we don't check the full ingest pipeline here,
  15. # just that the gate lets the request through)
  16. #
  17. # Requires:
  18. # - authd running on $BA_AUTHD_HTTP (default http://127.0.0.1:8804)
  19. # - admind running on $BA_ADMIND_HTTP (default http://127.0.0.1:8803)
  20. # - ingestd running on $BA_INGESTD_HTTP (default http://127.0.0.1:8800)
  21. # - $BA_AUTHD_JWT_SECRET set
  22. # - super_admin user in Postgres with a known password
  23. # (created by scripts/bootstrap-super-admin.sh)
  24. #
  25. # Run:
  26. # bash scripts/m13a_smoke.sh
  27. #
  28. # Exits 0 if all steps pass, non-zero with a summary table on failure.
  29. set -euo pipefail
  30. cd "$(dirname "$0")/.."
  31. AUTHD="${BA_AUTHD_HTTP:-http://127.0.0.1:8804}"
  32. ADMIND="${BA_ADMIND_HTTP:-http://127.0.0.1:8803}"
  33. INGESTD="${BA_INGESTD_HTTP:-http://127.0.0.1:8800}"
  34. SUPER_EMAIL="${BA_SMOKE_SUPER_EMAIL:-super@broad-announce.test}"
  35. SUPER_PASSWORD="${BA_SMOKE_SUPER_PASSWORD:-test-password-123}"
  36. TENANT_SLUG="${BA_SMOKE_TENANT_SLUG:-acme}"
  37. INVITE_EMAIL="${BA_SMOKE_INVITE_EMAIL:-invite-$(date +%s)@acme.test}"
  38. PASS=0
  39. FAIL=0
  40. RESULTS=()
  41. check() {
  42. local name="$1"
  43. local actual="$2"
  44. local want="$3"
  45. if [[ "$actual" == "$want" ]]; then
  46. PASS=$((PASS+1))
  47. RESULTS+=("OK $name")
  48. else
  49. FAIL=$((FAIL+1))
  50. RESULTS+=("FAIL $name (got $actual, want $want)")
  51. fi
  52. }
  53. # ---------------------------------------------------------------------------
  54. # 1. authd /health
  55. # ---------------------------------------------------------------------------
  56. status=$(curl -s -o /dev/null -w "%{http_code}" "$AUTHD/health")
  57. check "1. authd /health" "$status" "200"
  58. # ---------------------------------------------------------------------------
  59. # 2. login
  60. # ---------------------------------------------------------------------------
  61. login_resp=$(curl -s -X POST "$AUTHD/v1/auth/login" \
  62. -H 'Content-Type: application/json' \
  63. -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
  64. login_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/login" \
  65. -H 'Content-Type: application/json' \
  66. -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
  67. check "2. login (super_admin)" "$login_code" "200"
  68. ACCESS=$(echo "$login_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))")
  69. REFRESH=$(echo "$login_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('refresh_token',''))")
  70. if [[ -z "$ACCESS" || -z "$REFRESH" ]]; then
  71. echo "FATAL: login response missing tokens" >&2
  72. echo "$login_resp" >&2
  73. exit 1
  74. fi
  75. # ---------------------------------------------------------------------------
  76. # 3. /v1/users/me with Bearer
  77. # ---------------------------------------------------------------------------
  78. me_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $ACCESS" "$AUTHD/v1/users/me")
  79. check "3. /v1/users/me (with Bearer)" "$me_code" "200"
  80. me_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" "$AUTHD/v1/users/me")
  81. check "3a. /v1/users/me (no Bearer)" "$me_no_auth_code" "401"
  82. # ---------------------------------------------------------------------------
  83. # 4. refresh → new pair
  84. # ---------------------------------------------------------------------------
  85. refresh_resp=$(curl -s -X POST "$AUTHD/v1/auth/refresh" \
  86. -H 'Content-Type: application/json' \
  87. -d "{\"refresh_token\":\"$REFRESH\"}")
  88. refresh_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/refresh" \
  89. -H 'Content-Type: application/json' \
  90. -d "{\"refresh_token\":\"$REFRESH\"}")
  91. check "4. refresh" "$refresh_code" "200"
  92. NEW_ACCESS=$(echo "$refresh_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))")
  93. NEW_REFRESH=$(echo "$refresh_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('refresh_token',''))")
  94. if [[ "$NEW_ACCESS" == "$ACCESS" || "$NEW_REFRESH" == "$REFRESH" ]]; then
  95. FAIL=$((FAIL+1))
  96. RESULTS+=("FAIL 4a. refresh rotated (got same tokens)")
  97. else
  98. PASS=$((PASS+1))
  99. RESULTS+=("OK 4a. refresh rotated (new JTI + new refresh)")
  100. fi
  101. # ---------------------------------------------------------------------------
  102. # 5. re-use OLD refresh → session_killed
  103. # ---------------------------------------------------------------------------
  104. reuse_resp=$(curl -s -X POST "$AUTHD/v1/auth/refresh" \
  105. -H 'Content-Type: application/json' \
  106. -d "{\"refresh_token\":\"$REFRESH\"}")
  107. reuse_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/refresh" \
  108. -H 'Content-Type: application/json' \
  109. -d "{\"refresh_token\":\"$REFRESH\"}")
  110. check "5. re-use old refresh" "$reuse_code" "401"
  111. if echo "$reuse_resp" | grep -q "session_killed"; then
  112. PASS=$((PASS+1))
  113. RESULTS+=("OK 5a. reuse → session_killed (family killed)")
  114. else
  115. FAIL=$((FAIL+1))
  116. RESULTS+=("FAIL 5a. reuse → expected 'session_killed', got: $reuse_resp")
  117. fi
  118. # After kill, NEW_REFRESH is also revoked
  119. post_kill_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/refresh" \
  120. -H 'Content-Type: application/json' \
  121. -d "{\"refresh_token\":\"$NEW_REFRESH\"}")
  122. check "5b. new refresh after family kill" "$post_kill_code" "401"
  123. # ---------------------------------------------------------------------------
  124. # 6. invite
  125. # ---------------------------------------------------------------------------
  126. invite_resp=$(curl -s -X POST "$AUTHD/v1/users/invite" \
  127. -H "Authorization: Bearer $ACCESS" \
  128. -H 'Content-Type: application/json' \
  129. -d "{\"tenant_slug\":\"$TENANT_SLUG\",\"email\":\"$INVITE_EMAIL\",\"role\":\"tenant_admin\"}")
  130. invite_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/users/invite" \
  131. -H "Authorization: Bearer $ACCESS" \
  132. -H 'Content-Type: application/json' \
  133. -d "{\"tenant_slug\":\"$TENANT_SLUG\",\"email\":\"another-$(date +%s)@acme.test\",\"role\":\"viewer\"}")
  134. check "6. invite (super_admin)" "$invite_code" "200"
  135. invite_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/users/invite" \
  136. -H 'Content-Type: application/json' \
  137. -d '{"email":"x@y.com","role":"viewer"}')
  138. check "6a. invite (no Bearer)" "$invite_no_auth_code" "401"
  139. # ---------------------------------------------------------------------------
  140. # 7. admind /v1/dlq (gated)
  141. # ---------------------------------------------------------------------------
  142. dlq_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" "$ADMIND/v1/dlq")
  143. check "7. admind /v1/dlq (no auth)" "$dlq_no_auth_code" "401"
  144. # Need a fresh token (the family was killed above)
  145. login_resp=$(curl -s -X POST "$AUTHD/v1/auth/login" \
  146. -H 'Content-Type: application/json' \
  147. -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
  148. ACCESS=$(echo "$login_resp" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
  149. dlq_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $ACCESS" "$ADMIND/v1/dlq")
  150. check "7a. admind /v1/dlq (super_admin Bearer)" "$dlq_code" "200"
  151. # ---------------------------------------------------------------------------
  152. # 8. ingestd /v1/admin/ingest (gated)
  153. # ---------------------------------------------------------------------------
  154. ingest_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$INGESTD/v1/admin/ingest")
  155. check "8. ingestd /v1/admin/ingest (no auth)" "$ingest_no_auth_code" "401"
  156. ingest_auth_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$INGESTD/v1/admin/ingest" \
  157. -H "Authorization: Bearer $ACCESS" \
  158. -H 'Content-Type: application/json' \
  159. -d '{}')
  160. # We expect 4xx (invalid alert) or 5xx (deps nil), not 401. The point is
  161. # the gate let us through.
  162. if [[ "$ingest_auth_code" == "401" ]]; then
  163. FAIL=$((FAIL+1))
  164. RESULTS+=("FAIL 8a. ingestd /v1/admin/ingest (auth Bearer) — gate rejected the token")
  165. else
  166. PASS=$((PASS+1))
  167. RESULTS+=("OK 8a. ingestd /v1/admin/ingest (auth Bearer) — gate passed ($ingest_auth_code)")
  168. fi
  169. # ---------------------------------------------------------------------------
  170. # Summary
  171. # ---------------------------------------------------------------------------
  172. echo
  173. echo "=== M13a smoke summary ==="
  174. for r in "${RESULTS[@]}"; do
  175. echo " $r"
  176. done
  177. echo
  178. echo " $PASS passed, $FAIL failed"
  179. echo
  180. if [[ $FAIL -gt 0 ]]; then
  181. exit 1
  182. fi
  183. echo "all M13a smoke checks passed"