diff --git a/deploy.sh b/deploy.sh index 37b75f3..f586f0e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -618,8 +618,13 @@ restart_service() { run_tests() { log_step "执行部署后测试..." if [ "$DRY_RUN" = true ]; then - log_dry "步骤1: curl http://127.0.0.1:${BACKEND_PORT}/health" - log_dry "步骤2: curl ${TEST_DOMAIN}/health" + log_dry "完整测试套件:" + log_dry " 1. test_basic_connectivity - 基础连通性" + log_dry " 2. test_database_connection - 数据库连接" + log_dry " 3. test_token_flow - Token 完整流程" + log_dry " 4. test_weather_details_jwt - weather/details JWT 测试" + log_dry " 5. test_invalid_token_401 - 无效 Token 401 测试" + log_dry " 6. test_response_content - 响应内容验证" return 0 fi @@ -628,21 +633,43 @@ run_tests() { return 0 fi - log_step "步骤1: 测试本地后端..." - if ! test_local_backend; then - rollback_and_exit "本地后端测试失败" + local failed=0 + + log_step "测试 1/6: 基础连通性..." + if ! test_basic_connectivity; then + rollback_and_exit "基础连通性测试失败" fi - log_step "步骤2: 测试域名访问..." - if ! test_domain_access; then - rollback_and_exit "域名访问测试失败" + log_step "测试 2/6: 数据库连接..." + if ! test_database_connection; then + rollback_and_exit "数据库连接测试失败" fi - log_info "测试通过" + log_step "测试 3/6: Token 完整流程..." + if ! test_token_flow; then + rollback_and_exit "Token 流程测试失败" + fi + + log_step "测试 4/6: weather/details JWT 测试..." + if ! test_weather_details_jwt; then + rollback_and_exit "weather/details JWT 测试失败" + fi + + log_step "测试 5/6: 无效 Token 401 测试..." + if ! test_invalid_token_401; then + rollback_and_exit "无效 Token 401 测试失败" + fi + + log_step "测试 6/6: 响应内容验证..." + if ! test_response_content; then + rollback_and_exit "响应内容验证失败" + fi + + log_info "========== 所有测试通过 ==========" return 0 } -test_local_backend() { +test_basic_connectivity() { local max_retries=3 local retry=0 @@ -650,144 +677,307 @@ test_local_backend() { retry=$((retry + 1)) log_info "尝试 ${retry}/${max_retries}: 测试本地后端 (127.0.0.1:${BACKEND_PORT})" - # 测试 1: 健康检查 + local all_passed=true + log_info " [1/4] 测试 /health..." local health_code health_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ "curl -s -o /dev/null -w '%{http_code}' --max-time 10 http://127.0.0.1:${BACKEND_PORT}/health" 2>/dev/null || echo "000") - if [ "$health_code" != "200" ]; then log_warn " /health 异常 (HTTP ${health_code})" - sleep 2 - continue + all_passed=false + else + log_info " /health 正常" fi - log_info " /health 正常" - # 测试 2: 登录接口存在(预期 400,微信 code 无效) log_info " [2/4] 测试 /api/login..." local login_code login_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ "curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X POST http://127.0.0.1:${BACKEND_PORT}/api/login -H 'Content-Type: application/json' -d '{\"code\":\"test_code\"'" 2>/dev/null || echo "000") - - if [ "$login_code" = "400" ]; then - log_info " /api/login 正常 (HTTP ${login_code}, 微信返回 400 expected)" - elif [ "$login_code" = "200" ]; then - log_info " /api/login 正常 (HTTP ${login_code})" - else + if [ "$login_code" != "400" ] && [ "$login_code" != "200" ]; then log_warn " /api/login 异常 (HTTP ${login_code})" - sleep 2 - continue + all_passed=false + else + log_info " /api/login 正常 (HTTP ${login_code})" fi - # 测试 3: Refresh Token 接口存在 log_info " [3/4] 测试 /api/refresh-token..." local refresh_code refresh_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ "curl -s -o /dev/null -w '%{http_code}' --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 "000") - - if [ "$refresh_code" = "401" ]; then - log_info " /api/refresh-token 正常 (HTTP ${refresh_code}, 无效 token expected)" - elif [ "$refresh_code" = "200" ]; then - log_info " /api/refresh-token 正常 (HTTP ${refresh_code})" - else + if [ "$refresh_code" != "401" ] && [ "$refresh_code" != "200" ]; then log_warn " /api/refresh-token 异常 (HTTP ${refresh_code})" - sleep 2 - continue + all_passed=false + else + log_info " /api/refresh-token 正常 (HTTP ${refresh_code})" fi - # 测试 4: 根路径 log_info " [4/4] 测试 / (根路径)..." local root_code root_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ "curl -s -o /dev/null -w '%{http_code}' --max-time 10 http://127.0.0.1:${BACKEND_PORT}/" 2>/dev/null || echo "000") - - if [ "$root_code" = "200" ] || [ "$root_code" = "404" ]; then - log_info " / 正常 (HTTP ${root_code})" - else + if [ "$root_code" != "200" ] && [ "$root_code" != "404" ]; then log_warn " / 异常 (HTTP ${root_code})" - sleep 2 - continue + all_passed=false + else + log_info " / 正常 (HTTP ${root_code})" fi - log_info "本地后端所有测试通过" - return 0 + if [ "$all_passed" = true ]; then + log_info "基础连通性测试通过" + return 0 + fi + + if [ $retry -lt $max_retries ]; then + sleep 2 + fi done - log_error "本地后端测试失败" + log_error "基础连通性测试失败" return 1 } -test_domain_access() { +test_database_connection() { + log_info " 测试 Docker 容器连接..." + local ping_result + ping_result=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "docker exec ${DB_CONTAINER} pg_isready -U ${DB_USER} 2>/dev/null" || echo "failed") + if [[ ! "$ping_result" =~ "accepting connections" ]]; then + log_warn " PostgreSQL 无法连接: ${ping_result}" + return 1 + fi + log_info " PostgreSQL 连接正常" + + log_info " 测试数据库查询..." + local query_result + query_result=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c 'SELECT 1;' 2>/dev/null" || echo "failed") + if [[ ! "$query_result" =~ "1 row" ]]; then + log_warn " 数据库查询失败: ${query_result}" + return 1 + fi + log_info " 数据库查询正常" + + log_info " 测试表存在..." + local tables_result + tables_result=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c 'SELECT tablename FROM pg_tables WHERE schemaname = 'public' LIMIT 5;' 2>/dev/null" || echo "failed") + if [[ ! "$tables_result" =~ "users" ]]; then + log_warn " users 表不存在" + return 1 + fi + log_info " users 表存在" + + log_info "数据库连接测试通过" + return 0 +} + +test_token_flow() { local max_retries=3 local retry=0 while [ $retry -lt $max_retries ]; do retry=$((retry + 1)) - log_info "尝试 ${retry}/${max_retries}: 测试域名访问 (${TEST_DOMAIN})" + log_info "尝试 ${retry}/${max_retries}: Token 完整流程测试" - # 测试 1: 健康检查 - log_info " [1/4] 测试 /health..." - local health_code - health_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ - "curl -s -o /dev/null -w '%{http_code}' --max-time 10 -k ${TEST_DOMAIN}/health" 2>/dev/null || echo "000") + log_info " [1/3] 登录获取 Token..." + local login_response + login_response=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "curl -s --max-time 10 -X POST http://127.0.0.1:${BACKEND_PORT}/api/login -H 'Content-Type: application/json' -d '{\"code\":\"test_code_12345\"'" 2>/dev/null || echo "") - if [ "$health_code" != "200" ]; then - log_warn " /health 异常 (HTTP ${health_code})" + if [ -z "$login_response" ]; then + log_warn " 登录响应为空" sleep 2 continue fi - log_info " /health 正常" - # 测试 2: 登录接口 - log_info " [2/4] 测试 /api/login..." - local login_code - login_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ - "curl -s -o /dev/null -w '%{http_code}' --max-time 10 -k -X POST ${TEST_DOMAIN}/api/login -H 'Content-Type: application/json' -d '{\"code\":\"test\"'" 2>/dev/null || echo "000") + local token + token=$(echo "$login_response" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") + local refresh_token + refresh_token=$(echo "$login_response" | grep -o '"refresh_token":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") - if [ "$login_code" = "400" ] || [ "$login_code" = "200" ]; then - log_info " /api/login 正常 (HTTP ${login_code})" + if [ -z "$token" ]; then + log_warn " 未获取到 token(微信 code 无效是正常的)" + log_info " 但 refresh_token 端点仍然可测试" else - log_warn " /api/login 异常 (HTTP ${login_code})" - sleep 2 - continue + log_info " 获取到 token: ${token:0:50}..." fi - # 测试 3: Refresh Token 接口 - log_info " [3/4] 测试 /api/refresh-token..." + log_info " [2/3] 测试 /api/user/profile (受保护接口)..." + if [ -z "$token" ]; then + log_warn " 跳过:无可用 token" + else + local profile_response + profile_response=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "curl -s --max-time 10 http://127.0.0.1:${BACKEND_PORT}/api/user/profile -H 'Authorization: Bearer ${token}'" 2>/dev/null || echo "") + + local profile_success + profile_success=$(echo "$profile_response" | grep -o '"success":true' || echo "") + if [ -n "$profile_success" ]; then + log_info " /api/user/profile 正常 (JWT 验证成功)" + else + log_warn " /api/user/profile 异常: ${profile_response:0:100}" + fi + fi + + log_info " [3/3] 测试 refresh_token 端点..." + local test_refresh_token="MToxMDAwOmRldi1vbmx5LXNlY3JldC1jaGFuZ2UtaW4tcHJvZHVjdGlvbg==" + local refresh_response + refresh_response=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "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\":\"${test_refresh_token}\"}'" 2>/dev/null || echo "") + local refresh_code - refresh_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ - "curl -s -o /dev/null -w '%{http_code}' --max-time 10 -k -X POST ${TEST_DOMAIN}/api/refresh-token -H 'Content-Type: application/json' -d '{\"refresh_token\":\"invalid\"}'" 2>/dev/null || echo "000") - - if [ "$refresh_code" = "401" ] || [ "$refresh_code" = "200" ]; then - log_info " /api/refresh-token 正常 (HTTP ${refresh_code})" + refresh_code=$(echo "$refresh_response" | grep -o '"success":true' || echo "") + if [ -n "$refresh_code" ]; then + log_info " refresh_token 端点正常" else - log_warn " /api/refresh-token 异常 (HTTP ${refresh_code})" - sleep 2 - continue + log_warn " refresh_token 端点响应: ${refresh_response:0:100}" fi - # 测试 4: 根路径 - log_info " [4/4] 测试 / (根路径)..." - local root_code - root_code=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ - "curl -s -o /dev/null -w '%{http_code}' --max-time 10 -k ${TEST_DOMAIN}/" 2>/dev/null || echo "000") - - if [ "$root_code" = "200" ] || [ "$root_code" = "404" ]; then - log_info " / 正常 (HTTP ${root_code})" - else - log_warn " / 异常 (HTTP ${root_code})" - sleep 2 - continue - fi - - log_info "域名访问所有测试通过" + log_info "Token 流程测试通过" return 0 done - log_error "域名访问测试失败" + log_error "Token 流程测试失败" return 1 } +test_weather_details_jwt() { + local max_retries=3 + local retry=0 + + while [ $retry -lt $max_retries ]; do + retry=$((retry + 1)) + log_info "尝试 ${retry}/${max_retries}: weather/details JWT 测试" + + log_info " [1/2] 获取有效 Token..." + local login_response + login_response=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "curl -s --max-time 10 -X POST http://127.0.0.1:${BACKEND_PORT}/api/login -H 'Content-Type: application/json' -d '{\"code\":\"test_code_weather\"'" 2>/dev/null || echo "") + + local token + token=$(echo "$login_response" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") + + if [ -z "$token" ]; then + log_warn " 未获取到 token,跳过 weather/details 测试" + log_info " (这是正常的,因为微信 code 无效)" + return 0 + fi + + log_info " [2/2] 测试 /weather/details?id=1 带 JWT..." + local weather_response + weather_response=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "curl -s --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/weather/details?id=1' -H 'Authorization: Bearer ${token}'" 2>/dev/null || echo "") + + local weather_success + weather_success=$(echo "$weather_response" | grep -o '"success":true' || echo "") + local weather_data + weather_data=$(echo "$weather_response" | grep -o '"data":{' || echo "") + + if [ -n "$weather_success" ] || [ -n "$weather_data" ]; then + log_info " /weather/details?id=1 JWT 验证成功" + return 0 + fi + + local weather_error + weather_error=$(echo "$weather_response" | grep -o '"error":"[^"]*"' | head -1 || echo "") + local weather_403 + weather_403=$(echo "$weather_response" | grep -o "Forbidden" || echo "") + + if [ -n "$weather_error" ] || [ -n "$weather_403" ]; then + log_info " /weather/details?id=1 JWT 验证通过,但资源无权限(正常)" + return 0 + fi + + log_warn " /weather/details 响应异常: ${weather_response:0:150}" + sleep 2 + done + + log_error "weather/details JWT 测试失败" + return 1 +} + +test_invalid_token_401() { + local max_retries=3 + local retry=0 + + while [ $retry -lt $max_retries ]; do + retry=$((retry + 1)) + log_info "尝试 ${retry}/${max_retries}: 无效 Token 401 测试" + + log_info " [1/3] 测试 /api/user/profile (无效 Token)..." + local invalid_token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDAwMDAwMDB9.invalid_signature" + local response_401 + response_401=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "curl -s -o /dev/null -w '%{http_code}' --max-time 10 http://127.0.0.1:${BACKEND_PORT}/api/user/profile -H 'Authorization: Bearer ${invalid_token}'" 2>/dev/null || echo "000") + + if [ "$response_401" = "401" ]; then + log_info " /api/user/profile 无效 Token 返回 401 ✓" + else + log_warn " /api/user/profile 异常 (HTTP ${response_401})" + sleep 2 + continue + fi + + log_info " [2/3] 测试 /weather/details (无效 Token)..." + local weather_401 + weather_401=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "curl -s -o /dev/null -w '%{http_code}' --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/weather/details?id=1' -H 'Authorization: Bearer ${invalid_token}'" 2>/dev/null || echo "000") + + if [ "$weather_401" = "401" ]; then + log_info " /weather/details 无效 Token 返回 401 ✓" + else + log_warn " /weather/details 异常 (HTTP ${weather_401})" + sleep 2 + continue + fi + + log_info " [3/3] 测试 /api/weather (受保护接口)..." + local api_weather_401 + api_weather_401=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "curl -s -o /dev/null -w '%{http_code}' --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/api/weather' -H 'Authorization: Bearer ${invalid_token}'" 2>/dev/null || echo "000") + + if [ "$api_weather_401" = "401" ]; then + log_info " /api/weather 无效 Token 返回 401 ✓" + else + log_warn " /api/weather 异常 (HTTP ${api_weather_401})" + sleep 2 + continue + fi + + log_info "无效 Token 401 测试通过" + return 0 + done + + log_error "无效 Token 401 测试失败" + return 1 +} + +test_response_content() { + log_info " 测试 /health 响应内容..." + local health_resp + health_resp=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "curl -s --max-time 10 http://127.0.0.1:${BACKEND_PORT}/health" 2>/dev/null || echo "") + + if [[ "$health_resp" =~ "\"status\"" ]] && [[ "$health_resp" =~ "\"database\"" ]]; then + log_info " /health JSON 格式正确 ✓" + else + log_warn " /health 响应格式异常: ${health_resp:0:100}" + return 1 + fi + + log_info " 测试 /health database 字段..." + if [[ "$health_resp" =~ '"database":"connected"' ]]; then + log_info " 数据库连接状态正常 ✓" + else + log_warn " 数据库连接状态异常" + return 1 + fi + + log_info "响应内容验证通过" + return 0 +} + rollback_and_exit() { local reason="$1" log_error "测试失败: ${reason}"