#!/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 [跟随重定向]:只取状态码,连不上返回 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 :取响应体(用于断言统一信封) body() { curl -s -m "$TIMEOUT" "$1" 2>/dev/null || true } # expect_code <期望> <说明> [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 <必须包含的片段> <说明> # 只看 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 <说明>:未授权路径必须返回统一响应体(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