m13b_w1_smoke.sh 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. #!/usr/bin/env bash
  2. # m13b_w1_smoke.sh — End-to-end smoke for the M13b W1 tenant CRUD.
  3. #
  4. # Walks through:
  5. # 1. authd /health and /metrics
  6. # 2. login (super_admin) → access + refresh
  7. # 3. GET /v1/tenants (initially empty or has prior smoke data)
  8. # 4. POST /v1/tenants (create) — super_admin OK
  9. # 5. GET /v1/tenants/{id} (the one we just created)
  10. # 6. PATCH /v1/tenants/{id} (change display_name + rate_limit_per_sec)
  11. # 7. POST /v1/tenants/{id}/status (suspend)
  12. # 8. POST /v1/tenants/{id}/status (activate)
  13. # 9. POST /v1/tenants (duplicate slug) → 409
  14. # 10. POST /v1/tenants (bad slug) → 400
  15. # 11. Login as tenant_admin of the new tenant → can GET own
  16. # 12. tenant_admin trying to GET another tenant's id → 403
  17. # 13. tenant_admin trying to POST /v1/tenants → 403
  18. # 14. POST /v1/tenants/{id}/status as tenant_admin → 403
  19. # 15. POST /v1/tenants/{id}/status with bad status → 400
  20. # 16. (cleanup) super_admin archives the new tenant
  21. #
  22. # Requires:
  23. # - authd running on $BA_AUTHD_HTTP (default http://127.0.0.1:8804)
  24. # - $BA_AUTHD_JWT_SECRET set
  25. # - super_admin user in Postgres (scripts/bootstrap-super-admin.sh)
  26. #
  27. # Run:
  28. # bash scripts/m13b_w1_smoke.sh
  29. #
  30. # Exits 0 if all steps pass.
  31. set -euo pipefail
  32. cd "$(dirname "$0")/.."
  33. AUTHD="${BA_AUTHD_HTTP:-http://127.0.0.1:8804}"
  34. SUPER_EMAIL="${BA_SMOKE_SUPER_EMAIL:-super@broad-announce.test}"
  35. SUPER_PASSWORD="${BA_SMOKE_SUPER_PASSWORD:-test-password-123}"
  36. # We need a tenant_admin in the new tenant for steps 11-14. The
  37. # simplest path: after creating the tenant, invite one via
  38. # /v1/users/invite (super_admin), grab the magic token from the
  39. # DB, and use it to set the password. This is exactly the flow
  40. # that scripts/m13a_smoke.sh avoids (because it just checks the
  41. # route is wired), but here we need a real user. v1.1 should
  42. # expose a /v1/users/bootstrap-tenant-admin endpoint to make this
  43. # easier; for now we go through the SQL path.
  44. DSN="${BA_POSTGRES_DSN:-${PG_DSN:-postgres://ba:ba@localhost:5432/ba?sslmode=disable}}"
  45. PASS=0
  46. FAIL=0
  47. RESULTS=()
  48. TENANT_SLUG="smoke-$(date +%s)"
  49. TENANT_EMAIL="ops-${TENANT_SLUG}@smoke.test"
  50. TENANT_ADMIN_EMAIL="admin-${TENANT_SLUG}@smoke.test"
  51. TENANT_ADMIN_PASSWORD="smoke-test-password-1234"
  52. check() {
  53. local name="$1"
  54. local actual="$2"
  55. local want="$3"
  56. if [[ "$actual" == "$want" ]]; then
  57. PASS=$((PASS+1))
  58. RESULTS+=("OK $name")
  59. else
  60. FAIL=$((FAIL+1))
  61. RESULTS+=("FAIL $name (got $actual, want $want)")
  62. fi
  63. }
  64. # JSON helper: jq-less extract of a top-level string field.
  65. # Usage: json_field body field
  66. json_field() {
  67. echo "$1" | python3 -c "import json,sys; d=json.load(sys.stdin); k='$2'.split('.'); v=d
  68. for kk in k:
  69. v=v[kk] if isinstance(v,dict) else v[int(kk)]
  70. print(v if not isinstance(v,(list,dict,bool)) else json.dumps(v))"
  71. }
  72. # -------------------------------------------------------------------
  73. # 1. health
  74. # -------------------------------------------------------------------
  75. status=$(curl -s -o /dev/null -w "%{http_code}" "$AUTHD/health")
  76. check "1. authd /health" "$status" "200"
  77. # -------------------------------------------------------------------
  78. # 2. login (super_admin)
  79. # -------------------------------------------------------------------
  80. login_body=$(curl -s -X POST "$AUTHD/v1/auth/login" \
  81. -H 'Content-Type: application/json' \
  82. -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
  83. SUPER_TOKEN=$(json_field "$login_body" access_token)
  84. if [[ -z "$SUPER_TOKEN" ]]; then
  85. echo "FATAL: super_admin login failed: $login_body"
  86. exit 1
  87. fi
  88. check "2. super_admin login" "200" "200"
  89. # -------------------------------------------------------------------
  90. # 3. GET /v1/tenants (initial list)
  91. # -------------------------------------------------------------------
  92. list=$(curl -s -H "Authorization: Bearer $SUPER_TOKEN" "$AUTHD/v1/tenants?limit=10")
  93. total=$(json_field "$list" total)
  94. echo " initial tenant count: $total"
  95. check "3. GET /v1/tenants" "200" "200"
  96. # -------------------------------------------------------------------
  97. # 4. POST /v1/tenants (create)
  98. # -------------------------------------------------------------------
  99. create=$(curl -s -w "\n%{http_code}" -X POST "$AUTHD/v1/tenants" \
  100. -H "Authorization: Bearer $SUPER_TOKEN" \
  101. -H 'Content-Type: application/json' \
  102. -d "{\"slug\":\"$TENANT_SLUG\",\"display_name\":\"Smoke Tenant\",\"contact_email\":\"$TENANT_EMAIL\",\"rate_limit_per_sec\":5000,\"fcm_shared\":true}")
  103. create_code=$(echo "$create" | tail -1)
  104. create_body=$(echo "$create" | head -n -1)
  105. TENANT_ID=$(json_field "$create_body" id)
  106. check "4. POST /v1/tenants" "$create_code" "201"
  107. [[ -n "$TENANT_ID" ]] || { echo "FATAL: no tenant id"; exit 1; }
  108. echo " new tenant: $TENANT_ID"
  109. # -------------------------------------------------------------------
  110. # 5. GET /v1/tenants/{id}
  111. # -------------------------------------------------------------------
  112. get=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $SUPER_TOKEN" "$AUTHD/v1/tenants/$TENANT_ID")
  113. check "5. GET /v1/tenants/{id}" "$get" "200"
  114. # -------------------------------------------------------------------
  115. # 6. PATCH /v1/tenants/{id}
  116. # -------------------------------------------------------------------
  117. patch=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH "$AUTHD/v1/tenants/$TENANT_ID" \
  118. -H "Authorization: Bearer $SUPER_TOKEN" \
  119. -H 'Content-Type: application/json' \
  120. -d '{"display_name":"Smoke Tenant (edited)","rate_limit_per_sec":7500}')
  121. check "6. PATCH /v1/tenants/{id}" "$patch" "200"
  122. # -------------------------------------------------------------------
  123. # 7. POST /v1/tenants/{id}/status suspend
  124. # -------------------------------------------------------------------
  125. sus=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/status" \
  126. -H "Authorization: Bearer $SUPER_TOKEN" \
  127. -H 'Content-Type: application/json' \
  128. -d '{"status":"suspended"}')
  129. check "7. POST /v1/tenants/{id}/status suspend" "$sus" "200"
  130. # -------------------------------------------------------------------
  131. # 8. POST /v1/tenants/{id}/status activate
  132. # -------------------------------------------------------------------
  133. act=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/status" \
  134. -H "Authorization: Bearer $SUPER_TOKEN" \
  135. -H 'Content-Type: application/json' \
  136. -d '{"status":"active"}')
  137. check "8. POST /v1/tenants/{id}/status activate" "$act" "200"
  138. # -------------------------------------------------------------------
  139. # 9. POST /v1/tenants (duplicate slug) → 409
  140. # -------------------------------------------------------------------
  141. dup=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants" \
  142. -H "Authorization: Bearer $SUPER_TOKEN" \
  143. -H 'Content-Type: application/json' \
  144. -d "{\"slug\":\"$TENANT_SLUG\",\"display_name\":\"Dup\",\"contact_email\":\"$TENANT_EMAIL\"}")
  145. check "9. POST /v1/tenants (dup slug)" "$dup" "409"
  146. # -------------------------------------------------------------------
  147. # 10. POST /v1/tenants (bad slug) → 400
  148. # -------------------------------------------------------------------
  149. bad=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants" \
  150. -H "Authorization: Bearer $SUPER_TOKEN" \
  151. -H 'Content-Type: application/json' \
  152. -d '{"slug":"Bad Slug!","display_name":"x","contact_email":"a@b.c"}')
  153. check "10. POST /v1/tenants (bad slug)" "$bad" "400"
  154. # -------------------------------------------------------------------
  155. # 11. Bootstrap a tenant_admin in the new tenant.
  156. # This goes through the SQL path because we don't have an
  157. # open invite-magic endpoint in v1.
  158. # -------------------------------------------------------------------
  159. export PGPASSWORD="$(echo "$DSN" | sed -E 's|.*://[^:]+:([^@]+)@.*|\1|')"
  160. HASH=$(python3 -c "
  161. import bcrypt
  162. print(bcrypt.hashpw(b'${TENANT_ADMIN_PASSWORD}', bcrypt.gensalt(rounds=10)).decode())
  163. ")
  164. psql "$DSN" -v ON_ERROR_STOP=0 -X -q -c "
  165. INSERT INTO auth.users (tenant_id, email, role, status, password_hash)
  166. SELECT id, '${TENANT_ADMIN_EMAIL}', 'tenant_admin', 'active', '${HASH}'
  167. FROM auth.tenants WHERE slug = '${TENANT_SLUG}'
  168. ON CONFLICT (email, tenant_id) WHERE tenant_id IS NOT NULL DO UPDATE SET password_hash = EXCLUDED.password_hash, status = 'active';
  169. " >/dev/null
  170. ta_login=$(curl -s -X POST "$AUTHD/v1/auth/login" \
  171. -H 'Content-Type: application/json' \
  172. -d "{\"email\":\"$TENANT_ADMIN_EMAIL\",\"password\":\"$TENANT_ADMIN_PASSWORD\"}")
  173. TA_TOKEN=$(json_field "$ta_login" access_token)
  174. if [[ -z "$TA_TOKEN" ]]; then
  175. echo "FATAL: tenant_admin login failed: $ta_login"
  176. exit 1
  177. fi
  178. check "11. tenant_admin login (own tenant)" "200" "200"
  179. # 11b. tenant_admin can GET own tenant
  180. ta_get=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TA_TOKEN" "$AUTHD/v1/tenants/$TENANT_ID")
  181. check "11b. tenant_admin GET own tenant" "$ta_get" "200"
  182. # 11c. tenant_admin can PATCH own display_name (restricted fields)
  183. ta_patch=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH "$AUTHD/v1/tenants/$TENANT_ID" \
  184. -H "Authorization: Bearer $TA_TOKEN" \
  185. -H 'Content-Type: application/json' \
  186. -d '{"display_name":"Smoke (tenant-admin edited)"}')
  187. check "11c. tenant_admin PATCH own display_name" "$ta_patch" "200"
  188. # 11d. tenant_admin CANNOT change rate_limit_per_sec (restricted)
  189. ta_rl=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH "$AUTHD/v1/tenants/$TENANT_ID" \
  190. -H "Authorization: Bearer $TA_TOKEN" \
  191. -H 'Content-Type: application/json' \
  192. -d '{"rate_limit_per_sec":1234}')
  193. check "11d. tenant_admin PATCH own rate_limit_per_sec (forbidden)" "$ta_rl" "400"
  194. # -------------------------------------------------------------------
  195. # 12. tenant_admin trying to GET another tenant's id → 403
  196. # (use a random uuid that won't exist; canAccessTenant fails
  197. # before the DB lookup, so we get 403 not 404)
  198. # -------------------------------------------------------------------
  199. fake_id="00000000-0000-0000-0000-000000000000"
  200. ta_other=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TA_TOKEN" "$AUTHD/v1/tenants/$fake_id")
  201. check "12. tenant_admin GET other tenant" "$ta_other" "403"
  202. # -------------------------------------------------------------------
  203. # 13. tenant_admin trying to POST /v1/tenants → 403
  204. # -------------------------------------------------------------------
  205. ta_create=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants" \
  206. -H "Authorization: Bearer $TA_TOKEN" \
  207. -H 'Content-Type: application/json' \
  208. -d '{"slug":"x","display_name":"x","contact_email":"a@b.c"}')
  209. check "13. tenant_admin POST /v1/tenants" "$ta_create" "403"
  210. # -------------------------------------------------------------------
  211. # 14. tenant_admin trying to POST /v1/tenants/{id}/status → 403
  212. # -------------------------------------------------------------------
  213. ta_st=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/status" \
  214. -H "Authorization: Bearer $TA_TOKEN" \
  215. -H 'Content-Type: application/json' \
  216. -d '{"status":"suspended"}')
  217. check "14. tenant_admin POST status" "$ta_st" "403"
  218. # -------------------------------------------------------------------
  219. # 15. POST /v1/tenants/{id}/status with bad status → 400
  220. # -------------------------------------------------------------------
  221. bad_st=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/status" \
  222. -H "Authorization: Bearer $SUPER_TOKEN" \
  223. -H 'Content-Type: application/json' \
  224. -d '{"status":"paused"}')
  225. check "15. POST status (bad value)" "$bad_st" "400"
  226. # -------------------------------------------------------------------
  227. # 16. cleanup: super_admin archives the new tenant
  228. # -------------------------------------------------------------------
  229. arc=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/tenants/$TENANT_ID/status" \
  230. -H "Authorization: Bearer $SUPER_TOKEN" \
  231. -H 'Content-Type: application/json' \
  232. -d '{"status":"archived"}')
  233. check "16. POST status archived (cleanup)" "$arc" "200"
  234. # -------------------------------------------------------------------
  235. # summary
  236. # -------------------------------------------------------------------
  237. echo
  238. echo "=== M13b W1 smoke results ==="
  239. printf '%s\n' "${RESULTS[@]}"
  240. echo
  241. echo "Passed: $PASS Failed: $FAIL"
  242. [[ $FAIL -eq 0 ]] || exit 1