UI-UX/scripts/e2e-vote-flow.sh
iye 10878ddb3f
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
feat(vote): 重构投票模型为终身 12 票 + 每艺人 1 票
前端:
- store 改为 votedArtists[] + zustand persist
- VoteModal 删除 1/3/5/ALL 选择器,改三态(待投/已投/满额)
- 卡片/排行/详情页加 hasVoted 状态 + ✓ 角标
- Hero 右上角 Countdown 替换为 HeroVoteProgress(12 格点亮进度)
- /me 改为终身额度叙事(QuotaCard / StatsGrid / MyFanSupport)

后端:
- votes 表加 @@unique([userId, artistId])(已 apply 到生产 RDS)
- /api/vote 重写:12 票上限 + P2002 ALREADY_VOTED + P2003 NOT_FOUND 兜底
- /api/me 新增 votedArtists[] + voteQuota,移除 dailyQuota
- 新增 ERR.ALREADY_VOTED 错误码

测试:
- DB 层 5/5 + E2E 18/18 通过(scripts/e2e-vote-flow.sh)
- 修复 P2003 FK 违反未识别的 bug

详情见 docs/todo/voting-refactor-完成报告.md 与 voting-refactor-backend-完成报告.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 20:14:57 +08:00

148 lines
5.4 KiB
Bash

#!/usr/bin/env bash
# 端到端回归测试:走完整 next-auth OTP 登录 → /api/me → /api/vote 4 种路径
#
# 测试用户:全新手机号 13800138000(测试结束 cleanup-test-user.mjs 删除)
# dev OTP 万能码:123456
# 实际命中 route handler,不绕过任何中间件。
set -uo pipefail
BASE="http://localhost:3000"
PHONE="13800138000"
CODE="123456"
COOKIES=$(mktemp)
trap 'rm -f "$COOKIES"' EXIT
green() { echo -e "\033[32m$1\033[0m"; }
red() { echo -e "\033[31m$1\033[0m"; }
yel() { echo -e "\033[33m$1\033[0m"; }
# 累计通过 / 失败
PASS=0
FAIL=0
assert_eq() {
local name="$1" expected="$2" actual="$3"
if [[ "$actual" == "$expected" ]]; then
PASS=$((PASS+1)); green " [PASS] $name"
else
FAIL=$((FAIL+1)); red " [FAIL] $name -- expected=$expected actual=$actual"
fi
}
assert_contains() {
local name="$1" needle="$2" haystack="$3"
if echo "$haystack" | grep -q "$needle"; then
PASS=$((PASS+1)); green " [PASS] $name"
else
FAIL=$((FAIL+1)); red " [FAIL] $name -- did not find '$needle' in: $haystack"
fi
}
echo "=== 端到端回归测试 ==="
echo "测试用户 phone=$PHONE"
echo ""
# ===== 0. cleanup 任何上次残留的测试数据(测试幂等) =====
yel "[0] cleanup 上次测试残留"
node scripts/cleanup-test-user.mjs "$PHONE" 2>&1 | sed 's/^/ /'
# ===== 1. 取 CSRF token =====
yel "[1] 获取 CSRF token"
CSRF_RAW=$(curl -s -c "$COOKIES" "$BASE/api/auth/csrf")
CSRF=$(echo "$CSRF_RAW" | sed -n 's/.*"csrfToken":"\([^"]*\)".*/\1/p')
if [[ -z "$CSRF" ]]; then
red "无法获取 csrfToken,响应:$CSRF_RAW"
exit 1
fi
green " csrfToken=${CSRF:0:20}..."
# ===== 2. OTP 登录(Credentials provider 路径 /api/auth/callback/phone-otp) =====
yel "[2] OTP 登录 phone=$PHONE code=$CODE"
LOGIN_HTTP=$(curl -s -b "$COOKIES" -c "$COOKIES" -o /tmp/login.body -w "%{http_code}" \
-X POST "$BASE/api/auth/callback/phone-otp" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "phone=$PHONE" \
--data-urlencode "code=$CODE" \
--data-urlencode "csrfToken=$CSRF" \
--data-urlencode "redirect=false" \
--data-urlencode "json=true")
echo " HTTP $LOGIN_HTTP"
# ===== 3. 验证 session 已建立 =====
yel "[3] 验证 session"
SESSION=$(curl -s -b "$COOKIES" "$BASE/api/auth/session")
echo " session: $SESSION"
assert_contains "session 包含 user.id" '"id"' "$SESSION"
# ===== 4. GET /api/me 验返回结构 =====
yel "[4] GET /api/me 验证新字段"
ME=$(curl -s -b "$COOKIES" "$BASE/api/me")
echo " body 摘要: $(echo "$ME" | head -c 300)..."
assert_contains "/api/me 返回 ok:true" '"ok":true' "$ME"
assert_contains "/api/me 含 voteQuota 字段" '"voteQuota"' "$ME"
assert_contains "/api/me voteQuota.total=12" '"total":12' "$ME"
assert_contains "/api/me 含 votedArtists 字段" '"votedArtists"' "$ME"
if echo "$ME" | grep -q '"dailyQuota"'; then
FAIL=$((FAIL+1)); red " [FAIL] /api/me 仍含 dailyQuota 字段(应已被 voteQuota 替换)"
else
PASS=$((PASS+1)); green " [PASS] /api/me 不再含旧 dailyQuota 字段"
fi
# ===== 5. POST /api/vote 投未投过的艺人(预期成功) =====
TEST_ARTIST="001"
yel "[5] POST /api/vote artistId=$TEST_ARTIST(首次投,预期 200)"
V1=$(curl -s -b "$COOKIES" -X POST "$BASE/api/vote" \
-H "Content-Type: application/json" \
-d "{\"artistId\":\"$TEST_ARTIST\"}")
echo " body: $V1"
assert_contains "首投返回 ok:true" '"ok":true' "$V1"
assert_contains "首投返回 totalQuota:12" '"totalQuota":12' "$V1"
assert_contains "首投返回 votedCount=1" '"votedCount":1' "$V1"
assert_contains "首投返回 remaining=11" '"remaining":11' "$V1"
# ===== 6. POST /api/vote 同一艺人(预期 409 ALREADY_VOTED) =====
yel "[6] POST /api/vote artistId=$TEST_ARTIST(重投,预期 409 ALREADY_VOTED)"
V2=$(curl -s -b "$COOKIES" -X POST "$BASE/api/vote" \
-H "Content-Type: application/json" \
-d "{\"artistId\":\"$TEST_ARTIST\"}")
echo " body: $V2"
assert_contains "重投返回 ok:false" '"ok":false' "$V2"
assert_contains "重投返回 ALREADY_VOTED" '"code":"ALREADY_VOTED"' "$V2"
# ===== 7. POST /api/vote 不存在的艺人(预期 404 NOT_FOUND) =====
yel "[7] POST /api/vote artistId=999(不存在,预期 404)"
V3=$(curl -s -b "$COOKIES" -X POST "$BASE/api/vote" \
-H "Content-Type: application/json" \
-d '{"artistId":"999"}')
echo " body: $V3"
assert_contains "无效艺人返回 ok:false" '"ok":false' "$V3"
assert_contains "无效艺人返回 NOT_FOUND" '"code":"NOT_FOUND"' "$V3"
# ===== 8. 再次 GET /api/me 验 votedArtists 含新投艺人 =====
yel "[8] GET /api/me 复查 votedArtists"
ME2=$(curl -s -b "$COOKIES" "$BASE/api/me")
assert_contains "复查 votedArtists 含 $TEST_ARTIST" "\"$TEST_ARTIST\"" "$ME2"
assert_contains "复查 voteQuota.used=1" '"used":1' "$ME2"
assert_contains "复查 voteQuota.remaining=11" '"remaining":11' "$ME2"
# ===== 9. 验证未登录 → 401 =====
yel "[9] 未登录调 /api/vote(预期 401)"
V_NOAUTH=$(curl -s -X POST "$BASE/api/vote" \
-H "Content-Type: application/json" \
-d '{"artistId":"001"}')
assert_contains "无 session 返回 UNAUTHORIZED" '"code":"UNAUTHORIZED"' "$V_NOAUTH"
# ===== 10. 最终 cleanup =====
yel "[10] 测试结束 cleanup"
node scripts/cleanup-test-user.mjs "$PHONE" 2>&1 | sed 's/^/ /'
# ===== 汇总 =====
echo ""
echo "=== 汇总 ==="
echo "通过 $PASS · 失败 $FAIL"
if [[ $FAIL -gt 0 ]]; then
red "回归测试有失败,详见上方"
exit 1
else
green "全部通过"
fi