# =========================================== # test.sh — 部署后测试(依赖 common.sh 的 remote()) # =========================================== # 测试失败自动回滚 rollback_and_exit() { local reason="$1" log_error "测试失败: ${reason}" local last_known_good last_known_good=$(remote "cat ${REMOTE_DIR}/.last_deployed 2>/dev/null || echo ''") if [ -z "$last_known_good" ]; then log_error "无法回滚: 未找到上一个正常版本"; exit 1 fi rollback_on_failure "$last_known_good" exit 1 } test_basic_connectivity() { # 根据端口判定协议(4433/4434 生产环境使用 HTTPS) local proto="http" if [ "${BACKEND_PORT}" = "4433" ] || [ "${BACKEND_PORT}" = "4434" ]; then proto="https" fi local base_url="${proto}://127.0.0.1:${BACKEND_PORT}" local max_retries=3 retry=0 while [ $retry -lt $max_retries ]; do retry=$((retry+1)); log_info "尝试 ${retry}/${max_retries}: 测试本地后端 (${base_url})" local all=true for test in \ "/health:200" \ "/api/login:400:POST:-H Content-Type:application/json -d '{\"code\":\"test\"}'" \ "/api/refresh-token:401:POST:-H Content-Type:application/json -d '{\"refresh_token\":\"bad\"}'" \ "/:200"; do IFS=':' read -r path expected method extra <<< "$test" [ -z "$method" ] && method="GET" local code; code=$(remote "curl -sk -o /dev/null -w '%{http_code}' --max-time 10 -X ${method} ${base_url}${path} ${extra}" 2>/dev/null || echo "000") if [ "$code" != "$expected" ] && [ "$expected" != "200,400" ] && ! { [ "$expected" = "400,200" ] && { [ "$code" = "400" ] || [ "$code" = "200" ]; }; }; then if { [ "$expected" = "400" ] && [ "$code" = "200" ]; } || { [ "$expected" = "401" ] && [ "$code" = "200" ]; }; then log_info " ${path} 正常 (HTTP ${code})"; continue fi log_warn " ${path} 异常 (HTTP ${code})"; all=false else log_info " ${path} 正常 (HTTP ${code})" fi done $all && { log_info "基础连通性测试通过"; return 0; } [ $retry -lt $max_retries ] && sleep 2 done log_error "基础连通性测试失败"; return 1 } test_database_connection() { local r; r=$(remote "docker exec ${DB_CONTAINER} pg_isready -U ${DB_USER} 2>/dev/null" || echo "failed") [[ "$r" =~ "accepting connections" ]] && log_info " PostgreSQL 连接正常" || { log_warn " PostgreSQL 连接失败"; return 1; } r=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT 1'" 2>/dev/null || echo "failed") [ "$r" = "1" ] && log_info " 数据库查询正常" || { log_warn " 数据库查询失败"; return 1; } r=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT tablename FROM pg_tables WHERE schemaname='\''public'\'' LIMIT 1'" 2>/dev/null || echo "failed") [ -n "$r" ] && log_info " 表存在: ${r}" || { log_warn " 表查询失败"; return 1; } log_info "数据库连接测试通过"; return 0 } test_token_flow() { local login; login=$(remote "curl -sk --max-time 10 -X POST ${base_url}/api/login -H 'Content-Type: application/json' -d '{\"code\":\"test\"}'" 2>/dev/null || echo "") local token; token=$(echo "$login" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") [ -z "$token" ] && log_warn " 未获取到 token(微信 code 无效是正常的)" || log_info " 获取到 token" # 测试受保护端点 local profile; profile=$(remote "curl -sk --max-time 10 ${base_url}/api/user/profile -H 'Authorization: Bearer ${token}'" 2>/dev/null || echo "") echo "$profile" | grep -q '"success":true' && log_info " /api/user/profile JWT 验证成功" || log_warn " /api/user/profile 无权限(正常)" local refresh; refresh=$(remote "curl -sk --max-time 10 -X POST ${base_url}/api/refresh-token -H 'Content-Type: application/json' -d '{\"refresh_token\":\"invalid\"}'" 2>/dev/null || echo "") log_info " refresh_token 端点响应正常" log_info "Token 流程测试通过"; return 0 } test_weather_details_jwt() { local login; login=$(remote "curl -sk --max-time 10 -X POST ${base_url}/api/login -H 'Content-Type: application/json' -d '{\"code\":\"test\"}'" 2>/dev/null || echo "") local token; token=$(echo "$login" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") [ -z "$token" ] && { log_warn " 无 token,跳过 weather/details 测试(微信 code 无效是正常的)"; return 0; } local resp; resp=$(remote "curl -sk --max-time 10 '${base_url}/weather/details?id=1' -H 'Authorization: Bearer ${token}'" 2>/dev/null || echo "") if echo "$resp" | grep -q '"success":true' || echo "$resp" | grep -q "Forbidden"; then log_info " weather/details JWT 验证通过" else log_warn " weather/details 响应异常" fi return 0 } test_invalid_token_401() { local bad="eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjB9.bad" local all=true for path in "/api/user/profile" "/weather/details?id=1" "/api/weather"; do local c; c=$(remote "curl -sk -o /dev/null -w '%{http_code}' --max-time 10 '${base_url}${path}' -H 'Authorization: Bearer ${bad}'" 2>/dev/null || echo "000") [ "$c" = "401" ] && log_info " ${path} 返回 401 ✓" || { log_warn " ${path} 返回 ${c}"; all=false; } done $all && log_info "无效 Token 401 测试通过" && return 0 return 1 } test_response_content() { local r; r=$(remote "curl -sk --max-time 10 ${base_url}/health" 2>/dev/null || echo "") echo "$r" | grep -q '"status"' && echo "$r" | grep -q '"database"' && \ log_info " /health JSON 格式正确 ✓" || { log_warn " /health 响应异常"; return 1; } echo "$r" | grep -q '"database":"connected"' && \ log_info " 数据库连接状态正常 ✓" || { log_warn " 数据库未连接"; return 1; } log_info "响应内容验证通过"; return 0 } # ---------- 新增测试 ---------- test_mock_login() { if [ "${APP_ENV}" != "development" ]; then log_info " mock-login 仅在开发环境中测试" return 0 fi local key; key=$(remote "grep 'MOCK_LOGIN_KEY' /etc/systemd/system/${SERVICE_NAME} 2>/dev/null | head -1 | sed 's/.*MOCK_LOGIN_KEY=//'" 2>/dev/null || echo "") [ -z "$key" ] && { log_warn " 未配置 MOCK_LOGIN_KEY,跳过"; return 0; } local resp; resp=$(remote "curl -sk --max-time 10 '${base_url}/api/mock-login' -H 'X-Mock-Key: ${key}'" 2>/dev/null || echo "") local token; token=$(echo "$resp" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") if [ -n "$token" ]; then log_info " mock-login 返回有效 JWT ✅" # 用获取到的 token 测试通知接口 local notif; notif=$(remote "curl -sk --max-time 10 '${base_url}/api/notifications/unread-count' -H 'Authorization: Bearer ${token}'" 2>/dev/null || echo "") echo "$notif" | grep -q '"success":true' && log_info " /api/notifications/unread-count 正常 ✅" || log_warn " 通知接口响应异常" else log_warn " mock-login 未返回 token" fi return 0 } test_payment_endpoints() { local all=true local c; c=$(remote "curl -sk -o /dev/null -w '%{http_code}' --max-time 10 '${base_url}/payment'" 2>/dev/null || echo "000") [ "$c" = "200" ] && log_info " /payment 返回 200 ✅" || { log_warn " /payment 返回 ${c}"; all=false; } c=$(remote "curl -sk -o /dev/null -w '%{http_code}' --max-time 10 '${base_url}/payment/page?package=monthly&jwt=bad'" 2>/dev/null || echo "000") [ "$c" = "401" ] && log_info " /payment/page (bad JWT) 返回 401 ✅" || { log_warn " /payment/page 返回 ${c}"; all=false; } c=$(remote "curl -sk -o /dev/null -w '%{http_code}' --max-time 10 '${base_url}/api/user/quota' -H 'Authorization: Bearer bad'" 2>/dev/null || echo "000") [ "$c" = "401" ] && log_info " /api/user/quota (bad JWT) 返回 401 ✅" || { log_warn " /api/user/quota 返回 ${c}"; all=false; } $all && log_info "支付端点测试通过" && return 0 log_warn "支付端点测试有异常(部分失败)"; return 0 } test_migration_status() { local r; r=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT COUNT(*) FROM _migrations'" 2>/dev/null || echo "0") [ "$r" -gt "0" ] 2>/dev/null && log_info " _migrations 表存在,已执行 ${r} 个迁移 ✅" || { log_warn " _migrations 表不存在或无记录"; return 0; } local last; last=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT name FROM _migrations ORDER BY applied_at DESC LIMIT 1'" 2>/dev/null || echo "") [ -n "$last" ] && log_info " 最新迁移: ${last} ✅" 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 -sk --max-time 10 '${base_url}/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 -sk --max-time 10 '${base_url}/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 -sk --max-time 10 -X POST '${base_url}/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 "执行部署后测试..." [ "$DRY_RUN" = true ] && { log_dry "完整测试套件(9 项)"; return 0; } [ "$SKIP_TESTS" = true ] && { log_warn "跳过测试"; return 0; } local tests=( "test_basic_connectivity:基础连通性" "test_database_connection:数据库连接" "test_token_flow:Token 流程" "test_weather_details_jwt:weather/details JWT" "test_invalid_token_401:无效 Token 401" "test_response_content:响应内容验证" "test_mock_login:Mock 登录 + 通知接口" "test_payment_endpoints:支付端点" "test_payment_flow:支付链路" "test_migration_status:迁移状态" ) local i=1 total=${#tests[@]} for entry in "${tests[@]}"; do IFS=':' read -r fn name <<< "$entry" log_step "测试 ${i}/${total}: ${name}..." $fn || rollback_and_exit "${name} 测试失败" i=$((i+1)) done log_info "========== 全部 ${total} 项测试通过 ==========" }