Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
前端: - 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>
148 lines
5.4 KiB
Bash
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
|