#!/usr/bin/env bash # P9 smoke — runbook §4 (16 nginx routes + 4 manual checks 17-20). # # Runs the existing P8.1 nginx route smoke, then layers in the four # manual checks the runbook says must pass at T+0+48min: # 17. login (POST /api/auth/login → 200 + Set-Cookie session) # 18. RSC fetch (/anime/154587 → episodeTitles in HTML) # 19. subscription (POST /api/subscriptions → 200, with session) # 20. danmaku degraded (GET /api/danmaku// → 200) # # Usage: # ./scripts/p9-smoke.sh # https://localhost # ./scripts/p9-smoke.sh --target=https://animegoclub.com # # Env (required for check 17/19): # TEST_EMAIL=... prod-safe known account # TEST_PASSWORD=... for that account # # Exit codes: # 0 — all 20 checks passed # 1 — at least one failed (count printed at end) set -euo pipefail # --- log file ---------------------------------------------------------- TS="$(date +%Y%m%d-%H%M%S)" LOG_FILE="/tmp/p9-smoke-${TS}.log" # --- color helpers ---------------------------------------------------- if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then C_GREEN="$(tput setaf 2)" C_RED="$(tput setaf 1)" C_YELLOW="$(tput setaf 3)" C_BOLD="$(tput bold)" C_RESET="$(tput sgr0)" else C_GREEN=""; C_RED=""; C_YELLOW=""; C_BOLD=""; C_RESET="" fi ok() { printf "%s[OK]%s %s\n" "$C_GREEN" "$C_RESET" "$1"; } warn() { printf "%s[WARN]%s %s\n" "$C_YELLOW" "$C_RESET" "$1"; } fail() { printf "%s[FAIL]%s %s\n" "$C_RED" "$C_RESET" "$1"; } info() { printf "%s[..]%s %s\n" "$C_BOLD" "$C_RESET" "$1"; } exec > >(tee -a "$LOG_FILE") 2>&1 info "logging to $LOG_FILE" # --- args -------------------------------------------------------------- TARGET="https://localhost" for arg in "$@"; do case "$arg" in --target=*) TARGET="${arg#--target=}" ;; --help|-h) printf "usage: %s [--target=]\n" "$0" exit 0 ;; *) fail "unknown arg: $arg"; exit 2 ;; esac done info "target: $TARGET" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PASS_COUNT=0 FAIL_COUNT=0 record_pass() { PASS_COUNT=$((PASS_COUNT + 1)); } record_fail() { FAIL_COUNT=$((FAIL_COUNT + 1)); } # ====================================================================== # CHECK 1-16 — nginx routes via existing smoke # ====================================================================== info "CHECKS 1-16/20 — nginx routes (smoke-p8.1.sh)" SMOKE="$REPO_ROOT/scripts/smoke-p8.1.sh" if [ ! -f "$SMOKE" ]; then fail "missing $SMOKE" exit 2 fi info "\$ bash $SMOKE $TARGET" SMOKE_RC=0 bash "$SMOKE" "$TARGET" || SMOKE_RC=$? if [ "$SMOKE_RC" -eq 0 ]; then ok "smoke-p8.1 PASS (16/16)" PASS_COUNT=$((PASS_COUNT + 16)) else fail "smoke-p8.1 had $SMOKE_RC failures (of 16)" FAIL_COUNT=$((FAIL_COUNT + SMOKE_RC)) PASS_COUNT=$((PASS_COUNT + 16 - SMOKE_RC)) fi # ====================================================================== # CHECK 17 — login: POST /api/auth/login, expect 200 + Set-Cookie # ====================================================================== info "CHECK 17/20 — POST /api/auth/login" if [ -z "${TEST_EMAIL:-}" ] || [ -z "${TEST_PASSWORD:-}" ]; then fail "TEST_EMAIL / TEST_PASSWORD env vars are required for check 17" record_fail SESSION_COOKIE_JAR="" else SESSION_COOKIE_JAR="/tmp/p9-smoke-cookies-${TS}.txt" rm -f "$SESSION_COOKIE_JAR" # Login payload: Express controller (server/controllers/auth.controller.js:87) # reads {email, password} from req.body. Go-api parity preserved. LOGIN_PAYLOAD="$(printf '{"email":"%s","password":"%s"}' "$TEST_EMAIL" "$TEST_PASSWORD")" info "\$ curl -sk -c $SESSION_COOKIE_JAR -X POST $TARGET/api/auth/login ..." LOGIN_BODY="/tmp/p9-smoke-login-${TS}.json" LOGIN_HEAD="/tmp/p9-smoke-login-${TS}.head" LOGIN_CODE="$(curl -sk \ -c "$SESSION_COOKIE_JAR" \ -o "$LOGIN_BODY" \ -D "$LOGIN_HEAD" \ -w "%{http_code}" \ -X POST \ -H "Content-Type: application/json" \ --data "$LOGIN_PAYLOAD" \ "${TARGET}/api/auth/login" || echo "000")" if [ "$LOGIN_CODE" = "200" ] && grep -qi "^set-cookie:" "$LOGIN_HEAD"; then ok "login 200 + Set-Cookie present" record_pass else fail "login failed (HTTP $LOGIN_CODE, Set-Cookie=$(grep -ic '^set-cookie:' "$LOGIN_HEAD" || echo 0))" warn "response body (first 400 chars):" head -c 400 "$LOGIN_BODY" || true printf "\n" record_fail fi fi # ====================================================================== # CHECK 18 — RSC fetch: GET /anime/154587, look for episode titles # ====================================================================== info "CHECK 18/20 — GET /anime/154587 (RSC, expect episodeTitles)" RSC_BODY="/tmp/p9-smoke-rsc-${TS}.html" RSC_CODE="$(curl -sk -o "$RSC_BODY" -w "%{http_code}" "${TARGET}/anime/154587" || echo "000")" if [ "$RSC_CODE" != "200" ]; then fail "RSC fetch returned HTTP $RSC_CODE" record_fail else # episodeTitles ships in the RSC payload — confirmed at # next-app/src/app/anime/[id]/page.tsx:1112. Backup signal: # rendered episode markup. If both miss, RSC fetch is broken. if grep -q "episodeTitles" "$RSC_BODY"; then ok "RSC contains episodeTitles" record_pass elif grep -qE '"episode"|第.*[话集]' "$RSC_BODY"; then ok "RSC contains rendered episode markup (no episodeTitles literal)" record_pass else fail "RSC missing episodeTitles + episode markup" warn "first 400 chars of body:" head -c 400 "$RSC_BODY" || true printf "\n" record_fail fi fi # ====================================================================== # CHECK 19 — subscription: POST /api/subscriptions, expect 200 # ====================================================================== info "CHECK 19/20 — POST /api/subscriptions (with session)" if [ -z "${SESSION_COOKIE_JAR}" ] || [ ! -s "$SESSION_COOKIE_JAR" ]; then fail "no session cookie from check 17; skipping subscription check" record_fail else # Subscription payload: go-api/internal/subscriptions/types.go:28 # createSubscriptionReq = {anilistId int32 >=1, status oneof # watching|completed|plan_to_watch|dropped}. Express validator # rules match. SUB_PAYLOAD='{"anilistId":154587,"status":"watching"}' SUB_BODY="/tmp/p9-smoke-sub-${TS}.json" info "\$ curl -sk -b $SESSION_COOKIE_JAR -X POST $TARGET/api/subscriptions -d '$SUB_PAYLOAD'" SUB_CODE="$(curl -sk \ -b "$SESSION_COOKIE_JAR" \ -o "$SUB_BODY" \ -w "%{http_code}" \ -X POST \ -H "Content-Type: application/json" \ --data "$SUB_PAYLOAD" \ "${TARGET}/api/subscriptions" || echo "000")" if [ "$SUB_CODE" = "200" ] || [ "$SUB_CODE" = "201" ]; then ok "subscription returned $SUB_CODE" record_pass else fail "subscription returned HTTP $SUB_CODE" warn "response body (first 400 chars):" head -c 400 "$SUB_BODY" || true printf "\n" record_fail fi fi # ====================================================================== # CHECK 20 — danmaku degraded: GET /api/danmaku//, expect 200 # ====================================================================== info "CHECK 20/20 — GET /api/danmaku/154587/1 (degraded http only)" # go-api/internal/danmaku/handlers.go:7 — GET /api/danmaku/:anilistId/:episode # (public, no auth). Real-time send/receive is socket.io and only # verifiable in a browser; this is the HTTP read-side smoke only. DM_BODY="/tmp/p9-smoke-dm-${TS}.json" DM_CODE="$(curl -sk -o "$DM_BODY" -w "%{http_code}" \ "${TARGET}/api/danmaku/154587/1" || echo "000")" if [ "$DM_CODE" = "200" ]; then ok "danmaku endpoint returned 200" record_pass else fail "danmaku endpoint returned HTTP $DM_CODE" warn "response body (first 200 chars):" head -c 200 "$DM_BODY" || true printf "\n" record_fail fi # ====================================================================== # SUMMARY # ====================================================================== printf "\n%s========== SUMMARY ==========%s\n" "$C_BOLD" "$C_RESET" printf "passed: %d\n" "$PASS_COUNT" printf "failed: %d\n" "$FAIL_COUNT" # cleanup transient bodies (keep cookie jar so operator can replay) rm -f /tmp/p9-smoke-login-"${TS}".json /tmp/p9-smoke-login-"${TS}".head \ /tmp/p9-smoke-rsc-"${TS}".html /tmp/p9-smoke-sub-"${TS}".json \ /tmp/p9-smoke-dm-"${TS}".json if [ "$FAIL_COUNT" -eq 0 ]; then ok "P9 SMOKE: PASS (20/20)" info "log: $LOG_FILE" exit 0 else fail "P9 SMOKE: FAIL — $FAIL_COUNT of 20 checks failed" info "log: $LOG_FILE" exit 1 fi