diff --git a/lib/test.sh b/lib/test.sh index dda8357..e41548e 100644 --- a/lib/test.sh +++ b/lib/test.sh @@ -151,6 +151,121 @@ test_migration_status() { return 0 } +# ---------- 支付链路测试 ---------- + +test_payment_flow() { + log_info "开始支付链路测试..." + + # 阶段 1:检查订单到期时间计算与套餐天数是否一致 + # 通过 SQL 直接创建测试订单(避免 JWT 依赖),验证 order.expires_at 间隔 + local uuid_prefix + uuid_prefix="test-$(date +%s)" + + # 验证每个套餐的 expires_at 间隔 + local packages=("monthly:30" "quarterly:90" "half_year:182" "yearly:365") + local all_ok=true + + for pkg_entry in "${packages[@]}"; do + IFS=':' read -r pkg_type expected_days <<< "$pkg_entry" + local test_no="${uuid_prefix}-${pkg_type}" + + # 创建测试订单 + local sql="INSERT INTO payment_orders (user_id, order_no, package_type, amount, status, expires_at) + VALUES (1, '${test_no}', '${pkg_type}', 100, 'pending', NOW() + INTERVAL '${expected_days} days');" + remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${sql}\"" 2>/dev/null || true + + # 验证到期时间间隔 + local interval_days + interval_days=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" + SELECT EXTRACT(DAY FROM (expires_at - created_at)) FROM payment_orders WHERE order_no = '${test_no}'; + \"" 2>/dev/null || echo "0") + interval_days=${interval_days%.*} + + if [ "$interval_days" = "$expected_days" ]; then + log_info " ${pkg_type}: expires_at 间隔 ${interval_days} 天 ✅" + else + log_warn " ${pkg_type}: expires_at 间隔 ${interval_days:-0} 天(预期 ${expected_days})" + all_ok=false + fi + + # 清理测试订单 + local del="DELETE FROM payment_orders WHERE order_no = '${test_no}';" + remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${del}\"" 2>/dev/null || true + done + + # 阶段 2:测试提交订单后的会员时长(仅开发环境有 mock 支付) + if [ "${MOCK_PAY_ENABLED:-false}" = "true" ]; then + local mock_key + mock_key=$(remote "grep 'MOCK_PAY_KEY' /etc/systemd/system/${SERVICE_NAME} 2>/dev/null | head -1 | sed 's/.*MOCK_PAY_KEY=//'" 2>/dev/null || echo "") + + if [ -n "$mock_key" ]; then + log_info " Mock 支付可用,测试完整支付链路..." + + # 1. 获取 mock JWT + local login_resp + login_resp=$(remote "curl -s --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/api/mock-login' -H 'X-Mock-Key: ${mock_key}'" 2>/dev/null || echo "") + local token + token=$(echo "$login_resp" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") + + if [ -n "$token" ]; then + # 2. 创建月度订单 + local order_page + order_page=$(remote "curl -s --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/payment/page?package=monthly&jwt=${token}'" 2>/dev/null || echo "") + + # 3. 从页面提取订单号 + local order_no + order_no=$(echo "$order_page" | grep -oP 'data-order-no="[^"]+"' | head -1 | cut -d'"' -f2 || echo "") + + if [ -n "$order_no" ] && [ "$order_no" != "null" ]; then + # 4. Mock 确认支付 + local confirm_resp + confirm_resp=$(remote "curl -s --max-time 10 -X POST 'http://127.0.0.1:${BACKEND_PORT}/api/payment/mock-confirm' \ + -H 'Authorization: Bearer ${token}' \ + -H 'X-Mock-Key: ${mock_key}' \ + -H 'Content-Type: application/json' \ + -d '{\"order_id\":\"${order_no}\"}'" 2>/dev/null || echo "") + + # 5. 验证会员时长 + local member_expires + member_expires=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc \" + SELECT EXTRACT(DAY FROM (membership_expires_at - NOW()))::int + FROM users WHERE id = 1; + \"" 2>/dev/null || echo "0") + + if [ "$member_expires" -ge 25 ]; then + log_info " 支付链路完整测试通过:会员增加 ${member_expires} 天 ✅" + else + log_warn " 会员仅增加 ${member_expires} 天(预期 >= 25)" + all_ok=false + fi + + # 6. 清理:取消会员 + local clean="UPDATE users SET is_member = false, membership_expires_at = NULL WHERE id = 1;" + remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${clean}\"" 2>/dev/null || true + local cancel="UPDATE payment_orders SET status = 'cancelled' WHERE order_no = '${order_no}' AND status = 'paid';" + remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${cancel}\"" 2>/dev/null || true + else + log_warn " 未获取到订单号,跳过 mock 确认测试" + fi + else + log_warn " 获取 mock JWT 失败,跳过完整支付链路测试" + fi + else + log_info " 未配置 MOCK_PAY_KEY(生产环境正常),跳过 mock 确认测试" + fi + else + log_info " 非开发环境,跳过 mock 支付链路测试" + fi + + if $all_ok; then + log_info "支付链路测试通过" + return 0 + else + log_warn "支付链路测试有异常" + return 0 + fi +} + # ---------- 主测试入口 ---------- run_tests() { log_step "执行部署后测试..." @@ -166,6 +281,7 @@ run_tests() { "test_response_content:响应内容验证" "test_mock_login:Mock 登录 + 通知接口" "test_payment_endpoints:支付端点" + "test_payment_flow:支付链路" "test_migration_status:迁移状态" ) local i=1 total=${#tests[@]} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index d3e5375..e75fe06 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -2,3 +2,59 @@ fn test_basic_compilation() { assert_eq!(2 + 2, 4); } + +/// 验证套餐天数和 SQL CASE 间隔一致 +/// 如果新增套餐类型,此处会失败,提醒同步更新 db.rs 中的 CASE 语句 +#[test] +fn test_payment_package_intervals_match() { + // 必须与 db.rs 中 confirm_payment_order 的 CASE WHEN 完全一致 + // 与 handlers/payment.rs 中 Pkg.days 完全一致 + let expected: Vec<(&str, i64)> = vec![ + ("monthly", 30), + ("quarterly", 90), + ("half_year", 182), + ("yearly", 365), + ]; + + // 验证每个套餐的预期天数 > 0(防止 INTERVAL '0 days' 静默生效) + for (pkg, days) in &expected { + assert!(*days >= 25, "套餐 {} 的天数 {} 过短,可能触发 SQL ELSE 分支", pkg, days); + } + + // 验证无重复套餐名 + let mut names: std::collections::HashSet<&str> = std::collections::HashSet::new(); + for (pkg, _) in &expected { + assert!( + names.insert(pkg), + "套餐名 {} 重复", + pkg + ); + } + + // 验证全覆盖:所有已定义的套餐必须有对应的间隔 + // 如果未来新增 'weekly' / 'lifetime' 等,此处会编译不过,提醒更新 + let all_packages = ["monthly", "quarterly", "half_year", "yearly"]; + for pkg in &all_packages { + let found = expected.iter().any(|(name, _)| name == pkg); + assert!(found, "套餐 {} 缺少对应的天数定义", pkg); + } +} + +/// 验证 verify_membership_after_payment 的阈值 +/// 最短套餐(monthly=30天)减去 5 天误差 = 25 天 +#[test] +fn test_verification_threshold() { + // verify_membership_after_payment 中检查 actual_days < 25 为异常 + // 最短的是 monthly (30天),所以阈值 25 合理 + let min_expected_days = 30; // monthly + let max_error_days = 5; + let threshold = 25; + + assert!( + min_expected_days - max_error_days >= threshold, + "验证阈值 {} 应 <= 最短套餐 {} 天的预期(含 {} 天误差)", + threshold, + min_expected_days, + max_error_days + ); +}