m13b_w3_smoke.sh 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. #!/usr/bin/env bash
  2. # m13b_w3_smoke.sh — End-to-end smoke for the M13b W3 telegram bot CRUD.
  3. #
  4. # Walks through:
  5. # 1. authd /health
  6. # 2. login (super_admin) → access + refresh
  7. # 3. POST /v1/tenants (create a tenant to host a bot)
  8. # 4. GET /v1/tenants/{id}/telegram/bots (initially empty)
  9. # 5. POST /v1/tenants/{id}/telegram/bots (create with bot_token)
  10. # 6. GET /v1/tenants/{id}/telegram/bots/{bid} (verify bot_token_set=true)
  11. # 7. GET /v1/tenants/{id}/telegram/bots (list has 1)
  12. # 8. PATCH /v1/tenants/{id}/telegram/bots/{bid} (change welcome_message)
  13. # 9. POST .../status (pause)
  14. # 10. POST .../status (activate)
  15. # 11. POST .../rotate-token (new token; bot_token_set still true)
  16. # 12. POST (duplicate id) → 409
  17. # 13. POST (bad id) → 400
  18. # 14. POST (bad token) → 400
  19. # 15. GET (no token field in response)
  20. # 16. tenant_admin tries telegram endpoints → 403 (super_admin only)
  21. # 17. Cleanup: archive the tenant
  22. #
  23. # Requires:
  24. # - authd running on $BA_AUTHD_HTTP (default http://127.0.0.1:8804)
  25. # - $BA_AUTHD_JWT_SECRET set
  26. # - super_admin user in Postgres (scripts/bootstrap-super-admin.sh)
  27. # - 012_telegram_bot_fields migration applied
  28. #
  29. # Run:
  30. # bash scripts/m13b_w3_smoke.sh
  31. #
  32. # Exits 0 if all steps pass.
  33. set -euo pipefail
  34. cd "$(dirname "$0")/.."
  35. AUTHD="${BA_AUTHD_HTTP:-http://127.0.0.1:8804}"
  36. SUPER_EMAIL="${BA_SMOKE_SUPER_EMAIL:-super@broad-announce.test}"
  37. SUPER_PASSWORD="${BA_SMOKE_SUPER_PASSWORD:-test-password-123}"
  38. DSN="${BA_POSTGRES_DSN:-${PG_DSN:-postgres://ba:ba@localhost:5432/ba?sslmode=disable}}"
  39. PASS=0
  40. FAIL=0
  41. RESULTS=()
  42. TENANT_SLUG="smoke-tg-$(date +%s)"
  43. TENANT_EMAIL="ops-${TENANT_SLUG}@smoke.test"
  44. TENANT_ADMIN_EMAIL="admin-${TENANT_SLUG}@smoke.test"
  45. TENANT_ADMIN_PASSWORD="smoke-test-password-1234"
  46. BOT_ID="primary"
  47. BOT_TOKEN="12345678:$(python3 -c "import secrets; print(secrets.token_hex(18)[:35])")"
  48. ROTATED_TOKEN="12345678:$(python3 -c "import secrets; print(secrets.token_hex(18)[:35])")"
  49. check() {
  50. local name="$1"
  51. local actual="$2"
  52. local want="$3"
  53. if [[ "$actual" == "$want" ]]; then
  54. PASS=$((PASS+1))
  55. RESULTS+=("OK $name")
  56. else
  57. FAIL=$((FAIL+1))
  58. RESULTS+=("FAIL $name (got $actual, want $want)")
  59. fi
  60. }
  61. # JSON helper: extract a top-level field as string.
  62. json_field() {
  63. python3 -c "import json,sys; d=json.load(sys.stdin); k='$2'.split('.'); v=d
  64. for kk in k:
  65. v=v[kk] if isinstance(v,dict) else v[int(kk)]
  66. print(v if not isinstance(v,(list,dict,bool)) else json.dumps(v))" <<<"$1"
  67. }
  68. # -------------------------------------------------------------------
  69. # 1. health
  70. # -------------------------------------------------------------------
  71. status=$(curl -s -o /dev/null -w "%{http_code}" "$AUTHD/health")
  72. check "1. authd /health" "$status" "200"
  73. # -------------------------------------------------------------------
  74. # 2. login (super_admin)
  75. # -------------------------------------------------------------------
  76. login_body=$(curl -s -X POST "$AUTHD/v1/auth/login" \
  77. -H 'Content-Type: application/json' \
  78. -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
  79. SUPER_TOKEN=$(json_field "$login_body" access_token)
  80. if [[ -z "$SUPER_TOKEN" ]]; then
  81. echo "FATAL: super_admin login failed: $login_body"
  82. exit 1
  83. fi
  84. check "2. super_admin login" "200" "200"
  85. # -------------------------------------------------------------------
  86. # 3. POST /v1/tenants (create a tenant to host a bot)
  87. # -------------------------------------------------------------------
  88. create=$(curl -s -w "\n%{http_code}" -X POST "$AUTHD/v1/tenants" \
  89. -H "Authorization: Bearer $SUPER_TOKEN" \
  90. -H 'Content-Type: application/json' \
  91. -d "{\"slug\":\"$TENANT_SLUG\",\"display_name\":\"Smoke Telegram Tenant\",\"contact_email\":\"$TENANT_EMAIL\",\"rate_limit_per_sec\":5000,\"fcm_shared\":true}")
  92. create_code=$(echo "$create" | tail -1)
  93. create_body=$(echo "$create" | head -n -1)
  94. TENANT_ID=$(json_field "$create_body" id)
  95. check "3. POST /v1/tenants" "$create_code" "201"
  96. [[ -n "$TENANT_ID" ]] || { echo "FATAL: no tenant id"; exit 1; }
  97. echo " new tenant: $TENANT_ID"
  98. # -------------------------------------------------------------------
  99. # 4. GET /v1/tenants/{id}/telegram/bots (initially empty)
  100. # -------------------------------------------------------------------
  101. list=$(curl -s -H "Authorization: Bearer $SUPER_TOKEN" "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots?limit=10")
  102. total=$(json_field "$list" total)
  103. check "4. GET /v1/tenants/{id}/telegram/bots (empty)" "$total" "0"
  104. # -------------------------------------------------------------------
  105. # 5. POST /v1/tenants/{id}/telegram/bots
  106. # -------------------------------------------------------------------
  107. create=$(curl -s -w "\n%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots" \
  108. -H "Authorization: Bearer $SUPER_TOKEN" \
  109. -H 'Content-Type: application/json' \
  110. -d "{\"id\":\"$BOT_ID\",\"name\":\"Acme Ops Bot\",\"bot_token\":\"$BOT_TOKEN\",\"welcome_message\":\"Welcome!\",\"description\":\"smoke test\"}")
  111. create_code=$(echo "$create" | tail -1)
  112. create_body=$(echo "$create" | head -n -1)
  113. if [[ "$create_code" != "201" ]]; then
  114. echo "FATAL: create bot failed ($create_code): $create_body"
  115. exit 1
  116. fi
  117. BOT_ID_BACK=$(json_field "$create_body" id)
  118. BOT_TOKEN_SET=$(json_field "$create_body" bot_token_set)
  119. WELCOME=$(json_field "$create_body" welcome_message)
  120. check "5. POST /v1/tenants/{id}/telegram/bots" "$create_code" "201"
  121. check "5b. response id == $BOT_ID" "$BOT_ID_BACK" "$BOT_ID"
  122. check "5c. response bot_token_set == true" "$BOT_TOKEN_SET" "true"
  123. check "5d. response welcome_message" "$WELCOME" "Welcome!"
  124. # 5e. response MUST NOT include the plaintext bot_token.
  125. if echo "$create_body" | grep -q '"bot_token"'; then
  126. check "5e. response does NOT contain bot_token" "present" "absent"
  127. else
  128. check "5e. response does NOT contain bot_token" "absent" "absent"
  129. fi
  130. # -------------------------------------------------------------------
  131. # 6. GET /v1/tenants/{id}/telegram/bots/{bid}
  132. # -------------------------------------------------------------------
  133. detail=$(curl -s -H "Authorization: Bearer $SUPER_TOKEN" "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots/$BOT_ID")
  134. detail_id=$(json_field "$detail" id)
  135. detail_status=$(json_field "$detail" status)
  136. detail_token_set=$(json_field "$detail" bot_token_set)
  137. check "6. GET bot id" "$detail_id" "$BOT_ID"
  138. check "6b. status == active" "$detail_status" "active"
  139. check "6c. bot_token_set == true" "$detail_token_set" "true"
  140. # 6d. detail MUST NOT include bot_token.
  141. if echo "$detail" | grep -q '"bot_token"'; then
  142. check "6d. detail does NOT contain bot_token" "present" "absent"
  143. else
  144. check "6d. detail does NOT contain bot_token" "absent" "absent"
  145. fi
  146. # -------------------------------------------------------------------
  147. # 7. GET /v1/tenants/{id}/telegram/bots (list has 1)
  148. # -------------------------------------------------------------------
  149. list=$(curl -s -H "Authorization: Bearer $SUPER_TOKEN" "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots?limit=10")
  150. total=$(json_field "$list" total)
  151. check "7. GET telegram/bots (count)" "$total" "1"
  152. # -------------------------------------------------------------------
  153. # 8. PATCH /v1/tenants/{id}/telegram/bots/{bid}
  154. # -------------------------------------------------------------------
  155. patch=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots/$BOT_ID" \
  156. -H "Authorization: Bearer $SUPER_TOKEN" \
  157. -H 'Content-Type: application/json' \
  158. -d '{"welcome_message":"Welcome to Acme!","default_source_id":"primary"}')
  159. check "8. PATCH telegram/bots/{bid}" "$patch" "200"
  160. detail=$(curl -s -H "Authorization: Bearer $SUPER_TOKEN" "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots/$BOT_ID")
  161. detail_welcome=$(json_field "$detail" welcome_message)
  162. detail_default=$(json_field "$detail" default_source_id)
  163. check "8b. PATCH welcome_message persisted" "$detail_welcome" "Welcome to Acme!"
  164. check "8c. PATCH default_source_id persisted" "$detail_default" "primary"
  165. # -------------------------------------------------------------------
  166. # 9. POST .../status pause
  167. # -------------------------------------------------------------------
  168. pause=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots/$BOT_ID/status" \
  169. -H "Authorization: Bearer $SUPER_TOKEN" \
  170. -H 'Content-Type: application/json' \
  171. -d '{"status":"paused"}')
  172. check "9. POST .../status pause" "$pause" "200"
  173. detail=$(curl -s -H "Authorization: Bearer $SUPER_TOKEN" "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots/$BOT_ID")
  174. detail_status=$(json_field "$detail" status)
  175. check "9b. status == paused" "$detail_status" "paused"
  176. # -------------------------------------------------------------------
  177. # 10. POST .../status activate
  178. # -------------------------------------------------------------------
  179. act=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots/$BOT_ID/status" \
  180. -H "Authorization: Bearer $SUPER_TOKEN" \
  181. -H 'Content-Type: application/json' \
  182. -d '{"status":"active"}')
  183. check "10. POST .../status activate" "$act" "200"
  184. # -------------------------------------------------------------------
  185. # 11. POST .../rotate-token
  186. # -------------------------------------------------------------------
  187. rot=$(curl -s -w "\n%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots/$BOT_ID/rotate-token" \
  188. -H "Authorization: Bearer $SUPER_TOKEN" \
  189. -H 'Content-Type: application/json' \
  190. -d "{\"bot_token\":\"$ROTATED_TOKEN\"}")
  191. rot_code=$(echo "$rot" | tail -1)
  192. rot_body=$(echo "$rot" | head -n -1)
  193. check "11. POST .../rotate-token" "$rot_code" "200"
  194. rot_token_set=$(json_field "$rot_body" bot_token_set)
  195. rot_last=$(json_field "$rot_body" last_rotated_at)
  196. check "11b. rotated bot_token_set == true" "$rot_token_set" "true"
  197. if [[ -n "$rot_last" && "$rot_last" != "—" ]]; then
  198. check "11c. last_rotated_at populated" "yes" "yes"
  199. else
  200. check "11c. last_rotated_at populated" "$rot_last" "yes"
  201. fi
  202. # 11d. rotated response MUST NOT contain the new bot_token.
  203. if echo "$rot_body" | grep -q "$ROTATED_TOKEN"; then
  204. check "11d. rotate response does NOT echo the token" "present" "absent"
  205. else
  206. check "11d. rotate response does NOT echo the token" "absent" "absent"
  207. fi
  208. # -------------------------------------------------------------------
  209. # 12. POST duplicate id → 409
  210. # -------------------------------------------------------------------
  211. dup=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots" \
  212. -H "Authorization: Bearer $SUPER_TOKEN" \
  213. -H 'Content-Type: application/json' \
  214. -d "{\"id\":\"$BOT_ID\",\"name\":\"Dup\",\"bot_token\":\"$BOT_TOKEN\"}")
  215. check "12. POST telegram/bots (dup id) \u2192 409" "$dup" "409"
  216. # -------------------------------------------------------------------
  217. # 13. POST bad id → 400
  218. # -------------------------------------------------------------------
  219. bad=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots" \
  220. -H "Authorization: Bearer $SUPER_TOKEN" \
  221. -H 'Content-Type: application/json' \
  222. -d "{\"id\":\"Bad ID!\",\"name\":\"x\",\"bot_token\":\"$BOT_TOKEN\"}")
  223. check "13. POST telegram/bots (bad id) \u2192 400" "$bad" "400"
  224. # -------------------------------------------------------------------
  225. # 14. POST bad token → 400
  226. # -------------------------------------------------------------------
  227. bad=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots" \
  228. -H "Authorization: Bearer $SUPER_TOKEN" \
  229. -H 'Content-Type: application/json' \
  230. -d '{"id":"secondary","name":"x","bot_token":"not-a-token"}')
  231. check "14. POST telegram/bots (bad token) \u2192 400" "$bad" "400"
  232. # -------------------------------------------------------------------
  233. # 15. tenant_admin (created below) is FORBIDDEN on telegram endpoints
  234. # -------------------------------------------------------------------
  235. export PGPASSWORD="$(echo "$DSN" | sed -E 's|.*://[^:]+:([^@]+)@.*|\1|')"
  236. HASH=$(python3 -c "
  237. import bcrypt
  238. print(bcrypt.hashpw(b'${TENANT_ADMIN_PASSWORD}', bcrypt.gensalt(rounds=10)).decode())
  239. ")
  240. psql "$DSN" -v ON_ERROR_STOP=0 -X -q -c "
  241. INSERT INTO auth.users (tenant_id, email, role, status, password_hash)
  242. SELECT id, '${TENANT_ADMIN_EMAIL}', 'tenant_admin', 'active', '${HASH}'
  243. FROM auth.tenants WHERE slug = '${TENANT_SLUG}'
  244. ON CONFLICT (email, tenant_id) WHERE tenant_id IS NOT NULL DO UPDATE SET password_hash = EXCLUDED.password_hash, status = 'active';
  245. " >/dev/null
  246. ta_login=$(curl -s -X POST "$AUTHD/v1/auth/login" \
  247. -H 'Content-Type: application/json' \
  248. -d "{\"email\":\"$TENANT_ADMIN_EMAIL\",\"password\":\"$TENANT_ADMIN_PASSWORD\"}")
  249. TA_TOKEN=$(json_field "$ta_login" access_token)
  250. if [[ -z "$TA_TOKEN" ]]; then
  251. echo "FATAL: tenant_admin login failed: $ta_login"
  252. exit 1
  253. fi
  254. check "15. tenant_admin login" "200" "200"
  255. ta_list=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TA_TOKEN" "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots")
  256. check "15a. tenant_admin GET telegram/bots \u2192 403" "$ta_list" "403"
  257. ta_post=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots" \
  258. -H "Authorization: Bearer $TA_TOKEN" \
  259. -H 'Content-Type: application/json' \
  260. -d "{\"id\":\"other\",\"name\":\"x\",\"bot_token\":\"$BOT_TOKEN\"}")
  261. check "15b. tenant_admin POST telegram/bots \u2192 403" "$ta_post" "403"
  262. ta_get=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TA_TOKEN" "$AUTHD/v1/tenants/$TENANT_ID/telegram/bots/$BOT_ID")
  263. check "15c. tenant_admin GET telegram/bots/{bid} \u2192 403" "$ta_get" "403"
  264. # -------------------------------------------------------------------
  265. # 16. Cleanup: archive the tenant
  266. # -------------------------------------------------------------------
  267. arc=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/status" \
  268. -H "Authorization: Bearer $SUPER_TOKEN" \
  269. -H 'Content-Type: application/json' \
  270. -d '{"status":"archived"}')
  271. check "16. cleanup: archive tenant" "$arc" "200"
  272. # -------------------------------------------------------------------
  273. # Summary
  274. # -------------------------------------------------------------------
  275. echo
  276. for r in "${RESULTS[@]}"; do echo " $r"; done
  277. echo
  278. echo "PASS=$PASS FAIL=$FAIL"
  279. if [[ $FAIL -gt 0 ]]; then
  280. exit 1
  281. fi
  282. exit 0