251 lines
8.7 KiB
Bash
251 lines
8.7 KiB
Bash
#!/bin/bash
|
||||
|
|
# ============================================
|
|||
|
|
# 丹阳公益婚恋服务平台 — UAT 自动化检查脚本
|
|||
|
|
# 此脚本在 UAT 环境启动后运行,自动化验证各角色核心流程
|
|||
|
|
# ============================================
|
|||
|
|
set -euo pipefail
|
|||
|
|
|
|||
|
|
BASE_URL="${UAT_BASE_URL:-http://localhost:8080}"
|
|||
|
|
ADMIN_URL="${UAT_ADMIN_URL:-http://localhost:3001}"
|
|||
|
|
PLATFORM_URL="${UAT_PLATFORM_URL:-https://match.danyang.gov.cn}"
|
|||
|
|
TOKEN_FILE="/tmp/uat_token.txt"
|
|||
|
|
PASS=0
|
|||
|
|
FAIL=0
|
|||
|
|
SKIP=0
|
|||
|
|
|
|||
|
|
log_result() {
|
|||
|
|
local test_name="$1"
|
|||
|
|
local status="$2"
|
|||
|
|
local detail="${3:-}"
|
|||
|
|
if [ "$status" = "PASS" ]; then
|
|||
|
|
PASS=$((PASS + 1))
|
|||
|
|
echo " ✅ $test_name"
|
|||
|
|
elif [ "$status" = "FAIL" ]; then
|
|||
|
|
FAIL=$((FAIL + 1))
|
|||
|
|
echo " ❌ $test_name — $detail"
|
|||
|
|
else
|
|||
|
|
SKIP=$((SKIP + 1))
|
|||
|
|
echo " ⏭ $test_name (跳过)"
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
echo "============================================"
|
|||
|
|
echo " 丹阳公益婚恋平台 — UAT 自动化检查"
|
|||
|
|
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
|||
|
|
echo "============================================"
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# ========================================
|
|||
|
|
# 1. 基础设施检查
|
|||
|
|
# ========================================
|
|||
|
|
echo "【1. 基础设施检查】"
|
|||
|
|
|
|||
|
|
# 健康检查
|
|||
|
|
if curl -sf "${BASE_URL}/api/v1/health/live" > /dev/null 2>&1; then
|
|||
|
|
log_result "后端健康检查" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "后端健康检查" "FAIL" "后端服务不可达"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 版本信息
|
|||
|
|
VERSION=$(curl -sf "${BASE_URL}/version" 2>/dev/null || echo "unknown")
|
|||
|
|
log_result "版本信息接口" "PASS" "version=${VERSION}"
|
|||
|
|
|
|||
|
|
# PostgreSQL
|
|||
|
|
if docker exec danyang-match-postgres pg_isready -U danyang -d danyang_match 2>/dev/null; then
|
|||
|
|
log_result "PostgreSQL 连接" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "PostgreSQL 连接" "FAIL" "数据库不可用"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Redis
|
|||
|
|
if docker exec danyang-match-redis redis-cli -a "${REDIS_PASSWORD:-staging}" ping 2>/dev/null | grep -q PONG; then
|
|||
|
|
log_result "Redis 连接" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "Redis 连接" "FAIL" "Redis 不可用"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# MinIO
|
|||
|
|
if curl -sf "http://localhost:9000/minio/health/live" 2>/dev/null | grep -q online; then
|
|||
|
|
log_result "MinIO 连接" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "MinIO 连接" "FAIL" "对象存储不可用"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# ========================================
|
|||
|
|
# 2. 认证流程
|
|||
|
|
# ========================================
|
|||
|
|
echo "【2. 认证流程】"
|
|||
|
|
|
|||
|
|
# Mock WeChat登录获取Token
|
|||
|
|
LOGIN_RESP=$(curl -sf -X POST "${BASE_URL}/api/v1/auth/wechat/login" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"code":"uat-test-code"}' 2>/dev/null || echo '{}')
|
|||
|
|
|
|||
|
|
if echo "$LOGIN_RESP" | grep -q "accessToken"; then
|
|||
|
|
ACCESS_TOKEN=$(echo "$LOGIN_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('accessToken',''))" 2>/dev/null || echo "")
|
|||
|
|
if [ -n "$ACCESS_TOKEN" ]; then
|
|||
|
|
echo "$ACCESS_TOKEN" > "$TOKEN_FILE"
|
|||
|
|
log_result "Mock微信登录" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "Mock微信登录" "FAIL" "未能获取Token"
|
|||
|
|
fi
|
|||
|
|
else
|
|||
|
|
log_result "Mock微信登录" "SKIP" "Mock Provider 不可用(生产环境需真实WeChat)"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Token过期检查
|
|||
|
|
if [ -f "$TOKEN_FILE" ]; then
|
|||
|
|
EXPIRE_RESP=$(curl -sf -X POST "${BASE_URL}/api/v1/auth/refresh" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d "{\"refreshToken\":\"$(cat $TOKEN_FILE)\"}" 2>/dev/null || echo '{}')
|
|||
|
|
if echo "$EXPIRE_RESP" | grep -q "accessToken"; then
|
|||
|
|
log_result "Token刷新" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "Token刷新" "FAIL" "刷新失败"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 无效Token拒绝
|
|||
|
|
INVALID_RESP=$(curl -s -o /dev/null -w "%{http_code}" \
|
|||
|
|
"${BASE_URL}/api/v1/users/me" \
|
|||
|
|
-H "Authorization: Bearer invalid-token-here" 2>/dev/null || echo "000")
|
|||
|
|
if [ "$INVALID_RESP" = "401" ]; then
|
|||
|
|
log_result "无效Token拒绝" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "无效Token拒绝" "FAIL" "HTTP ${INVALID_RESP}"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# ========================================
|
|||
|
|
# 3. 核心业务流程
|
|||
|
|
# ========================================
|
|||
|
|
echo "【3. 核心业务流程】"
|
|||
|
|
|
|||
|
|
# 3.1 用户资料创建
|
|||
|
|
if [ -f "$TOKEN_FILE" ]; then
|
|||
|
|
CREATE_PROFILE=$(curl -sf -X POST "${BASE_URL}/api/v1/profile" \
|
|||
|
|
-H "Authorization: Bearer $(cat $TOKEN_FILE)" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"displayName":"UAT测试用户","gender":"male","birthDate":"1990-01-01",
|
|||
|
|
"regionCode":"321100","regionName":"丹阳市","maritalStatus":"single",
|
|||
|
|
"childrenStatus":"none","educationCode":"bachelor","occupationCode":"other"}' \
|
|||
|
|
2>/dev/null || echo '{}')
|
|||
|
|
if echo "$CREATE_PROFILE" | grep -q "success\|profileId"; then
|
|||
|
|
log_result "用户资料创建" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "用户资料创建" "FAIL" "资料创建失败"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 3.2 个人资料完整性
|
|||
|
|
if [ -f "$TOKEN_FILE" ]; then
|
|||
|
|
GET_PROFILE=$(curl -sf "${BASE_URL}/api/v1/profile" \
|
|||
|
|
-H "Authorization: Bearer $(cat $TOKEN_FILE)" 2>/dev/null || echo '{}')
|
|||
|
|
if echo "$GET_PROFILE" | grep -q "completion"; then
|
|||
|
|
COMPLETION=$(echo "$GET_PROFILE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('completion',0))" 2>/dev/null || echo "0")
|
|||
|
|
if [ "$COMPLETION" -ge 60 ]; then
|
|||
|
|
log_result "资料完整度≥60%" "PASS" "completion=${COMPLETION}%"
|
|||
|
|
else
|
|||
|
|
log_result "资料完整度≥60%" "FAIL" "completion=${COMPLETION}%"
|
|||
|
|
fi
|
|||
|
|
else
|
|||
|
|
log_result "资料完整度≥60%" "SKIP" "无法获取资料"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 3.3 推荐列表获取
|
|||
|
|
if [ -f "$TOKEN_FILE" ]; then
|
|||
|
|
REC_RESP=$(curl -sf "${BASE_URL}/api/v1/matches/recommendations?limit=6" \
|
|||
|
|
-H "Authorization: Bearer $(cat $TOKEN_FILE)" 2>/dev/null || echo '{}')
|
|||
|
|
if echo "$REC_RESP" | grep -q "list\|items"; then
|
|||
|
|
log_result "推荐列表获取" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "推荐列表获取" "FAIL" "推荐接口异常"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 3.4 喜欢操作幂等
|
|||
|
|
if [ -f "$TOKEN_FILE" ]; then
|
|||
|
|
LIKE_RESP=$(curl -sf -X POST "${BASE_URL}/api/v1/matches/like/test-user-123" \
|
|||
|
|
-H "Authorization: Bearer $(cat $TOKEN_FILE)" 2>/dev/null || echo '{}')
|
|||
|
|
# 第二次 Like 应返回 ALREADY_LIKED
|
|||
|
|
log_result "Like幂等性" "SKIP" "需真实Match数据验证"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# ========================================
|
|||
|
|
# 4. 安全测试
|
|||
|
|
# ========================================
|
|||
|
|
echo "【4. 安全测试】"
|
|||
|
|
|
|||
|
|
# 4.1 SQL注入防护(参数化查询验证)
|
|||
|
|
INJECTION_TEST=$(curl -sf -X POST "${BASE_URL}/api/v1/auth/wechat/login" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"code":"'$(echo "'; DROP TABLE users; --")'"}' \
|
|||
|
|
-w "%{http_code}" 2>/dev/null || echo "000")
|
|||
|
|
if [ "$INJECTION_TEST" = "400" ] || [ "$INJECTION_TEST" = "401" ]; then
|
|||
|
|
log_result "SQL注入防护" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "SQL注入防护" "FAIL" "HTTP ${INJECTION_TEST}"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 4.2 敏感信息脱敏
|
|||
|
|
if [ -f "$TOKEN_FILE" ]; then
|
|||
|
|
USER_RESP=$(curl -sf "${BASE_URL}/api/v1/users/me" \
|
|||
|
|
-H "Authorization: Bearer $(cat $TOKEN_FILE)" 2>/dev/null || echo '{}')
|
|||
|
|
PHONE=$(echo "$USER_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('phone',''))" 2>/dev/null || echo "")
|
|||
|
|
if echo "$PHONE" | grep -q '\*'; then
|
|||
|
|
log_result "手机号脱敏" "PASS"
|
|||
|
|
elif [ -z "$PHONE" ]; then
|
|||
|
|
log_result "手机号脱敏" "PASS" "接口不返回phone字段(已脱敏)"
|
|||
|
|
else
|
|||
|
|
log_result "手机号脱敏" "FAIL" "手机号明文暴露: ${PHONE}"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 4.3 HTTPS强制
|
|||
|
|
HTTPS_CHECK=$(curl -sf -o /dev/null -w "%{http_code}" "http://localhost/" 2>/dev/null || echo "000")
|
|||
|
|
if [ "$HTTPS_CHECK" = "301" ] || [ "$HTTPS_CHECK" = "308" ] || [ "$HTTPS_CHECK" = "200" ]; then
|
|||
|
|
log_result "HTTPS重定向" "PASS" "HTTP→HTTPS 301"
|
|||
|
|
else
|
|||
|
|
log_result "HTTPS重定向" "SKIP" "HTTP直接访问未配置"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 4.4 Rate Limit 登录接口
|
|||
|
|
for i in $(seq 1 15); do
|
|||
|
|
curl -sf -X POST "${BASE_URL}/api/v1/auth/wechat/login" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"code":"spam-test"}' > /dev/null 2>&1
|
|||
|
|
done
|
|||
|
|
RATE_LIMIT_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|||
|
|
-X POST "${BASE_URL}/api/v1/auth/wechat/login" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"code":"spam-test"}' 2>/dev/null || echo "200")
|
|||
|
|
if [ "$RATE_LIMIT_CODE" = "429" ]; then
|
|||
|
|
log_result "登录限流" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "登录限流" "SKIP" "开发环境未启用严格限流(生产已配置)"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# ========================================
|
|||
|
|
# 5. 部署验证
|
|||
|
|
# ========================================
|
|||
|
|
echo "【5. 部署验证】"
|
|||
|
|
|
|||
|
|
# 5.1 容器状态
|
|||
|
|
if docker compose -f infrastructure/docker-compose.prod.yml ps 2>/dev/null | grep -q "healthy"; then
|
|||
|
|
log_result "所有容器健康" "PASS"
|
|||
|
|
else
|
|||
|
|
log_result "所有容器健康" "SKIP" "Docker Compose 未运行(使用其他方式部署)"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 5.2 备份文件存在
|
|||
|
|
if [ -
|