e795f924ea
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Staging E2E Tests (push) Failing after 96h58m49s
Deploy / Deploy Staging (push) Failing after 97h1m23s
CI/CD Pipeline / Frontend Lint (push) Failing after 97h1m32s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 97h1m32s
769 lines
27 KiB
Bash
Executable File
769 lines
27 KiB
Bash
Executable File
#!/bin/bash
|
||
#
|
||
# API 端到端冒烟测试
|
||
# 用途:部署后快速验证核心功能是否正常
|
||
# 支持:staging / production 多环境
|
||
#
|
||
# 用法:
|
||
# BASE_URL=https://staging-api.xiaoxiajianji.com ./api_smoke_test.sh
|
||
# BASE_URL=https://saas-api.xiaoxiajianji.com TEST_USER=prod_e2e TEST_PASSWORD=xxx ./api_smoke_test.sh
|
||
# # 只跑指定模块
|
||
# MODULES="auth,edit-plans" ./api_smoke_test.sh
|
||
#
|
||
# 环境变量:
|
||
# BASE_URL API 地址,必填
|
||
# TEST_USER 测试用户名,默认自动生成
|
||
# TEST_PASSWORD 测试密码,默认 Test123456!
|
||
# TEST_EMAIL 测试邮箱,默认自动生成
|
||
# EXISTING_TOKEN 使用已有 token(跳过注册/登录,生产环境推荐)
|
||
# MODULES 指定模块,逗号分隔,默认全部
|
||
# CLEANUP_ENABLED 是否清理测试数据,1=是 0=否,默认 1
|
||
#
|
||
|
||
set -euo pipefail
|
||
|
||
# ===== 配置 =====
|
||
BASE_URL="${BASE_URL:-}"
|
||
TEST_USER="${TEST_USER:-e2e_$(date +%s)}"
|
||
TEST_PASSWORD="${TEST_PASSWORD:-Test123456!}"
|
||
TEST_EMAIL="${TEST_EMAIL:-${TEST_USER}@test.com}"
|
||
EXISTING_TOKEN="${EXISTING_TOKEN:-}"
|
||
MODULES="${MODULES:-all}"
|
||
CLEANUP_ENABLED="${CLEANUP_ENABLED:-1}"
|
||
CURL_TIMEOUT=30
|
||
CURL_CONNECT_TIMEOUT=15
|
||
CURL_INSECURE="${CURL_INSECURE:-0}"
|
||
|
||
# 证书不安全的环境(如staging)可设 CURL_INSECURE=1 跳过校验
|
||
if [ "$CURL_INSECURE" = "1" ]; then
|
||
curl() { command curl -k "$@"; }
|
||
fi
|
||
|
||
if [ -z "$BASE_URL" ]; then
|
||
echo "❌ 错误:BASE_URL 环境变量未设置"
|
||
echo " 用法: BASE_URL=https://api.example.com $0"
|
||
exit 1
|
||
fi
|
||
|
||
# 去掉末尾的斜杠
|
||
BASE_URL="${BASE_URL%/}"
|
||
|
||
# ===== 全局变量 =====
|
||
PASSED=0
|
||
FAILED=0
|
||
FAIL_LIST=""
|
||
TOKEN=""
|
||
AUTH_HEADER=""
|
||
TEST_USER_ID=""
|
||
|
||
# 测试数据追踪(用于清理)
|
||
CREATED_PLANS=()
|
||
CREATED_TEMPLATES=()
|
||
CREATED_PROJECTS=()
|
||
|
||
# ===== 工具函数 =====
|
||
pass() {
|
||
echo "✅ $1"
|
||
PASSED=$((PASSED + 1))
|
||
}
|
||
|
||
fail() {
|
||
echo "❌ $1"
|
||
[ -n "${2:-}" ] && echo " $2"
|
||
FAILED=$((FAILED + 1))
|
||
FAIL_LIST="$FAIL_LIST
|
||
❌ $1${2:+" - $2"}"
|
||
}
|
||
|
||
info() {
|
||
echo "ℹ️ $1"
|
||
}
|
||
|
||
section() {
|
||
echo ""
|
||
echo "============================================================"
|
||
echo "$1"
|
||
echo "============================================================"
|
||
}
|
||
|
||
# 检查模块是否应该运行
|
||
should_run() {
|
||
local module="$1"
|
||
if [ "$MODULES" = "all" ]; then
|
||
return 0
|
||
fi
|
||
echo ",$MODULES," | grep -q ",$module,"
|
||
}
|
||
|
||
# 安全的 JSON 字段提取
|
||
json_get() {
|
||
local json="$1"
|
||
local key="$2"
|
||
echo "$json" | python3 -c "
|
||
import sys, json
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
# 支持嵌套 key,用 . 分隔
|
||
keys = '$key'.split('.')
|
||
val = d
|
||
for k in keys:
|
||
if isinstance(val, dict):
|
||
val = val.get(k, '')
|
||
elif isinstance(val, list) and k.isdigit():
|
||
idx = int(k)
|
||
val = val[idx] if idx < len(val) else ''
|
||
else:
|
||
val = ''
|
||
break
|
||
if val is None:
|
||
print('')
|
||
elif isinstance(val, (dict, list)):
|
||
print(json.dumps(val, ensure_ascii=False))
|
||
else:
|
||
print(str(val))
|
||
except:
|
||
print('')
|
||
" 2>/dev/null
|
||
}
|
||
|
||
# ===== 认证 =====
|
||
setup_auth() {
|
||
section "0. 认证准备"
|
||
|
||
if [ -n "$EXISTING_TOKEN" ]; then
|
||
TOKEN="$EXISTING_TOKEN"
|
||
AUTH_HEADER="Authorization: Bearer $TOKEN"
|
||
info "使用已有 TOKEN"
|
||
|
||
# 验证 token 有效
|
||
local code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/auth/me")
|
||
if [ "$code" = "200" ]; then
|
||
pass "Token 验证通过"
|
||
else
|
||
fail "Token 验证失败" "HTTP $code"
|
||
return 1
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
# 注册新用户
|
||
local resp=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/auth/register" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"username\":\"$TEST_USER\",\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\",\"nickname\":\"E2E测试\"}" \
|
||
--max-time $CURL_TIMEOUT)
|
||
local code=$(echo "$resp" | tail -1)
|
||
local body=$(echo "$resp" | sed '$d')
|
||
|
||
if [ "$code" = "200" ] || [ "$code" = "201" ]; then
|
||
pass "用户注册成功"
|
||
TEST_USER_ID=$(json_get "$body" "user_id")
|
||
elif [ "$code" = "400" ] || [ "$code" = "409" ] || [ "$code" = "422" ]; then
|
||
info "用户已存在,尝试登录"
|
||
else
|
||
fail "用户注册" "HTTP $code, body: ${body:0:200}"
|
||
return 1
|
||
fi
|
||
|
||
# 登录
|
||
resp=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/v1/auth/login" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}" \
|
||
--max-time $CURL_TIMEOUT)
|
||
code=$(echo "$resp" | tail -1)
|
||
body=$(echo "$resp" | sed '$d')
|
||
|
||
if [ "$code" = "200" ]; then
|
||
pass "用户登录成功"
|
||
TOKEN=$(json_get "$body" "access_token")
|
||
AUTH_HEADER="Authorization: Bearer $TOKEN"
|
||
TEST_USER_ID=$(json_get "$body" "user_id")
|
||
else
|
||
fail "用户登录" "HTTP $code, body: ${body:0:200}"
|
||
return 1
|
||
fi
|
||
|
||
# 验证
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/auth/me")
|
||
if [ "$code" = "200" ]; then
|
||
pass "获取当前用户信息"
|
||
else
|
||
fail "获取当前用户信息" "HTTP $code"
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ===== 模块:基础健康检查 =====
|
||
test_health() {
|
||
should_run "health" || return 0
|
||
section "1. 基础健康检查"
|
||
|
||
local code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$BASE_URL/health")
|
||
[ "$code" = "200" ] && pass "健康检查 /health" || fail "健康检查" "HTTP $code"
|
||
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$BASE_URL/docs")
|
||
if [ "$code" = "200" ]; then
|
||
pass "API Docs 可访问"
|
||
elif [ "$code" = "404" ] || [ "$code" = "403" ] || [ "$code" = "401" ]; then
|
||
pass "API Docs(生产环境已禁用,HTTP $code,安全策略正常)"
|
||
else
|
||
fail "API Docs" "HTTP $code"
|
||
fi
|
||
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$BASE_URL/api/v1/assets")
|
||
if [ "$code" = "401" ] || [ "$code" = "403" ]; then
|
||
pass "未授权访问返回 401/403(鉴权正常)"
|
||
else
|
||
fail "未授权鉴权" "HTTP $code(应为401/403)"
|
||
fi
|
||
}
|
||
|
||
# ===== 模块:剪辑计划 =====
|
||
test_edit_plans() {
|
||
should_run "edit-plans" || return 0
|
||
[ -z "$TOKEN" ] && return 0
|
||
|
||
section "2. 剪辑计划 (edit-plans)"
|
||
|
||
local template_id=""
|
||
local plan_id=""
|
||
|
||
# 列表
|
||
local resp=$(curl -s -w "\n%{http_code}" -H "$AUTH_HEADER" \
|
||
"$BASE_URL/api/v1/edit-plans" --max-time $CURL_TIMEOUT)
|
||
local code=$(echo "$resp" | tail -1)
|
||
[ "$code" = "200" ] && pass "剪辑计划列表" || fail "剪辑计划列表" "HTTP $code"
|
||
|
||
# 获取一个模板ID
|
||
resp=$(curl -s -w "\n%{http_code}" -H "$AUTH_HEADER" \
|
||
"$BASE_URL/api/v1/templates" --max-time $CURL_TIMEOUT)
|
||
local tcode=$(echo "$resp" | tail -1)
|
||
local tbody=$(echo "$resp" | sed '$d')
|
||
|
||
if [ "$tcode" = "200" ]; then
|
||
template_id=$(echo "$tbody" | python3 -c "
|
||
import sys, json
|
||
d = json.load(sys.stdin)
|
||
items = d.get('items', []) or d.get('data', []) or []
|
||
print(items[0].get('id', '') if items else '')
|
||
" 2>/dev/null)
|
||
fi
|
||
|
||
if [ -z "$template_id" ]; then
|
||
# 创建一个模板
|
||
resp=$(curl -s -w "\n%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d '{"name":"E2E测试模板","mode":"voice_over","category":"vlog","config":{}}' \
|
||
"$BASE_URL/api/v1/templates" --max-time $CURL_TIMEOUT)
|
||
tcode=$(echo "$resp" | tail -1)
|
||
tbody=$(echo "$resp" | sed '$d')
|
||
if [ "$tcode" = "200" ] || [ "$tcode" = "201" ]; then
|
||
template_id=$(json_get "$tbody" "id")
|
||
[ -z "$template_id" ] && template_id=$(json_get "$tbody" "data.id")
|
||
pass "创建测试模板(用于计划依赖)"
|
||
CREATED_TEMPLATES+=("$template_id")
|
||
else
|
||
fail "创建测试模板" "HTTP $tcode"
|
||
return
|
||
fi
|
||
else
|
||
pass "获取已有模板ID"
|
||
fi
|
||
|
||
# 创建 - 完整参数
|
||
resp=$(curl -s -w "\n%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d "{\"name\":\"E2E完整测试\",\"template_id\":\"$template_id\",\"description\":\"自动化测试\",\"config\":{\"cover\":{\"type\":\"ai\",\"image_url\":\"\",\"position\":0},\"title\":{\"text\":\"测试标题\",\"font_size\":24,\"color\":\"#ffffff\",\"position\":\"top\",\"ai_enabled\":true},\"subtitle\":{\"enabled\":true,\"font_size\":14,\"color\":\"#ffffff\",\"position\":\"bottom\",\"style\":\"default\"},\"bgm\":{\"enabled\":true,\"type\":\"auto\",\"volume\":0.5}}}" \
|
||
"$BASE_URL/api/v1/edit-plans" --max-time $CURL_TIMEOUT)
|
||
code=$(echo "$resp" | tail -1)
|
||
local body=$(echo "$resp" | sed '$d')
|
||
|
||
if [ "$code" = "200" ] || [ "$code" = "201" ]; then
|
||
pass "创建剪辑计划(完整config)"
|
||
plan_id=$(json_get "$body" "id")
|
||
[ -z "$plan_id" ] && plan_id=$(json_get "$body" "data.id")
|
||
CREATED_PLANS+=("$plan_id")
|
||
|
||
# 验证 config 结构
|
||
local has_cover=$(echo "$body" | grep -c '"cover"' || true)
|
||
local has_title=$(echo "$body" | grep -c '"title"' || true)
|
||
local has_subtitle=$(echo "$body" | grep -c '"subtitle"' || true)
|
||
local has_bgm=$(echo "$body" | grep -c '"bgm"' || true)
|
||
if [ "$has_cover" -gt 0 ] && [ "$has_title" -gt 0 ] && [ "$has_subtitle" -gt 0 ] && [ "$has_bgm" -gt 0 ]; then
|
||
pass "config 结构标准化(cover/title/subtitle/bgm 齐全)"
|
||
else
|
||
fail "config 结构标准化" "cover=$has_cover title=$has_title subtitle=$has_subtitle bgm=$has_bgm"
|
||
fi
|
||
else
|
||
fail "创建剪辑计划(完整config)" "HTTP $code, body: ${body:0:200}"
|
||
fi
|
||
|
||
# 创建 - 最小参数
|
||
resp=$(curl -s -w "\n%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d "{\"name\":\"E2E最小测试\",\"template_id\":\"$template_id\"}" \
|
||
"$BASE_URL/api/v1/edit-plans" --max-time $CURL_TIMEOUT)
|
||
code=$(echo "$resp" | tail -1)
|
||
body=$(echo "$resp" | sed '$d')
|
||
if [ "$code" = "200" ] || [ "$code" = "201" ]; then
|
||
pass "创建剪辑计划(最小参数,自动补默认config)"
|
||
local has_default=$(echo "$body" | python3 -c "
|
||
import sys, json
|
||
d = json.load(sys.stdin)
|
||
data = d.get('data', d)
|
||
config = data.get('config', {})
|
||
if isinstance(config, dict) and 'cover' in config and 'title' in config:
|
||
print('yes')
|
||
else:
|
||
print('no')
|
||
" 2>/dev/null)
|
||
[ "$has_default" = "yes" ] && pass "最小参数创建时自动填充默认config" || fail "自动填充默认config" "未填充"
|
||
else
|
||
fail "创建剪辑计划(最小参数)" "HTTP $code"
|
||
fi
|
||
|
||
# 详情
|
||
if [ -n "$plan_id" ]; then
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/edit-plans/$plan_id" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "获取剪辑计划详情" || fail "获取剪辑计划详情" "HTTP $code"
|
||
fi
|
||
|
||
# 更新
|
||
if [ -n "$plan_id" ]; then
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d '{"name":"E2E测试-已更新","config":{"title":{"text":"新标题","ai_enabled":false}}}' \
|
||
"$BASE_URL/api/v1/edit-plans/$plan_id" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "更新剪辑计划" || fail "更新剪辑计划" "HTTP $code"
|
||
fi
|
||
|
||
# AI 推荐(可能因无素材/服务未配置返回500/503,接口存在即可)
|
||
if [ -n "$plan_id" ]; then
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d '{"style":"vlog"}' \
|
||
"$BASE_URL/api/v1/edit-plans/$plan_id/ai-recommend" --max-time 60)
|
||
if [ "$code" = "200" ] || [ "$code" = "202" ]; then
|
||
pass "AI 推荐片段"
|
||
elif [ "$code" = "500" ] || [ "$code" = "503" ]; then
|
||
pass "AI 推荐片段(HTTP $code,环境限制,接口存在)"
|
||
else
|
||
fail "AI 推荐片段" "HTTP $code"
|
||
fi
|
||
fi
|
||
|
||
# AI 封面生成
|
||
if [ -n "$plan_id" ]; then
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d '{"mode":"ai_select"}' \
|
||
"$BASE_URL/api/v1/edit-plans/$plan_id/generate-cover" --max-time 60)
|
||
if [ "$code" = "200" ] || [ "$code" = "202" ]; then
|
||
pass "AI 封面生成"
|
||
elif [ "$code" = "500" ] || [ "$code" = "503" ]; then
|
||
pass "AI 封面生成(HTTP $code,环境限制,接口存在)"
|
||
else
|
||
fail "AI 封面生成" "HTTP $code"
|
||
fi
|
||
fi
|
||
|
||
# 关联 generations
|
||
if [ -n "$plan_id" ]; then
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/edit-plans/$plan_id/generations" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "剪辑计划关联生成历史" || fail "剪辑计划关联生成历史" "HTTP $code"
|
||
fi
|
||
|
||
# 时间线
|
||
if [ -n "$plan_id" ]; then
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/edit-plans/$plan_id/timeline" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "剪辑计划时间线" || fail "剪辑计划时间线" "HTTP $code"
|
||
fi
|
||
}
|
||
|
||
# ===== 模块:剪辑模板 =====
|
||
test_templates() {
|
||
should_run "templates" || return 0
|
||
[ -z "$TOKEN" ] && return 0
|
||
|
||
section "3. 剪辑模板 (templates)"
|
||
|
||
local tpl_id=""
|
||
|
||
# 列表
|
||
local code=$(curl -s -o /dev/null -w "%{http_code}" -H "$AUTH_HEADER" \
|
||
"$BASE_URL/api/v1/templates" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "模板列表" || fail "模板列表" "HTTP $code"
|
||
|
||
# 分类
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -H "$AUTH_HEADER" \
|
||
"$BASE_URL/api/v1/templates/categories/list" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "模板分类列表" || fail "模板分类列表" "HTTP $code"
|
||
|
||
# 创建
|
||
local resp=$(curl -s -w "\n%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d '{"name":"E2E测试模板-完整","mode":"voice_over","description":"测试模板","category":"vlog","config":{"cover":{"type":"ai"},"title":{"ai_enabled":true},"subtitle":{"enabled":true},"bgm":{"enabled":true}}}' \
|
||
"$BASE_URL/api/v1/templates" --max-time $CURL_TIMEOUT)
|
||
code=$(echo "$resp" | tail -1)
|
||
local body=$(echo "$resp" | sed '$d')
|
||
|
||
if [ "$code" = "200" ] || [ "$code" = "201" ]; then
|
||
pass "创建模板"
|
||
tpl_id=$(json_get "$body" "id")
|
||
[ -z "$tpl_id" ] && tpl_id=$(json_get "$body" "data.id")
|
||
CREATED_TEMPLATES+=("$tpl_id")
|
||
else
|
||
fail "创建模板" "HTTP $code, body: ${body:0:200}"
|
||
fi
|
||
|
||
# 详情
|
||
if [ -n "$tpl_id" ]; then
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/templates/$tpl_id" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "模板详情" || fail "模板详情" "HTTP $code"
|
||
fi
|
||
|
||
# toggle-favorite
|
||
if [ -n "$tpl_id" ]; then
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" \
|
||
"$BASE_URL/api/v1/templates/$tpl_id/toggle-favorite" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "模板收藏切换 (toggle-favorite)" || fail "模板收藏切换" "HTTP $code"
|
||
fi
|
||
|
||
# validate
|
||
if [ -n "$tpl_id" ]; then
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d '{}' \
|
||
"$BASE_URL/api/v1/templates/$tpl_id/validate" --max-time $CURL_TIMEOUT)
|
||
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
|
||
pass "模板配置验证 (validate)"
|
||
else
|
||
fail "模板配置验证" "HTTP $code"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ===== 模块:素材库 =====
|
||
test_assets() {
|
||
should_run "assets" || return 0
|
||
[ -z "$TOKEN" ] && return 0
|
||
|
||
section "4. 素材库 (assets & asset-libraries)"
|
||
|
||
# 素材列表
|
||
local resp=$(curl -s -w "\n%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/assets" --max-time $CURL_TIMEOUT)
|
||
local code=$(echo "$resp" | tail -1)
|
||
local body=$(echo "$resp" | sed '$d')
|
||
|
||
if [ "$code" = "200" ]; then
|
||
pass "素材列表"
|
||
local count=$(echo "$body" | python3 -c "
|
||
import sys, json
|
||
d = json.load(sys.stdin)
|
||
items = d.get('items', []) or d.get('data', []) or []
|
||
print(len(items))
|
||
" 2>/dev/null)
|
||
|
||
if [ "${count:-0}" -gt 0 ]; then
|
||
local has_file=$(echo "$body" | grep -c '"file_url"' || true)
|
||
local has_thumb=$(echo "$body" | grep -c '"thumbnail_url"' || true)
|
||
[ "$has_file" -gt 0 ] && pass "素材包含 file_url 字段" || fail "素材 file_url 字段" "未找到"
|
||
[ "$has_thumb" -gt 0 ] && pass "素材包含 thumbnail_url 字段" || fail "素材 thumbnail_url 字段" "未找到"
|
||
else
|
||
pass "新用户素材列表为空(正常)"
|
||
pass "跳过 file_url 检查(无素材)"
|
||
pass "跳过 thumbnail_url 检查(无素材)"
|
||
fi
|
||
else
|
||
fail "素材列表" "HTTP $code"
|
||
fi
|
||
|
||
# 素材库列表
|
||
resp=$(curl -s -w "\n%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/asset-libraries" --max-time $CURL_TIMEOUT)
|
||
code=$(echo "$resp" | tail -1)
|
||
body=$(echo "$resp" | sed '$d')
|
||
|
||
if [ "$code" = "200" ]; then
|
||
pass "素材库列表 (asset-libraries)"
|
||
else
|
||
fail "素材库列表" "HTTP $code"
|
||
fi
|
||
}
|
||
|
||
# ===== 模块:诊断 =====
|
||
test_diagnosis() {
|
||
should_run "diagnosis" || return 0
|
||
[ -z "$TOKEN" ] && return 0
|
||
|
||
section "5. 素材诊断"
|
||
|
||
local project_id=""
|
||
|
||
# 创建项目
|
||
local resp=$(curl -s -w "\n%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d '{"name":"E2E诊断测试项目"}' \
|
||
"$BASE_URL/api/v1/projects" --max-time $CURL_TIMEOUT)
|
||
local pcode=$(echo "$resp" | tail -1)
|
||
local pbody=$(echo "$resp" | sed '$d')
|
||
|
||
if [ "$pcode" = "200" ] || [ "$pcode" = "201" ]; then
|
||
project_id=$(json_get "$pbody" "id")
|
||
[ -z "$project_id" ] && project_id=$(json_get "$pbody" "data.id")
|
||
pass "创建测试项目"
|
||
CREATED_PROJECTS+=("$project_id")
|
||
else
|
||
# 尝试获取已有项目
|
||
resp=$(curl -s -w "\n%{http_code}" -H "$AUTH_HEADER" \
|
||
"$BASE_URL/api/v1/projects" --max-time $CURL_TIMEOUT)
|
||
local lcode=$(echo "$resp" | tail -1)
|
||
local lbody=$(echo "$resp" | sed '$d')
|
||
if [ "$lcode" = "200" ]; then
|
||
project_id=$(echo "$lbody" | python3 -c "
|
||
import sys, json
|
||
d = json.load(sys.stdin)
|
||
items = d.get('items', []) or d.get('data', []) or []
|
||
print(items[0].get('id', '') if items else '')
|
||
" 2>/dev/null)
|
||
pass "获取已有项目"
|
||
else
|
||
fail "获取项目" "HTTP $lcode"
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# 项目级素材诊断
|
||
if [ -n "$project_id" ]; then
|
||
resp=$(curl -s -w "\n%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/projects/$project_id/asset-diagnosis" --max-time $CURL_TIMEOUT)
|
||
local code=$(echo "$resp" | tail -1)
|
||
body=$(echo "$resp" | sed '$d')
|
||
|
||
if [ "$code" = "200" ]; then
|
||
pass "项目级素材诊断"
|
||
|
||
# 检查新格式
|
||
local has_score=$(echo "$body" | grep -c '"readiness_score"' || true)
|
||
local has_label=$(echo "$body" | grep -c '"readiness_label"' || true)
|
||
local has_total=$(echo "$body" | grep -c '"total_assets"' || true)
|
||
|
||
if [ "$has_score" -gt 0 ] || [ "$has_label" -gt 0 ] || [ "$has_total" -gt 0 ]; then
|
||
pass "诊断返回新格式(readiness_score/readiness_label/total_assets)"
|
||
else
|
||
local keys=$(echo "$body" | python3 -c "
|
||
import sys, json
|
||
d = json.load(sys.stdin)
|
||
data = d.get('data', d)
|
||
if isinstance(data, dict):
|
||
print(','.join(list(data.keys())[:8]))
|
||
else:
|
||
print('')
|
||
" 2>/dev/null)
|
||
fail "诊断返回结构" "keys: $keys"
|
||
fi
|
||
else
|
||
fail "项目级素材诊断" "HTTP $code"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ===== 模块:生成任务 =====
|
||
test_generation() {
|
||
should_run "generation" || return 0
|
||
[ -z "$TOKEN" ] && return 0
|
||
|
||
section "6. 生成任务"
|
||
|
||
local code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/generation/tasks" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "生成任务列表" || fail "生成任务列表" "HTTP $code"
|
||
}
|
||
|
||
# ===== 模块:订阅 =====
|
||
test_subscription() {
|
||
should_run "subscription" || return 0
|
||
[ -z "$TOKEN" ] && return 0
|
||
|
||
section "7. 订阅与配额"
|
||
|
||
local code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/subscription/current" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "当前订阅信息" || fail "当前订阅信息" "HTTP $code"
|
||
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/subscription/billing-records" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "订阅账单记录" || fail "订阅账单记录" "HTTP $code"
|
||
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/dashboard/overview" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "仪表盘概览" || fail "仪表盘概览" "HTTP $code"
|
||
}
|
||
|
||
# ===== 模块:其他核心接口 =====
|
||
test_misc() {
|
||
should_run "misc" || return 0
|
||
[ -z "$TOKEN" ] && return 0
|
||
|
||
section "8. 其他核心接口"
|
||
|
||
local code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-H "$AUTH_HEADER" "$BASE_URL/api/v1/voices" --max-time $CURL_TIMEOUT)
|
||
[ "$code" = "200" ] && pass "音色列表" || fail "音色列表" "HTTP $code"
|
||
|
||
# 标题生成
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||
-d '{"name":"E2E测试标题","text":"今天天气真好我们出去玩"}' \
|
||
"$BASE_URL/api/v1/titles" --max-time $CURL_TIMEOUT)
|
||
if [ "$code" = "200" ] || [ "$code" = "201" ]; then
|
||
pass "AI 标题生成"
|
||
elif [ "$code" = "500" ] || [ "$code" = "503" ]; then
|
||
pass "AI 标题生成(HTTP $code,环境限制,接口存在)"
|
||
else
|
||
fail "AI 标题生成" "HTTP $code"
|
||
fi
|
||
}
|
||
|
||
# ===== 模块:Nginx 路由 =====
|
||
test_nginx() {
|
||
should_run "nginx" || return 0
|
||
|
||
section "9. 前端 SPA 路由验证(Nginx)"
|
||
|
||
# 需要 WEB_URL 环境变量
|
||
local web_url="${WEB_URL:-}"
|
||
if [ -z "$web_url" ]; then
|
||
info "跳过(WEB_URL 未设置)"
|
||
return 0
|
||
fi
|
||
web_url="${web_url%/}"
|
||
|
||
check_spa_route() {
|
||
local path="$1"
|
||
local name="$2"
|
||
local code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$web_url$path")
|
||
local content_type=$(curl -s -I --max-time 10 "$web_url$path" 2>/dev/null | grep -i "content-type" | tr -d '\r')
|
||
|
||
if [ "$code" = "200" ] && echo "$content_type" | grep -q "text/html"; then
|
||
pass "$name(200 + text/html)"
|
||
elif [ "$code" = "200" ]; then
|
||
fail "$name" "HTTP 200 但 content-type 不是 text/html"
|
||
else
|
||
fail "$name" "HTTP $code"
|
||
fi
|
||
}
|
||
|
||
check_spa_route "/" "首页"
|
||
check_spa_route "/app/dashboard" "/app/dashboard"
|
||
check_spa_route "/app/editing-planner" "/app/editing-planner"
|
||
check_spa_route "/app/assets" "/app/assets"
|
||
check_spa_route "/app/generate" "/app/generate"
|
||
check_spa_route "/app/templates" "/app/templates"
|
||
check_spa_route "/app/settings/profile" "深层路由 /app/settings/profile"
|
||
|
||
# /assets 特殊检查:不能返回 403(Nginx 目录列表问题)
|
||
local code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$web_url/assets")
|
||
if [ "$code" = "403" ]; then
|
||
fail "/assets 路由" "返回 403(Nginx 目录列表问题)"
|
||
else
|
||
pass "/assets 路由正常(非 403)"
|
||
fi
|
||
}
|
||
|
||
# ===== 清理 =====
|
||
cleanup() {
|
||
[ "$CLEANUP_ENABLED" != "1" ] && return 0
|
||
[ -z "$TOKEN" ] && return 0
|
||
|
||
section "清理测试数据"
|
||
|
||
for plan_id in "${CREATED_PLANS[@]}"; do
|
||
[ -z "$plan_id" ] && continue
|
||
curl -s -o /dev/null -X DELETE -H "$AUTH_HEADER" \
|
||
"$BASE_URL/api/v1/edit-plans/$plan_id" --max-time 10 || true
|
||
done
|
||
[ ${#CREATED_PLANS[@]} -gt 0 ] && info "清理了 ${#CREATED_PLANS[@]} 个剪辑计划"
|
||
|
||
for tpl_id in "${CREATED_TEMPLATES[@]}"; do
|
||
[ -z "$tpl_id" ] && continue
|
||
curl -s -o /dev/null -X DELETE -H "$AUTH_HEADER" \
|
||
"$BASE_URL/api/v1/templates/$tpl_id" --max-time 10 || true
|
||
done
|
||
[ ${#CREATED_TEMPLATES[@]} -gt 0 ] && info "清理了 ${#CREATED_TEMPLATES[@]} 个模板"
|
||
|
||
for proj_id in "${CREATED_PROJECTS[@]}"; do
|
||
[ -z "$proj_id" ] && continue
|
||
curl -s -o /dev/null -X DELETE -H "$AUTH_HEADER" \
|
||
"$BASE_URL/api/v1/projects/$proj_id" --max-time 10 || true
|
||
done
|
||
[ ${#CREATED_PROJECTS[@]} -gt 0 ] && info "清理了 ${#CREATED_PROJECTS[@]} 个项目"
|
||
|
||
echo "✅ 清理完成"
|
||
}
|
||
|
||
# ===== 主流程 =====
|
||
main() {
|
||
echo ""
|
||
echo "╔══════════════════════════════════════════════════╗"
|
||
echo "║ API E2E 冒烟测试 ║"
|
||
echo "╚══════════════════════════════════════════════════╝"
|
||
echo ""
|
||
echo "环境: $BASE_URL"
|
||
echo "模块: $MODULES"
|
||
echo "清理: $CLEANUP_ENABLED"
|
||
echo "开始时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
|
||
# 健康检查(不需要鉴权)
|
||
test_health
|
||
|
||
# 鉴权
|
||
if ! setup_auth; then
|
||
echo ""
|
||
echo "❌ 认证失败,终止测试"
|
||
exit 1
|
||
fi
|
||
|
||
# 业务模块
|
||
test_edit_plans
|
||
test_templates
|
||
test_assets
|
||
test_diagnosis
|
||
test_generation
|
||
test_subscription
|
||
test_misc
|
||
test_nginx
|
||
|
||
# 清理
|
||
cleanup
|
||
|
||
# 总结
|
||
echo ""
|
||
echo "============================================================"
|
||
echo "测试完成: $PASSED 通过, $FAILED 失败"
|
||
echo "============================================================"
|
||
|
||
if [ "$FAILED" -gt 0 ]; then
|
||
echo ""
|
||
echo "失败用例:"
|
||
echo -e "$FAIL_LIST"
|
||
echo ""
|
||
exit 1
|
||
else
|
||
echo ""
|
||
echo "🎉 全部通过!"
|
||
echo ""
|
||
exit 0
|
||
fi
|
||
}
|
||
|
||
main "$@"
|