180 lines
9.8 KiB
Bash
180 lines
9.8 KiB
Bash
# ===========================================
|
||
# 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() {
|
||
local max_retries=3 retry=0
|
||
while [ $retry -lt $max_retries ]; do
|
||
retry=$((retry+1)); log_info "尝试 ${retry}/${max_retries}: 测试本地后端 (127.0.0.1:${BACKEND_PORT})"
|
||
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 -s -o /dev/null -w '%{http_code}' --max-time 10 -X ${method} http://127.0.0.1:${BACKEND_PORT}${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 -s --max-time 10 -X POST http://127.0.0.1:${BACKEND_PORT}/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 -s --max-time 10 http://127.0.0.1:${BACKEND_PORT}/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 -s --max-time 10 -X POST http://127.0.0.1:${BACKEND_PORT}/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 -s --max-time 10 -X POST http://127.0.0.1:${BACKEND_PORT}/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 -s --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/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 -s -o /dev/null -w '%{http_code}' --max-time 10 'http://127.0.0.1:${BACKEND_PORT}${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 -s --max-time 10 http://127.0.0.1:${BACKEND_PORT}/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 -s --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/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 -s --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/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 -s -o /dev/null -w '%{http_code}' --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/payment'" 2>/dev/null || echo "000")
|
||
[ "$c" = "200" ] && log_info " /payment 返回 200 ✅" || { log_warn " /payment 返回 ${c}"; all=false; }
|
||
|
||
c=$(remote "curl -s -o /dev/null -w '%{http_code}' --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/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 -s -o /dev/null -w '%{http_code}' --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/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
|
||
}
|
||
|
||
# ---------- 主测试入口 ----------
|
||
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_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} 项测试通过 =========="
|
||
}
|