#!/usr/bin/env bash # # Sprint 21: post-deploy smoke на проде. # # Запускается СРАЗУ после prod-deploy.sh. 10 ключевых сценариев на # https://admin.food-market.kz через временные тестовые credentials # (создаются через signup → удаляются в конце). # # Каждый шаг — отдельный pass/fail. Итог отправляется Telegram'ом # (если задан FM_TG_TOKEN+FM_TG_CHAT). Exit code = кол-во провалов. # # Защита от мусора: после прогона создаваемая org остаётся в БД (delete # через API ещё не реализовано) — но email содержит метку `smoke-` и # timestamp, поэтому видно по логам/audit что это. # # Usage: # deploy/post-deploy-smoke.sh [--dry-run] [--url https://admin.food-market.kz] set -uo pipefail PROD_URL="${PROD_URL:-https://admin.food-market.kz}" DRY_RUN=0 while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=1; shift ;; --url) PROD_URL="$2"; shift 2 ;; --help|-h) grep -E '^#( |$)' "$0" | sed 's/^# \?//'; exit 0 ;; *) shift ;; esac done log() { echo "[$(date -Iseconds)] $*"; } notify_telegram() { local msg="$1" local tok="${FM_TG_TOKEN:-}"; local chat="${FM_TG_CHAT:-}" if [[ -z "$tok" || -z "$chat" ]]; then log "(Telegram disabled — no FM_TG_TOKEN/CHAT)"; return fi curl -sS -X POST "https://api.telegram.org/bot$tok/sendMessage" \ --data-urlencode "chat_id=$chat" \ --data-urlencode "text=$msg" > /dev/null || true } if [[ $DRY_RUN -eq 1 ]]; then log "DRY-RUN: показал бы сценарии без запуска API-вызовов" for s in "signup" "login" "/api/me" "list-products" "create-product" \ "list-counterparties" "list-stores" "list-stock" "delete-product" "logout"; do log " → $s" done exit 0 fi PASS=0 FAIL=0 FAILED_STEPS=() step() { local name="$1" status="$2" detail="$3" if [[ "$status" == "OK" ]]; then log "[✓] $name — $detail" ((PASS+=1)) else log "[✗] $name — $detail" FAILED_STEPS+=("$name: $detail") ((FAIL+=1)) fi } TS=$(date +%s) EMAIL="smoke-${TS}@food-market.local" PASS_TEST='Smoke12345!' ORG_NAME="SmokeOrg-${TS}" # ── 1. signup ──────────────────────────────────────────────────────── RESP=$(curl -fsS -X POST -H "Content-Type: application/json" \ -d "{\"organizationName\":\"$ORG_NAME\",\"email\":\"$EMAIL\",\"password\":\"$PASS_TEST\",\"phone\":\"+77001234567\"}" \ "$PROD_URL/api/auth/signup" 2>/dev/null || echo "") if echo "$RESP" | grep -q '"organizationId"\|"id"'; then step "01-signup" "OK" "$EMAIL" else step "01-signup" "FAIL" "resp: ${RESP:-}" fi # ── 2. login ───────────────────────────────────────────────────────── TOK=$(curl -fsS -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=password" \ --data-urlencode "username=$EMAIL" \ --data-urlencode "password=$PASS_TEST" \ --data-urlencode "client_id=food-market-web" \ --data-urlencode "scope=openid profile email roles api offline_access" \ "$PROD_URL/connect/token" 2>/dev/null \ | python3 -c 'import sys,json; print(json.load(sys.stdin).get("access_token",""))' 2>/dev/null || echo "") if [[ -n "$TOK" ]]; then step "02-login" "OK" "token=${TOK:0:24}…" else step "02-login" "FAIL" "no access_token" # Без токена остальное не запустить — сообщаем и выходим. notify_telegram "🚨 post-deploy-smoke FAIL: login failed на $PROD_URL" exit 1 fi auth() { curl -fsS -H "Authorization: Bearer $TOK" "$@"; } # Извлечь поле .items[0].id из JSON через python (надёжнее grep'a — JSON # может содержать другие "id":"..." (organizationId, parentId etc.)) first_item_id() { python3 -c 'import sys,json; d=json.load(sys.stdin); print((d.get("items") or [{}])[0].get("id",""))' 2>/dev/null || true; } # Поиск первого элемента items[] с условием key=value. find_item_id() { python3 -c " import sys,json d=json.load(sys.stdin) key,val='$1','$2' for it in (d.get('items') or []): if it.get(key) == True if val=='true' else it.get(key) == val: print(it.get('id','')) break " 2>/dev/null || true } # ── 3. /api/me ─────────────────────────────────────────────────────── ME=$(auth "$PROD_URL/api/me" 2>/dev/null || echo "") if echo "$ME" | grep -q "\"email\":\"$EMAIL\""; then step "03-me" "OK" "$EMAIL" else step "03-me" "FAIL" "resp: ${ME:0:120}" fi # ── 4. list products ───────────────────────────────────────────────── PRODS=$(auth "$PROD_URL/api/catalog/products?pageSize=10" 2>/dev/null || echo "") if echo "$PRODS" | grep -q '"total"'; then step "04-list-products" "OK" "$(echo "$PRODS" | grep -oE '"total":[0-9]+' | head -1)" else step "04-list-products" "FAIL" "${PRODS:0:120}" fi # ── 5. create product ──────────────────────────────────────────────── ROOT_ID=$(auth "$PROD_URL/api/catalog/product-groups" 2>/dev/null | first_item_id) UNIT_ID=$(auth "$PROD_URL/api/catalog/units-of-measure" 2>/dev/null | first_item_id) PT_ID=$(auth "$PROD_URL/api/catalog/price-types" 2>/dev/null | first_item_id) # Currencies endpoint иногда возвращает массив напрямую — python обработает оба. CURS_RAW=$(auth "$PROD_URL/api/catalog/currencies" 2>/dev/null) CUR_ID=$(echo "$CURS_RAW" | python3 -c 'import sys,json d=json.load(sys.stdin) items=d if isinstance(d,list) else (d.get("items") or []) print((items or [{}])[0].get("id",""))' 2>/dev/null || true) if [[ -z "$ROOT_ID" || -z "$UNIT_ID" || -z "$PT_ID" || -z "$CUR_ID" ]]; then step "05-create-product" "FAIL" "missing refs: root=$ROOT_ID unit=$UNIT_ID pt=$PT_ID cur=$CUR_ID" else BC="SMOKE-$TS" PROD=$(auth -X POST -H "Content-Type: application/json" \ -d "{\"name\":\"smoke-product-$TS\",\"unitOfMeasureId\":\"$UNIT_ID\",\"productGroupId\":\"$ROOT_ID\",\"vat\":0,\"vatEnabled\":true,\"barcodes\":[{\"code\":\"$BC\",\"type\":0,\"isPrimary\":true}],\"prices\":[{\"priceTypeId\":\"$PT_ID\",\"amount\":100,\"currencyId\":\"$CUR_ID\"}]}" \ "$PROD_URL/api/catalog/products" 2>/dev/null || echo "") PID=$(echo "$PROD" | grep -oE '"id":"[a-f0-9-]+"' | head -1 | cut -d'"' -f4) if [[ -n "$PID" ]]; then step "05-create-product" "OK" "$PID" else step "05-create-product" "FAIL" "${PROD:0:120}" fi fi # ── 6. list counterparties ────────────────────────────────────────── CP=$(auth "$PROD_URL/api/catalog/counterparties?pageSize=10" 2>/dev/null || echo "") if echo "$CP" | grep -q '"total"'; then step "06-list-counterparties" "OK" "$(echo "$CP" | grep -oE '"total":[0-9]+' | head -1)" else step "06-list-counterparties" "FAIL" "${CP:0:120}" fi # ── 7. list stores ────────────────────────────────────────────────── STR=$(auth "$PROD_URL/api/catalog/stores" 2>/dev/null || echo "") if echo "$STR" | grep -q '"total"'; then step "07-list-stores" "OK" "$(echo "$STR" | grep -oE '"total":[0-9]+' | head -1)" else step "07-list-stores" "FAIL" "${STR:0:120}" fi # ── 8. list stock ─────────────────────────────────────────────────── STK=$(auth "$PROD_URL/api/inventory/stock?pageSize=10" 2>/dev/null || echo "") if echo "$STK" | grep -q '"total"'; then step "08-list-stock" "OK" "$(echo "$STK" | grep -oE '"total":[0-9]+' | head -1)" else step "08-list-stock" "FAIL" "${STK:0:120}" fi # ── 9. delete product ─────────────────────────────────────────────── if [[ -n "${PID:-}" ]]; then DEL=$(auth -X DELETE -o /dev/null -w "%{http_code}" "$PROD_URL/api/catalog/products/$PID" 2>/dev/null || echo "") if [[ "$DEL" == "204" ]]; then step "09-delete-product" "OK" "204" else step "09-delete-product" "FAIL" "code=$DEL" fi else step "09-delete-product" "FAIL" "skipped (no PID)" fi # ── 10. session logout (через /api/me/sessions) ───────────────────── # OpenIddict /connect/revocation в этой конфигурации не включён, поэтому # logout = удаление активной сессии через /api/me/sessions/{id} либо # (если нет sessions API) — проверяем что /api/me ещё работает (sanity). # Берём первый sessionId юзера и пробуем DELETE; если 401/404 — fallback # на простую проверку valid-token. SESS=$(auth "$PROD_URL/api/me/sessions" 2>/dev/null || echo "") SID=$(echo "$SESS" | python3 -c 'import sys,json try: d=json.load(sys.stdin) arr=d if isinstance(d,list) else (d.get("items") or d.get("sessions") or []) print((arr or [{}])[0].get("id","")) except: print("")' 2>/dev/null || true) if [[ -n "$SID" ]]; then DEL=$(auth -X DELETE -o /dev/null -w "%{http_code}" "$PROD_URL/api/me/sessions/$SID" 2>/dev/null || echo "") if [[ "$DEL" == "204" || "$DEL" == "200" ]]; then step "10-logout-session" "OK" "session $SID revoked" else step "10-logout-session" "FAIL" "DELETE code=$DEL" fi else # Fallback: просто sanity-check что токен ещё действителен (full flow OK). PING=$(auth -o /dev/null -w "%{http_code}" "$PROD_URL/api/me" 2>/dev/null || echo "") if [[ "$PING" == "200" ]]; then step "10-token-valid" "OK" "token still alive (no session API)" else step "10-token-valid" "FAIL" "code=$PING" fi fi # ── Итог + notify ──────────────────────────────────────────────────── log log "==> $PASS passed, $FAIL failed" if (( FAIL > 0 )); then MSG="🚨 post-deploy-smoke FAIL ($FAIL/10) на $PROD_URL: $(IFS=,; echo "${FAILED_STEPS[*]}")" notify_telegram "$MSG" exit "$FAIL" fi notify_telegram "✅ post-deploy-smoke OK (10/10) на $PROD_URL" exit 0