m8_smoke.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. #!/usr/bin/env bash
  2. # Live M8 smoke test. Run from repo root:
  3. # bash scripts/m8_smoke.sh
  4. #
  5. # Walks through the 4 scenarios in M8_VERIFICATION.md:
  6. #
  7. # Step 2 — happy path unchanged: a normal alert with
  8. # fakefcmd in healthy mode delivers in 1
  9. # attempt; 0 DLQ rows.
  10. # Step 3 — DLQ creation: flip fakefcmd into fail
  11. # mode at runtime via /control?fail=1, send
  12. # one alert, wait for the retry budget to
  13. # exhaust (~12s for the default config), and
  14. # verify 1 row in deliveries_dlq with
  15. # attempts=10 + 10 'failed' rows in
  16. # deliveries.
  17. # Step 4 — replay: flip fakefcmd back to healthy,
  18. # POST /v1/dlq/{id}/replay, verify a fresh
  19. # 'sent' row in deliveries and the DLQ row
  20. # is now discarded.
  21. # Step 5 — discard: flip fakefcmd to fail again,
  22. # send another alert, wait, POST
  23. # /v1/dlq/{id}/discard, verify hidden from
  24. # the default list and visible with
  25. # include=all.
  26. #
  27. # Exit code is the number of failed checks.
  28. set -e
  29. cd "$(dirname "$0")/.."
  30. INGESTD=http://localhost:8800
  31. ADMIND=http://localhost:8803
  32. FAKEFCMD=http://localhost:8820
  33. COMPANY=acme-001
  34. SOURCE=prom-prod
  35. SECRET=s3cret-acme
  36. PGCMD="docker exec -i broad-announce-postgres-1 psql -U ba -d ba -A -t"
  37. fails=0
  38. pass() { echo " ✅ $*"; }
  39. fail() { echo " ❌ $*"; fails=$((fails+1)); }
  40. # ── Helpers ─────────────────────────────────────────────────────
  41. # HMAC-sign a body the same way M3 / M5 / M6 do.
  42. # X-BA-Signature: t=<unix>,v1=<hex(hmac-sha256(secret, "<unix>.<body>"))>
  43. # The 202 response body is {"alert_id":"...","dedupe_count":N,
  44. # "received_at":"..."} — we extract alert_id so the
  45. # downstream checks can find the right row.
  46. sign_and_send() {
  47. local body="$1"
  48. local ts=$(date +%s)
  49. local sig=$(printf '%s.%s' "$ts" "$body" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
  50. curl -sS -X POST "$INGESTD/v1/ingest" \
  51. -H "Content-Type: application/json" \
  52. -H "X-BA-Signature: t=$ts,v1=$sig" \
  53. --data "$body" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("alert_id",""))'
  54. }
  55. # ── Step 1: stack ready ─────────────────────────────────────────
  56. echo "Step 1 — stack ready"
  57. docker compose up -d --no-deps seed >/dev/null
  58. sleep 1
  59. if ! curl -sf "$INGESTD/health" >/dev/null; then
  60. fail "ingestd not healthy"
  61. exit 1
  62. fi
  63. pass "ingestd healthy"
  64. if ! curl -sf "$ADMIND/health" >/dev/null; then
  65. fail "admind not healthy"
  66. exit 1
  67. fi
  68. pass "admind healthy"
  69. if ! $PGCMD -c "\\d deliveries_dlq" >/dev/null 2>&1; then
  70. fail "deliveries_dlq table missing"
  71. exit 1
  72. fi
  73. pass "deliveries_dlq table present"
  74. # Reset fakefcmd to healthy.
  75. curl -sf "$FAKEFCMD/control?fail=0" >/dev/null
  76. pass "fakefcmd fail mode = 0 (healthy)"
  77. # ── Step 2: happy path ──────────────────────────────────────────
  78. echo "Step 2 — happy path: 1 alert, 1 attempt, 0 DLQ rows"
  79. DLQ_BEFORE=$($PGCMD -c "SELECT count(*) FROM deliveries_dlq WHERE discarded = false;" | tr -d ' ')
  80. ALERT_BODY=$(cat <<EOF
  81. {
  82. "id": "smoke-m8-happy-$(date +%s)",
  83. "company_id": "$COMPANY",
  84. "source_id": "$SOURCE",
  85. "title": "smoke m8 happy",
  86. "body": "M8 smoke happy path",
  87. "severity": "info",
  88. "category": "smoke"
  89. }
  90. EOF
  91. )
  92. ALERT_ID=$(sign_and_send "$ALERT_BODY")
  93. if [ -n "$ALERT_ID" ]; then
  94. pass "POST /v1/ingest accepted, alert_id=$ALERT_ID"
  95. else
  96. fail "POST /v1/ingest returned no alert_id"
  97. fi
  98. sleep 2
  99. SENT_ROW=$($PGCMD -c "SELECT count(*) FROM deliveries WHERE alert_id = '$ALERT_ID' AND status = 'sent';" | tr -d ' ')
  100. DLQ_AFTER=$($PGCMD -c "SELECT count(*) FROM deliveries_dlq WHERE discarded = false;" | tr -d ' ')
  101. # An alert fans out to every matching recipient; the
  102. # M2 seed has 2 individuals subscribed to acme-001:
  103. # prom-prod, so we expect 2 sent rows.
  104. if [ "$SENT_ROW" -ge "1" ]; then
  105. pass "$SENT_ROW sent deliveries rows for happy alert (>=1)"
  106. else
  107. fail "expected >=1 sent row, got $SENT_ROW"
  108. fi
  109. if [ "$DLQ_BEFORE" = "$DLQ_AFTER" ]; then
  110. pass "DLQ row count unchanged ($DLQ_BEFORE)"
  111. else
  112. fail "DLQ grew from $DLQ_BEFORE to $DLQ_AFTER on happy path"
  113. fi
  114. # ── Step 3: DLQ creation via runtime fail mode ──────────────────
  115. echo "Step 3 — DLQ creation: flip fakefcmd to fail, 1 alert, retries exhaust"
  116. curl -sf "$FAKEFCMD/control?fail=1" >/dev/null
  117. pass "fakefcmd fail mode = 1"
  118. ALERT_BODY=$(cat <<EOF
  119. {
  120. "id": "smoke-m8-dlq-$(date +%s)",
  121. "company_id": "$COMPANY",
  122. "source_id": "$SOURCE",
  123. "title": "smoke m8 dlq",
  124. "body": "M8 smoke DLQ test",
  125. "severity": "info",
  126. "category": "smoke"
  127. }
  128. EOF
  129. )
  130. ALERT_ID=$(sign_and_send "$ALERT_BODY")
  131. if [ -n "$ALERT_ID" ]; then
  132. pass "POST /v1/ingest accepted, alert_id=$ALERT_ID (will fail at delivery)"
  133. else
  134. fail "POST /v1/ingest returned no alert_id"
  135. fi
  136. # Wait for retry budget to exhaust. Defaults: 10
  137. # attempts, base 100ms, cap 2s ≈ 12s total. The
  138. # timeout ceiling here is 30s.
  139. for i in $(seq 1 30); do
  140. DLQ_COUNT=$($PGCMD -c "SELECT count(*) FROM deliveries_dlq WHERE alert_id = '$ALERT_ID';" | tr -d ' ')
  141. if [ "$DLQ_COUNT" = "1" ]; then
  142. break
  143. fi
  144. sleep 1
  145. done
  146. if [ "$DLQ_COUNT" = "1" ]; then
  147. pass "1 row in deliveries_dlq for $ALERT_ID"
  148. else
  149. fail "expected 1 DLQ row for $ALERT_ID, got $DLQ_COUNT (waited 30s)"
  150. fi
  151. ATTEMPTS=$($PGCMD -c "SELECT attempts FROM deliveries_dlq WHERE alert_id = '$ALERT_ID';" | tr -d ' ')
  152. if [ "$ATTEMPTS" = "10" ]; then
  153. pass "DLQ row has attempts=10"
  154. else
  155. fail "expected attempts=10, got $ATTEMPTS"
  156. fi
  157. FAILED_ROWS=$($PGCMD -c "SELECT count(*) FROM deliveries WHERE alert_id = '$ALERT_ID' AND status = 'failed';" | tr -d ' ')
  158. # Per-recipient: 10 attempts × N recipients (1 for
  159. # M8 smoke, but the seed could fan out). We check
  160. # >= 10*N for the smoke.
  161. EXPECTED_FAILED=$((10 * 1))
  162. if [ "$FAILED_ROWS" -ge "$EXPECTED_FAILED" ]; then
  163. pass "$FAILED_ROWS failed deliveries rows (>= $EXPECTED_FAILED for one recipient × 10 attempts)"
  164. else
  165. fail "expected >= $EXPECTED_FAILED failed rows, got $FAILED_ROWS"
  166. fi
  167. # Check the API: GET /v1/dlq should include our row.
  168. API_ROW=$(curl -sf "$ADMIND/v1/dlq?company_id=$COMPANY&alert_id=$ALERT_ID" | grep -o "\"alert_id\":\"$ALERT_ID\"" | head -1 || true)
  169. if [ -n "$API_ROW" ]; then
  170. pass "GET /v1/dlq returns the row"
  171. else
  172. fail "GET /v1/dlq missing the row"
  173. fi
  174. DLQ_ID=$($PGCMD -c "SELECT id FROM deliveries_dlq WHERE alert_id = '$ALERT_ID';" | tr -d ' ')
  175. # ── Step 4: replay ──────────────────────────────────────────────
  176. echo "Step 4 — replay: flip fakefcmd healthy, POST /v1/dlq/$DLQ_ID/replay"
  177. curl -sf "$FAKEFCMD/control?fail=0" >/dev/null
  178. pass "fakefcmd fail mode = 0"
  179. REPLAY_RESP=$(curl -sf -X POST "$ADMIND/v1/dlq/$DLQ_ID/replay" || true)
  180. REPLAY_OK=$(echo "$REPLAY_RESP" | grep -o '"replayed":true' | head -1 || true)
  181. if [ -n "$REPLAY_OK" ]; then
  182. pass "POST /v1/dlq/$DLQ_ID/replay returned replayed=true"
  183. else
  184. fail "POST /v1/dlq/$DLQ_ID/replay did not return replayed=true (got: $REPLAY_RESP)"
  185. fi
  186. # Wait for the replayed delivery to land. The replay
  187. # re-publishes onto deliveries.fcm.acme-001, which
  188. # deliverd-fcm consumes (now with healthy fakefcmd).
  189. for i in $(seq 1 15); do
  190. SENT_REPLAY=$($PGCMD -c "SELECT count(*) FROM deliveries WHERE alert_id = '$ALERT_ID' AND status = 'sent';" | tr -d ' ')
  191. if [ "$SENT_REPLAY" -ge "1" ]; then
  192. break
  193. fi
  194. sleep 1
  195. done
  196. if [ "$SENT_REPLAY" -ge "1" ]; then
  197. pass "replay produced a sent deliveries row"
  198. else
  199. fail "no sent row after replay (got $SENT_REPLAY)"
  200. fi
  201. DISCARDED=$($PGCMD -c "SELECT discarded FROM deliveries_dlq WHERE id = $DLQ_ID;" | tr -d ' ')
  202. if [ "$DISCARDED" = "t" ]; then
  203. pass "DLQ row marked discarded after replay"
  204. else
  205. fail "DLQ row not discarded (got '$DISCARDED')"
  206. fi
  207. API_AFTER=$(curl -sf "$ADMIND/v1/dlq?company_id=$COMPANY&alert_id=$ALERT_ID" | grep -o "\"alert_id\":\"$ALERT_ID\"" | head -1 || true)
  208. if [ -z "$API_AFTER" ]; then
  209. pass "GET /v1/dlq no longer returns the discarded row"
  210. else
  211. fail "GET /v1/dlq still returns the discarded row"
  212. fi
  213. # ── Step 5: discard ─────────────────────────────────────────────
  214. echo "Step 5 — discard: another DLQ row, then POST /v1/dlq/{id}/discard"
  215. curl -sf "$FAKEFCMD/control?fail=1" >/dev/null
  216. ALERT_BODY=$(cat <<EOF
  217. {
  218. "id": "smoke-m8-discard-$(date +%s)",
  219. "company_id": "$COMPANY",
  220. "source_id": "$SOURCE",
  221. "title": "smoke m8 discard",
  222. "body": "M8 smoke discard test",
  223. "severity": "info",
  224. "category": "smoke"
  225. }
  226. EOF
  227. )
  228. ALERT_ID2=$(sign_and_send "$ALERT_BODY")
  229. if [ -n "$ALERT_ID2" ]; then
  230. pass "POST /v1/ingest accepted, alert_id=$ALERT_ID2"
  231. else
  232. fail "POST /v1/ingest returned no alert_id"
  233. fi
  234. for i in $(seq 1 30); do
  235. DLQ2_COUNT=$($PGCMD -c "SELECT count(*) FROM deliveries_dlq WHERE alert_id = '$ALERT_ID2';" | tr -d ' ')
  236. if [ "$DLQ2_COUNT" = "1" ]; then
  237. break
  238. fi
  239. sleep 1
  240. done
  241. if [ "$DLQ2_COUNT" = "1" ]; then
  242. pass "1 row in deliveries_dlq for $ALERT_ID2"
  243. else
  244. fail "expected 1 DLQ row for $ALERT_ID2, got $DLQ2_COUNT"
  245. fi
  246. DLQ_ID2=$($PGCMD -c "SELECT id FROM deliveries_dlq WHERE alert_id = '$ALERT_ID2';" | tr -d ' ')
  247. DISC_RESP=$(curl -sf -X POST "$ADMIND/v1/dlq/$DLQ_ID2/discard" || true)
  248. DISC_OK=$(echo "$DISC_RESP" | grep -o '"discarded":true' | head -1 || true)
  249. if [ -n "$DISC_OK" ]; then
  250. pass "POST /v1/dlq/$DLQ_ID2/discard returned discarded=true"
  251. else
  252. fail "POST /v1/dlq/$DLQ_ID2/discard did not return discarded=true (got: $DISC_RESP)"
  253. fi
  254. API_DISC=$(curl -sf "$ADMIND/v1/dlq?company_id=$COMPANY&alert_id=$ALERT_ID2" | grep -o "\"alert_id\":\"$ALERT_ID2\"" | head -1 || true)
  255. if [ -z "$API_DISC" ]; then
  256. pass "GET /v1/dlq hides the discarded row"
  257. else
  258. fail "GET /v1/dlq still returns the discarded row"
  259. fi
  260. API_DISC_ALL=$(curl -sf "$ADMIND/v1/dlq?company_id=$COMPANY&alert_id=$ALERT_ID2&include=all" | grep -o "\"alert_id\":\"$ALERT_ID2\"" | head -1 || true)
  261. if [ -n "$API_DISC_ALL" ]; then
  262. pass "GET /v1/dlq?include=all shows the discarded row"
  263. else
  264. fail "GET /v1/dlq?include=all does not show the discarded row"
  265. fi
  266. # ── Summary ─────────────────────────────────────────────────────
  267. curl -sf "$FAKEFCMD/control?fail=0" >/dev/null
  268. echo
  269. if [ "$fails" = "0" ]; then
  270. echo "🎉 M8 smoke: 12/12 checks green."
  271. exit 0
  272. else
  273. echo "💥 M8 smoke: $fails check(s) failed."
  274. exit 1
  275. fi