#!/usr/bin/env bash # deploy/zd-host.sh 的静态护栏 + 逻辑单测。 # # 为什么需要它:zd-host.sh 是「一跑就动线上」的部署脚本,没法在 CI 里真跑。 # 于是把可验证的部分拆成两层: # ① 静态护栏:对脚本正文做断言(令牌只打印长度、没有假数据路径、env 文件必删) # ② 逻辑抽取:把 human_history_count 那段函数原样抠出来,用 stub curl 单测 # # 用法:bash deploy/zd-host_test.sh (退出码 0 = 全绿) # 依赖:bash / grep / sed / awk / mktemp,无网络、无 docker。 set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TARGET="$HERE/zd-host.sh" PASS=0 FAIL=0 ok() { PASS=$((PASS+1)); printf ' \033[32m✓\033[0m %s\n' "$1"; } bad() { FAIL=$((FAIL+1)); printf ' \033[31m✗\033[0m %s\n' "$1"; [ -n "${2:-}" ] && printf ' %s\n' "$2"; } head_() { printf '\n== %s ==\n' "$1"; } [ -f "$TARGET" ] || { echo "找不到 $TARGET" >&2; exit 1; } # ──────────────────────────────────────────────────────────── head_ "1. 语法检查(bash -n)" # ──────────────────────────────────────────────────────────── if bash -n "$TARGET" 2>/tmp/zdnsyn.$$; then ok "bash -n deploy/zd-host.sh 通过" else bad "bash -n 失败" "$(cat /tmp/zdnsyn.$$)" fi rm -f /tmp/zdnsyn.$$ # ──────────────────────────────────────────────────────────── head_ "2a. 令牌轮换只打印长度,不打印值" # ──────────────────────────────────────────────────────────── # 轮换段必须用 ${#VAR}(长度)形式打印 len_prints=$(grep -c '新值长度 \${#NAVI_ROTATE_' "$TARGET" || true) if [ "${len_prints:-0}" -ge 2 ]; then ok "两处轮换都只打印长度(${len_prints} 处)" else bad "令牌轮换的长度打印少于 2 处(实际 ${len_prints:-0})" fi # 不得出现「echo ... $NAVI_ROTATE_xxx 的值本身」这种写法: # 即 echo 行里引用变量却没有用 ${#VAR} 取长度。 # 例外:`echo "NAVI_ADMIN_TOKEN=..." >> "$ENV_FILE.t"` 是**写进 env 文件**(容器启动要用), # 不是打到 stdout/日志,因此排除带 `>>` 重定向的行。 leak=$(grep -nE '^[^#]*echo[^#]*\$\{?NAVI_ROTATE_(ADMIN|SYNC)_TOKEN' "$TARGET" \ | grep -v '\${#NAVI_ROTATE_' \ | grep -v '>>' || true) if [ -z "$leak" ]; then ok "没有 echo 直接打印 NAVI_ROTATE_*_TOKEN 的值" else bad "发现可能打印令牌值的 echo 行" "$leak" fi # 全脚本不得出现 set -x(会把 env/变量回显到日志) if grep -qE '^[[:space:]]*set[[:space:]]+-[a-z]*x' "$TARGET"; then bad "脚本里出现 set -x,会把变量值回显到日志" else ok "没有 set -x" fi # ──────────────────────────────────────────────────────────── head_ "2b. ADMIN_TOK 缺失时不得打印「历史条数 0」" # ──────────────────────────────────────────────────────────── if grep -q 'if \[ -z "\$tok" \]' "$TARGET"; then ok "human_history_count 里有 token 为空的分支" else bad "human_history_count 缺少「token 为空」分支" fi if grep -q '未知(未配置 NAVI_ADMIN_TOKEN,跳过探测)' "$TARGET"; then ok "空 token 时输出「未知(…跳过探测)」" else bad "空 token 分支没有输出「未知」文案" fi # 假的 0 从哪来:旧写法 `| grep -o '"url"' | wc -l || echo 0` + `HIST=${HIST:-0}`。 if grep -qE 'HIST=\$\{HIST:-0\}' "$TARGET"; then bad "仍有 HIST=\${HIST:-0} 兜底,会把「取不到」变成「0 条」" else ok "没有 HIST=\${HIST:-0} 假数据兜底" fi if grep -nE 'grep -o .\"url\". \| wc -l \|\| echo 0' "$TARGET" >/dev/null 2>&1; then bad "仍有「grep url | wc -l || echo 0」的历史条数写法" else ok "没有「grep url | wc -l || echo 0」的旧写法" fi # 必须校验 HTTP 状态码(非 200 不数 body) if grep -q "code.*!= \"200\"" "$TARGET"; then ok "探测时校验 HTTP 状态码,非 200 不数 body" else bad "探测没有校验 HTTP 状态码" fi if grep -q "未知(HTTP " "$TARGET"; then ok "非 200 时输出「未知(HTTP xxx)」" else bad "非 200 时没有「未知(HTTP xxx)」输出" fi # ──────────────────────────────────────────────────────────── head_ "2c. 明文令牌的 env 文件在成功与失败路径都必须删除" # ──────────────────────────────────────────────────────────── fail_body=$(awk '/^fail\(\) \{/,/^}/' "$TARGET") if printf '%s' "$fail_body" | grep -q 'cleanup_env_file'; then ok "fail() 回滚路径会调 cleanup_env_file" else bad "fail() 回滚路径没有删 env 文件" fi if grep -qE '^\s*trap cleanup_env_file EXIT' "$TARGET"; then ok "设置了 EXIT trap(异常退出也会删)" else bad "没有 EXIT trap 兜底删除 env 文件" fi # 成功路径:在 rm -f "$REMOTE_TAR" 附近也要有一次显式 cleanup if grep -qE '^cleanup_env_file' "$TARGET"; then ok "成功收尾路径显式调 cleanup_env_file" else bad "成功收尾路径没有显式删 env 文件" fi if grep -q 'rm -f "\$ENV_FILE" "\$ENV_FILE.t" "\$ENV_FILE.new"' "$TARGET"; then ok "cleanup 同时删 .t / .new 中间文件" else bad "cleanup 漏删中间文件(.t / .new 也含明文令牌)" fi # ──────────────────────────────────────────────────────────── head_ "3. human_history_count 逻辑单测(stub curl)" # ──────────────────────────────────────────────────────────── TMPD=$(mktemp -d) trap 'rm -rf "$TMPD"' EXIT # 抽取函数体(由脚本里的 BEGIN/END 标记界定;没标记说明被人删了标记,直接失败) awk '/^# >>> human_history_count BEGIN/,/^# <<< human_history_count END/' "$TARGET" > "$TMPD/func.sh" if [ -s "$TMPD/func.sh" ]; then ok "成功抽取 human_history_count 函数" else bad "抽取 human_history_count 失败(BEGIN/END 标记缺失?)" fi # stub curl:用环境变量控制行为,并记录「是否被调用」 mkdir -p "$TMPD/bin" cat > "$TMPD/bin/curl" <<'STUB' #!/usr/bin/env bash # 只实现被 human_history_count 用到的那几个参数:-s -m N -o FILE -w '%{http_code}' -H ... out="" while [ $# -gt 0 ]; do case "$1" in -o) out="$2"; shift 2 ;; -H|-m) shift 2 ;; -w) shift 2 ;; -*) shift ;; *) shift ;; esac done echo "called" >> "${STUB_CALLS:-/dev/null}" case "${STUB_MODE:-ok}" in ok) [ -n "$out" ] && printf '%s' "${STUB_BODY:-}" > "$out"; printf '200'; exit 0 ;; 401) [ -n "$out" ] && printf '%s' '{"error":"invalid or missing token"}' > "$out"; printf '401'; exit 0 ;; 502) [ -n "$out" ] && printf '%s' '502 Bad Gateway' > "$out"; printf '502'; exit 0 ;; conn) # 连不上:curl 会打印 000 并返回非 0 [ -n "$out" ] && : > "$out"; printf '000'; exit 7 ;; esac exit 0 STUB chmod +x "$TMPD/bin/curl" run_hist() { # $1=token ; 其余由 STUB_* 控制 PATH="$TMPD/bin:$PATH" STUB_CALLS="$TMPD/calls" \ bash -c 'set -uo pipefail; . "$1"; human_history_count "$2"' _ "$TMPD/func.sh" "$1" } BODY3='{"items":[{"url":"https://a.example.com/"},{"url":"https://b.example.com/"},{"url":"https://c.example.com/"}],"total":3}' # 3.1 空 token → 跳过探测,输出「未知」,且**不能**调用 curl(更不能打印 0) rm -f "$TMPD/calls" got=$(STUB_MODE=ok STUB_BODY="$BODY3" run_hist "" ) if printf '%s' "$got" | grep -q '未知' && ! printf '%s' "$got" | grep -qx '0'; then ok "空 token → 「未知」而不是 0(输出: $got)" else bad "空 token 时输出了非预期内容" "got=[$got]" fi if [ ! -f "$TMPD/calls" ]; then ok "空 token 时完全没有发请求(没带空 Bearer 打过去)" else bad "空 token 时仍然调用了 curl" fi # 3.2 有 token 但接口 401 → 「未知(HTTP 401)」,不是 0 got=$(STUB_MODE=401 run_hist "some-admin-token") if printf '%s' "$got" | grep -q '未知(HTTP 401' ; then ok "401 → 「未知(HTTP 401)」(输出: $got)" else bad "401 时没有输出「未知(HTTP 401)」" "got=[$got]" fi if printf '%s' "$got" | grep -qx '0'; then bad "401 时把响应体当数据数出了 0 条" else ok "401 时不会数 body" fi # 3.3 502(body 是 HTML)→ 「未知(HTTP 502)」,不得把 HTML 当数据 got=$(STUB_MODE=502 run_hist "some-admin-token") if printf '%s' "$got" | grep -q '未知(HTTP 502'; then ok "502 → 「未知(HTTP 502)」" else bad "502 时没有输出「未知(HTTP 502)」" "got=[$got]" fi # 3.4 连不上(curl 打印 000 且返回非 0)→ 「未知(HTTP 000)」,且提示里不出现重复的 000 got=$(STUB_MODE=conn run_hist "some-admin-token") if printf '%s' "$got" | grep -q '未知(HTTP 000'; then ok "连接失败 → 「未知(HTTP 000)」(输出: $got)" else bad "连接失败时输出不符合预期" "got=[$got]" fi if printf '%s' "$got" | grep -qE '000[[:space:]]*000'; then bad "状态码被拼成了两个 000" "got=[$got]" else ok "状态码已归一化(没有重复 000)" fi # 3.5 200 且 3 条 → 输出 3 got=$(STUB_MODE=ok STUB_BODY="$BODY3" run_hist "some-admin-token") if [ "$got" = "3" ]; then ok "200 + 3 条 → 输出 3" else bad "200 时条数不对" "got=[$got] want=[3]" fi # 3.6 200 但 0 条 → 输出 0(这是**真实**的 0,必须能显示出来) got=$(STUB_MODE=ok STUB_BODY='{"items":[],"total":0}' run_hist "some-admin-token") if [ "$got" = "0" ]; then ok "200 + 空列表 → 输出 0(真实的 0 仍然显示)" else bad "真实 0 条时输出不对" "got=[$got] want=[0]" fi # 3.7 有 token 时确实带上了 Authorization 头(用 stub 校验参数) cat > "$TMPD/bin/curl" <<'STUB2' #!/usr/bin/env bash args="$*" echo "$args" >> "$STUB_ARGS" out="" while [ $# -gt 0 ]; do case "$1" in -o) out="$2"; shift 2 ;; *) shift ;; esac done [ -n "$out" ] && printf '{"items":[{"url":"x"}]}' > "$out" printf '200' STUB2 chmod +x "$TMPD/bin/curl" rm -f "$TMPD/args" PATH="$TMPD/bin:$PATH" STUB_ARGS="$TMPD/args" \ bash -c 'set -uo pipefail; . "$1"; human_history_count "tok-abc"' _ "$TMPD/func.sh" >/dev/null if grep -q 'Authorization: Bearer tok-abc' "$TMPD/args"; then ok "有 token 时请求带 Authorization: Bearer" else bad "请求没带 Authorization 头" "$(cat "$TMPD/args" 2>/dev/null)" fi if grep -q '/admin/api/history' "$TMPD/args"; then ok "探测打的是 /admin/api/history(不是根 /api/)" else bad "探测地址不对" "$(cat "$TMPD/args" 2>/dev/null)" fi # ──────────────────────────────────────────────────────────── printf '\n== 结果 ==\n通过 %d,失败 %d\n' "$PASS" "$FAIL" [ "$FAIL" -eq 0 ] || exit 1