Files
FruitServer/tools/smoke.sh
T
jianghaifeng cbb9961627 fix(deploy): 冒烟脚本与 prod Swagger 开关解耦,默认改联调口径
为什么:prod profile 下 SWAGGER_ENABLED 默认 false 会把 /doc.html 与 /v3/api-docs 一起关掉,
而 smoke.sh 硬期望这两者 200 —— 照文档默认配置部署完,冒烟必然假失败,会被误判成"部署挂了";
且联调(10-19)需要 /doc.html 在线可查。

- smoke.sh 自适应:显式 SWAGGER_ENABLED 优先;未设置则自动探测(200+openapi=开、统一信封
  code=404=关、其它响应仍 FAIL);关闭时这三项打印 SKIP 而不是 FAIL,开启时照旧断言 200(断言未删)
- 新增 SWAGGER_REQUIRED 硬开关:联调环境可要求文档必须在线,不满足直接失败
- .env.example 默认改 SWAGGER_ENABLED=true,并写明该开关一次管两个端点
- DEPLOY.md 同步口径与冒烟说明
- 实测:prod+false 时 SKIP + 其余全 PASS + 退出码 0;prod+true 时两端口 /v3/api-docs 与 /doc.html 均 200;
  mvn test 83 条全过、两入口 package 通过
2026-10-16 13:17:29 +08:00

156 lines
6.3 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Author: fruit.com.cn
#
# 发布后冒烟:确认两个入口"活着 + 文档在线 + 未授权路径按统一信封拒绝"。
# 任一检查失败 → 非 0 退出(可直接用在部署流水线里)。
#
# 用法:
# tools/smoke.sh # 默认 127.0.0.1:8081 / 127.0.0.1:8080
# APP_BASE=http://10.0.0.5:8081 ADMIN_BASE=http://10.0.0.5:8080 tools/smoke.sh
set -u -o pipefail
APP_BASE="${APP_BASE:-http://127.0.0.1:8081}"
ADMIN_BASE="${ADMIN_BASE:-http://127.0.0.1:8080}"
TIMEOUT="${SMOKE_TIMEOUT:-8}"
FAILED=0
pass() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; FAILED=1; }
skip() { printf ' \033[33mSKIP\033[0m %s\n' "$1"; }
# 在线接口文档是否开启:
# 显式 SWAGGER_ENABLED=true/false 时按它;
# 未设置则自动探测 —— 只有"200 且含 openapi"才算开启,"统一信封 code=404"才算关闭,
# 其它响应(500/挂住/异常)一律算 broken(由调用方判 FAIL),避免"探测不出来就跳过"掩盖问题。
swagger_state() {
local explicit="${SWAGGER_ENABLED:-auto}" out code
case "$explicit" in
true) echo enabled; return ;;
false) echo disabled; return ;;
esac
code="$(http_code "$APP_BASE/v3/api-docs")"
out="$(body "$APP_BASE/v3/api-docs")"
if [ "$code" = "200" ] && printf '%s' "$out" | grep -q '"openapi"'; then
echo enabled
elif printf '%s' "$out" | grep -q '"code":404'; then
echo disabled
else
echo broken
fi
}
# 关闭原因:区分"显式配置关闭"与"自动探测到未开启",避免 SKIP 文案误导
swagger_disabled_reason() {
if [ "${SWAGGER_ENABLED:-auto}" = "false" ]; then
echo "SWAGGER_ENABLED=false,接口文档按配置关闭"
else
echo "自动探测:/v3/api-docs 返回 404 信封,说明接口文档未开启"
fi
}
# http_code <url> [跟随重定向]:只取状态码,连不上返回 000
http_code() {
local follow="${2:-}"
if [ "$follow" = "follow" ]; then
curl -sL -o /dev/null -m "$TIMEOUT" -w '%{http_code}' "$1" 2>/dev/null || echo 000
else
curl -s -o /dev/null -m "$TIMEOUT" -w '%{http_code}' "$1" 2>/dev/null || echo 000
fi
}
# body <url>:取响应体(用于断言统一信封)
body() {
curl -s -m "$TIMEOUT" "$1" 2>/dev/null || true
}
# expect_code <期望> <url> <说明> [follow]
expect_code() {
local want="$1" url="$2" label="$3" follow="${4:-}" got
got="$(http_code "$url" "$follow")"
if [ "$got" = "$want" ]; then
pass "${label}(${got})"
else
fail "${label}:期望 ${want},实际 ${got} —— ${url}"
fi
}
# expect_body <url> <必须包含的片段> <说明>
# 只看 HTTP 200 是不够的:本项目未授权时也是 HTTP 200 + 信封 code=401,
# 所以健康检查/文档必须断言响应体的关键字段。
expect_body() {
local url="$1" needle="$2" label="$3" out
out="$(body "$url")"
if printf '%s' "$out" | grep -q "$needle"; then
pass "${label}"
else
fail "${label}:期望包含 ${needle},实际:$(printf '%s' "$out" | head -c 120)"
fi
}
# expect_envelope <url> <说明>:未授权路径必须返回统一响应体(code 字段),而不是挂住/空响应
expect_envelope() {
local url="$1" label="$2" out
out="$(body "$url")"
if printf '%s' "$out" | grep -q '"code"'; then
pass "${label}($(printf '%s' "$out" | head -c 80))"
else
fail "$label:未返回统一响应体 —— $url,实际:$(printf '%s' "$out" | head -c 120)"
fi
}
printf '冒烟开始:APP=%s ADMIN=%s\n' "$APP_BASE" "$ADMIN_BASE"
printf '\n[1/5] 健康检查 /healthz\n'
expect_code 200 "$APP_BASE/healthz" "app-api /healthz 可访问"
expect_body "$APP_BASE/healthz" '"status":"UP"' "app-api /healthz 返回 status=UP"
expect_code 200 "$ADMIN_BASE/healthz" "admin-api /healthz 可访问"
expect_body "$ADMIN_BASE/healthz" '"status":"UP"' "admin-api /healthz 返回 status=UP"
printf '\n[2/5] 客户端目录接口\n'
expect_code 200 "$APP_BASE/app/categories" "GET /app/categories"
expect_code 200 "$APP_BASE/app/home/feed" "GET /app/home/feed"
expect_body "$APP_BASE/app/categories" '"code":0' "GET /app/categories 返回 code=0"
printf '\n[3/5] 接口文档在线\n'
case "$(swagger_state)" in
enabled)
# SWAGGER_ENABLED=true:/doc.html 与 /v3/api-docs 都必须在线(前端联调要看)
expect_code 200 "$APP_BASE/v3/api-docs" "app-api /v3/api-docs"
expect_code 200 "$ADMIN_BASE/v3/api-docs" "admin-api /v3/api-docs"
expect_body "$APP_BASE/v3/api-docs" '"openapi"' "app-api /v3/api-docs 是 OpenAPI 文档"
# /doc.html 是 springdoc 的 302 跳转,跟随重定向后应落到 Swagger UI(200)
expect_code 200 "$APP_BASE/doc.html" "app-api /doc.html(跟随 302)" follow
;;
disabled)
# SWAGGER_ENABLED=false(prod 允许的配置):文档按配置关闭,不是故障 → SKIP,不能算失败
skip "app-api /v3/api-docs —— $(swagger_disabled_reason)"
skip "admin-api /v3/api-docs —— 同上"
skip "app-api /doc.html —— 同上(联调环境请在 .env 里设 SWAGGER_ENABLED=true)"
;;
*)
fail "接口文档状态异常:既不是 200 的 OpenAPI 文档,也不是【按配置关闭】的统一信封 —— 请人工确认"
;;
esac
if [ "${SWAGGER_REQUIRED:-false}" = "true" ] && [ "$(swagger_state)" != "enabled" ]; then
# 联调/演示环境可以强制要求文档在线:给流水线一个硬开关
fail "SWAGGER_REQUIRED=true 但接口文档不可用(请检查 SWAGGER_ENABLED 与 springdoc 配置)"
fi
printf '\n[4/5] 未授权路径按统一信封拒绝(不挂住)\n'
expect_envelope "$APP_BASE/" "app-api /"
expect_envelope "$ADMIN_BASE/admin/auth/profile" "admin-api 未带令牌访问受保护接口"
printf '\n[5/5] 响应体完整性(/app/categories 必须是 {code,data})\n'
expect_body "$APP_BASE/app/categories" '"data"' "GET /app/categories 带 data 字段"
# 用错误的 HTTP 方法访问 POST-only 接口:只要是"统一信封"就算合格(当前实现返回 code=500 的系统异常信封)
expect_envelope "$ADMIN_BASE/admin/auth/login" "admin-api 用 GET 访问 POST-only 接口仍返回统一信封"
if [ "$FAILED" -eq 0 ]; then
printf '\n\033[32m冒烟通过\033[0m:全部检查通过\n'
exit 0
fi
printf '\n\033[31m冒烟失败\033[0m:存在未通过项,请先看上面的 FAIL 行\n'
exit 1