m3_smoke.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. #!/usr/bin/env bash
  2. # Live M3 smoke test. Run from repo root:
  3. # bash scripts/m3_smoke.sh
  4. #
  5. # Walks through the scenarios in M3_VERIFICATION.md:
  6. #
  7. # Step 2 — warning to db-prod-04 → Alice only (Alice pre-linked, Bob not
  8. # yet linked, Carol unlinked + quiet hours).
  9. # Expected: 1 fcm (Alice) + 1 telegram (Alice).
  10. # Step 3 — critical to db-prod-04 → Alice + Bob (fcm). Bob is unlinked
  11. # so no telegram for him.
  12. # Step 4 — Bob runs /start acme-bob-002 via faketgmd admin queue.
  13. # Expect reply + telegram_user_id linked.
  14. # Step 5 — critical to db-prod-04 again. Bob is now linked.
  15. # Expected: 1 fcm (Alice) + 1 fcm (Bob) + 1 telegram (Alice) +
  16. # 1 telegram (Bob).
  17. # Step 6 — inminent_colapse to rack-b. Carol's quiet hours (00:00-23:59
  18. # UTC) are bypassed. Bob is at min_severity=critical so he
  19. # passes. Expected: 1 fcm + 1 telegram for Alice, Bob, Carol
  20. # → 6 deliveries.
  21. # Step 7 — Bob /preferences. Confirm 1 subscription row, channel_mask
  22. # fcm+telegram, min=critical.
  23. # Step 8 — faketgmd /admin/sent should have 3 telegram messages
  24. # (one per Alice/Bob/Carol from step 6 plus 2 from step 5).
  25. #
  26. # Exit code is the number of failed checks.
  27. set -e
  28. cd "$(dirname "$0")/.."
  29. SECRET=s3cret-acme
  30. INGEST=http://localhost:8800/v1/ingest
  31. TELEGRAM_BOT_TOKEN=fake-tg-bot-token-acme-001
  32. FAKETGMD=http://localhost:8830
  33. PG="docker exec -i broad-announce-postgres-1 psql -U ba -d ba -A -t"
  34. send() {
  35. # send <body> <label> [var_out]
  36. # Echoes the response; if var_out is set, stores the alert_id in it.
  37. local body="$1" label="$2" var_out="$3"
  38. local ts=$(date +%s)
  39. local sig=$(printf '%s.%s' "$ts" "$body" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
  40. local resp
  41. resp=$(curl -s -X POST "$INGEST" \
  42. -H "Content-Type: application/json" \
  43. -H "X-BA-Signature: t=$ts,v1=$sig" \
  44. --data "$body")
  45. echo "[$label] $resp"
  46. if [[ -n "$var_out" ]]; then
  47. # alert_id is the first JSON string of the form "00000..."
  48. local aid
  49. aid=$(printf '%s' "$resp" | python3 -c 'import sys,json,re; s=sys.stdin.read(); m=re.search(r"\"alert_id\":\"([0-9a-f]+)\"", s); print(m.group(1) if m else "")')
  50. printf -v "$var_out" '%s' "$aid"
  51. fi
  52. }
  53. queue() {
  54. local text="$1" user_id="$2" chat_id="$3"
  55. curl -s -X POST "$FAKETGMD/admin/queue" \
  56. -H "Content-Type: application/json" \
  57. --data "$(cat <<JSON
  58. {"bot_token":"$TELEGRAM_BOT_TOKEN","user_id":$user_id,"chat_id":$chat_id,"text":"$text","first_name":"Bob SRE"}
  59. JSON
  60. )"
  61. echo
  62. }
  63. fails=0
  64. pass() { echo " ✅ $*"; }
  65. fail() { echo " ❌ $*"; fails=$((fails+1)); }
  66. echo "═══════════════════════════════════════════════════════"
  67. echo " M3 smoke test — broad-announce"
  68. echo "═══════════════════════════════════════════════════════"
  69. echo
  70. echo "▸ Pre-flight: reset state and restart telegramd so its long-poll offset clears"
  71. # Reset DB to M3 baseline: Bob unlinked, Bob subscription min=critical.
  72. $PG -c "UPDATE individuals SET telegram_chat_id = NULL, telegram_user_id = NULL, telegram_invite_code = 'acme-bob-002' WHERE id = 'ind-acme-002';" >/dev/null
  73. $PG -c "UPDATE individuals SET telegram_chat_id = NULL, telegram_user_id = NULL WHERE id = 'ind-acme-003';" >/dev/null
  74. $PG -c "UPDATE subscriptions SET min_severity = 'critical' WHERE individual_id = 'ind-acme-002' AND source_id = 'prom-prod';" >/dev/null
  75. $PG -c "TRUNCATE deliveries;" >/dev/null
  76. # Reset faketgmd's queue + sent (and its in-memory per-bot offset).
  77. curl -sS -X POST "$FAKETGMD/admin/reset" >/dev/null
  78. # Restart telegramd so its in-process long-poll offset goes back to 0.
  79. # (In production, Telegram's getUpdates tracks the offset server-side;
  80. # faketgmd is a fake that needs the bot to be re-started for clean state.)
  81. docker compose restart telegramd >/dev/null
  82. # Wait for telegramd to come up and load its bot list.
  83. for i in 1 2 3 4 5 6 7 8 9 10; do
  84. if curl -sS http://localhost:8822/health 2>/dev/null | grep -q '"status":"ok"'; then
  85. sleep 1 # let it issue the first getUpdates long-poll
  86. break
  87. fi
  88. sleep 1
  89. done
  90. echo
  91. echo "▸ Pre-flight: state"
  92. echo " individuals:"
  93. $PG -c "SELECT id, telegram_chat_id, telegram_user_id, telegram_invite_code FROM individuals ORDER BY id;" | sed 's/^/ /'
  94. echo " subscriptions:"
  95. $PG -c "SELECT individual_id, min_severity, channel_mask, status FROM subscriptions WHERE source_id='prom-prod' ORDER BY individual_id;" | sed 's/^/ /'
  96. echo " deliveries (should be 0):"
  97. $PG -c "SELECT COUNT(*) FROM deliveries;" | sed 's/^/ /'
  98. echo " faketgmd sent (should be 0):"
  99. curl -sS "$FAKETGMD/admin/sent" | sed 's/^/ /'
  100. echo
  101. # Helper: count rows in deliveries for a specific alert_id.
  102. rows_for() {
  103. $PG -c "SELECT COUNT(*) FROM deliveries WHERE alert_id = '$1';"
  104. }
  105. rows_total() { $PG -c "SELECT COUNT(*) FROM deliveries;"; }
  106. rows_channel() { $PG -c "SELECT COUNT(*) FROM deliveries WHERE channel='$1' AND status='sent';"; }
  107. rows_channel_total() { $PG -c "SELECT COUNT(*) FROM deliveries WHERE channel='$1';"; }
  108. # ── Step 2: warning → db-prod-04 → Alice only ─────────────────
  109. echo "▸ Step 2: POST warning (Alice only; Bob unlinked; Carol quiet hours)"
  110. send '{"company_id":"acme-001","source_id":"prom-prod","severity":"warning","category":"storage","title":"Disk 80% full","body":"db-prod-04","data":{"host":"db-prod-04"},"dedupe_key":"m3-s2"}' "step 2" aid2
  111. sleep 4
  112. n=$(rows_for "$aid2")
  113. [[ "$n" == "2" ]] && pass "2 deliveries for step-2 alert (1 fcm + 1 telegram for Alice)" || fail "expected 2 deliveries for step 2, got $n"
  114. fb=$(rows_channel fcm)
  115. tb=$(rows_channel telegram)
  116. [[ "$fb" == "1" && "$tb" == "1" ]] && pass "running totals: fcm=1, telegram=1" || fail "fcm=$fb, telegram=$tb (expected 1,1)"
  117. # ── Step 3: critical → db-prod-04 → Alice + Bob (fcm only) ─────
  118. echo
  119. echo "▸ Step 3: POST critical (Bob unlinked, telegram skipped for him)"
  120. send '{"company_id":"acme-001","source_id":"prom-prod","severity":"critical","category":"storage","title":"Disk full","body":"db-prod-04","data":{"host":"db-prod-04"},"dedupe_key":"m3-s3"}' "step 3" aid3
  121. sleep 4
  122. n=$(rows_for "$aid3")
  123. [[ "$n" == "3" ]] && pass "3 deliveries for step-3 alert (Alice fcm+telegram, Bob fcm only)" || fail "expected 3 deliveries for step 3, got $n"
  124. fb=$(rows_channel fcm)
  125. tb=$(rows_channel telegram)
  126. [[ "$fb" == "3" && "$tb" == "2" ]] && pass "running totals: fcm=3, telegram=2 (Bob telegram blocked by missing link, Alice has 1+1)" || fail "fcm=$fb, telegram=$tb (expected 3,2)"
  127. # ── Step 4: Bob /start acme-bob-002 via faketgmd queue ─────────
  128. echo
  129. echo "▸ Step 4: Bob runs /start acme-bob-002"
  130. queue "/start acme-bob-002" 900002 2002
  131. sleep 4
  132. linked=$($PG -c "SELECT telegram_user_id FROM individuals WHERE id = 'ind-acme-002';")
  133. [[ "$linked" == "900002" ]] && pass "Bob linked (telegram_user_id=900002)" || fail "Bob telegram_user_id='$linked' (expected 900002)"
  134. bot_reply=$(curl -sS "$FAKETGMD/admin/sent" | grep -c '"text":"Linked' || true)
  135. [[ "$bot_reply" -ge 1 ]] && pass "faketgmd saw bot's 'Linked' reply" || fail "no 'Linked' reply from bot"
  136. # ── Step 5: critical → db-prod-04 again; Bob telegram should now fire ─
  137. echo
  138. echo "▸ Step 5: POST critical (Bob now linked, telegram should fire for him)"
  139. send '{"company_id":"acme-001","source_id":"prom-prod","severity":"critical","category":"storage","title":"Disk full","body":"db-prod-04","data":{"host":"db-prod-04"},"dedupe_key":"m3-s5"}' "step 5" aid5
  140. sleep 4
  141. n=$(rows_for "$aid5")
  142. [[ "$n" == "4" ]] && pass "4 deliveries for step-5 alert (2 fcm + 2 telegram for Alice + Bob)" || fail "expected 4 deliveries for step 5, got $n"
  143. fb=$(rows_channel fcm)
  144. tb=$(rows_channel telegram)
  145. [[ "$fb" == "5" && "$tb" == "4" ]] && pass "running totals: fcm=5, telegram=4 (Bob now receives telegram)" || fail "fcm=$fb, telegram=$tb (expected 5,4)"
  146. # ── Step 6: inminent_colapse → all three via fcm, but Carol unlinked (no telegram) ─
  147. echo
  148. echo "▸ Step 6: POST inminent_colapse (Carol's quiet hours bypassed → fcm, but Carol unlinked → no telegram)"
  149. send '{"company_id":"acme-001","source_id":"prom-prod","severity":"inminent_colapse","category":"power","title":"PDU overload imminent","body":"rack-B","data":{"host":"rack-b"},"dedupe_key":"m3-s6"}' "step 6" aid6
  150. sleep 4
  151. n=$(rows_for "$aid6")
  152. [[ "$n" == "5" ]] && pass "5 deliveries for step-6 alert (3 fcm + 2 telegram: Alice, Bob fcm+telegram; Carol fcm only — never linked)" || fail "expected 5 deliveries for step 6, got $n"
  153. fb=$(rows_channel fcm)
  154. tb=$(rows_channel telegram)
  155. [[ "$fb" == "8" && "$tb" == "6" ]] && pass "running totals: fcm=8, telegram=6" || fail "fcm=$fb, telegram=$tb (expected 8,6)"
  156. # ── Step 7: Bob /preferences ────────────────────────────────────
  157. echo
  158. echo "▸ Step 7: Bob runs /preferences"
  159. queue "/preferences" 900002 2002
  160. sleep 3
  161. prefs=$(curl -sS "$FAKETGMD/admin/sent" | grep -c '"text":"Your subscriptions' || true)
  162. [[ "$prefs" -ge 1 ]] && pass "faketgmd saw bot's preferences reply" || fail "no preferences reply from bot"
  163. # ── Step 8: faketgmd sent count ─────────────────────────────────
  164. echo
  165. echo "▸ Step 8: faketgmd totals"
  166. sent=$(curl -sS "$FAKETGMD/admin/sent" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["count"])')
  167. [[ "$sent" -ge 5 ]] && pass "faketgmd received $sent sendMessage calls (>=5 expected: 1 alice/step2 + 1 alice/step3 + 1 linked-reply + 1 alice/step5 + 1 bob/step5 + 3 step6 + 1 prefs-reply = 8)" || fail "faketgmd sent count=$sent, expected >=5"
  168. echo
  169. echo "═══════════════════════════════════════════════════════"
  170. echo " Per-individual / per-channel delivery table"
  171. echo "═══════════════════════════════════════════════════════"
  172. $PG -c "SELECT individual_id, channel, status, COUNT(*) AS n FROM deliveries GROUP BY 1,2,3 ORDER BY 1,2;" | sed 's/^/ /'
  173. echo
  174. if [[ "$fails" -eq 0 ]]; then
  175. echo "✅ M3 smoke: 0 failures"
  176. exit 0
  177. else
  178. echo "❌ M3 smoke: $fails failure(s)"
  179. exit $fails
  180. fi