Files
webui4frpc/scripts/criteria/config-startup.sh
JianFeeeee 42765282a3 test(criteria): 判据自行补齐前端嵌入产物,保证在干净 clone 上可复现
internal/httpapi 用 //go:embed all:dist 嵌入前端,dist 是构建期产物且被
.gitignore 排除。干净 clone 上直接 go build 会报
"pattern all:dist: no matching files found"——这是基线既有状态,与本判据
要验证的东西无关,但会让判据在任意 checkout 上都红,失去可复现性。

现在若 dist 缺失就先 cd web 构建并拷回 internal/httpapi/dist(无 npm 时
给出明确的失败原因与手工步骤)。已实测:删掉 dist 后跑判据可自动补齐并全绿。
2026-10-23 09:19:27 +08:00

136 lines
6.7 KiB
Bash
Executable File
Raw 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.

#!/bin/bash
# 判据:webui4frpc 支持从配置文件启动,且口令不泄漏。
#
# 覆盖三件事:
# 1. go build ./... 与 go test ./... 全绿(含配置文件优先级的回归测试);
# 2. 优先级 显式 flag > 配置文件 > 内置默认值 —— 未显式传的 flag 不得用
# 默认值压过配置文件;
# 3. 用配置文件(file: 引用口令)启动后,明文口令既不出现在进程命令行,
# 也不出现在启动日志;坏 JSON 必须非零退出,文件缺失必须能正常启动。
#
# 退出码 0 = 通过。
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT" || { echo "FAIL: 无法进入仓库根 $ROOT"; exit 1; }
WORK="$(mktemp -d)"
PIDS=()
cleanup() { for p in "${PIDS[@]:-}"; do kill "$p" 2>/dev/null; done; wait 2>/dev/null; rm -rf "$WORK"; }
trap cleanup EXIT
fail() { echo "FAIL: $*"; exit 1; }
# 取一个空闲端口。判据里必须用随机空闲端口:默认 127.0.0.1:7500 上通常已有
# 生产实例在跑,端口冲突造成的非零退出会被误判成"配置错误已正确报错"。
free_port() {
python3 -c 'import socket
s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()'
}
# 断言:进程在配置文件非法时"没进入监听"就必须死。
# 只检查退出码不够——错误串里本来就含 "invalid JSON",实现把 Fatal 改成
# 打日志后继续跑,退出码可能因为端口冲突等原因仍非零。真正的判据是它不能再
# 继续提供服务,所以出现 listening 日志即判失败。
assert_died_before_listen() {
local log="$1" why="$2"
if grep -q 'listening on' "$log"; then
cat "$log"
fail "$why:配置错误被静默忽略后仍进入监听(应启动前就退出)"
fi
}
# ---- 0. 前端嵌入产物 ----
# internal/httpapi 用 //go:embed all:dist 嵌入前端,而 dist 是构建期产物、
# 被 .gitignore 排除。干净 clone 上不先构建前端,go build 会直接报
# "pattern all:dist: no matching files found"——那是基线既有状态,与本判据
# 无关,所以这里先补齐,让判据在任何 checkout 上都能复现。
if [ ! -f internal/httpapi/dist/index.html ]; then
command -v npm >/dev/null 2>&1 \
|| fail "缺少前端构建产物 internal/httpapi/dist 且没有 npm;请在有 node 的环境跑,或先手工 cd web && npm ci && npm run build && cp -r dist ../internal/httpapi/dist"
( cd web \
&& { [ -d node_modules ] || npm ci --no-audit --no-fund >/dev/null 2>&1; } \
&& npm run build >/dev/null 2>&1 ) || fail "前端构建失败(cd web && npm run build)"
rm -rf internal/httpapi/dist && mkdir -p internal/httpapi/dist
cp -r web/dist/. internal/httpapi/dist/ || fail "拷贝前端产物失败"
fi
# ---- 1. 构建与全量测试 ----
go build ./... || fail "go build ./..."
go test ./... > "$WORK/test.log" 2>&1 || { cat "$WORK/test.log"; fail "go test ./..."; }
# ---- 2. 优先级回归测试必须存在且通过 ----
# 这是整个改动最容易做错的一点:Go 的 flag 默认值会无条件覆盖配置文件。
go test ./internal/appconfig/ -count=1 \
-run 'TestResolveFileBeatsFlagDefaults|TestResolveExplicitFlagBeatsFile|TestLoadRejectsUnknownKey|TestSummaryRedactsSecrets' \
> "$WORK/appconfig.log" 2>&1 || { cat "$WORK/appconfig.log"; fail "appconfig 优先级/脱敏回归测试未通过"; }
# ---- 3. 端到端:配置文件启动,口令不落到命令行与日志 ----
BIN="$WORK/w4f"
go build -o "$BIN" ./cmd/webui4frpc || fail "构建二进制失败"
SECRET="CRIT_SECRET_${RANDOM}_9f3a"
printf '%s\n' "$SECRET" > "$WORK/pw"
chmod 600 "$WORK/pw"
PORT="$(free_port)"
python3 -c '
import json, sys
t, addr = sys.argv[1], sys.argv[2]
json.dump({"addr": addr, "user": "crituser",
"password": "file:" + t + "/pw", "workdir": t + "/data"},
open(t + "/cfg.json", "w"))
' "$WORK" "127.0.0.1:$PORT"
# 注意:-config 只是路径,口令来自 file: 引用,命令行本身不含秘密。
"$BIN" -config "$WORK/cfg.json" > "$WORK/run.log" 2>&1 &
P=$!
PIDS+=("$P")
sleep 3
kill -0 "$P" 2>/dev/null || { cat "$WORK/run.log"; fail "进程未能从配置文件启动"; }
if tr '\0' ' ' < "/proc/$P/cmdline" | grep -q -- "$SECRET"; then
fail "明文口令出现在进程命令行(ps 可见)"
fi
if grep -q -- "$SECRET" "$WORK/run.log"; then
cat "$WORK/run.log"
fail "明文口令出现在启动日志"
fi
grep -q "listening on 127.0.0.1:$PORT" "$WORK/run.log" \
|| { cat "$WORK/run.log"; fail "配置文件里的 addr 未生效(未在 $PORT 上监听)"; }
kill "$P" 2>/dev/null; wait "$P" 2>/dev/null
# ---- 4. 文件存在但 JSON 非法 -> 响亮报错退出 ----
# 用 timeout 兜底:若实现退化成"只记日志继续启动",进程会一直跑而不是报错,
# 这里必须把它判成失败,不能把自己挂死。
printf '{"addr":' > "$WORK/bad.json"
timeout 10 "$BIN" -config "$WORK/bad.json" \
-addr "127.0.0.1:$(free_port)" -workdir "$WORK/d_bad" > "$WORK/bad.log" 2>&1
rc=$?
[ "$rc" -eq 124 ] && { cat "$WORK/bad.log"; fail "配置文件 JSON 非法时进程没有退出(静默忽略),判据要求响亮报错"; }
[ "$rc" -eq 0 ] && { cat "$WORK/bad.log"; fail "配置文件 JSON 非法时应非零退出,实际正常退出"; }
assert_died_before_listen "$WORK/bad.log" "JSON 非法"
grep -qi 'invalid JSON' "$WORK/bad.log" || { cat "$WORK/bad.log"; fail "非法 JSON 的报错信息不明确"; }
# ---- 5. 未知键(拼写错误)-> 报错退出,不得静默回落到默认口令 ----
printf '{"passwd":"x"}' > "$WORK/typo.json"
timeout 10 "$BIN" -config "$WORK/typo.json" \
-addr "127.0.0.1:$(free_port)" -workdir "$WORK/d_typo" > "$WORK/typo.log" 2>&1
rc=$?
[ "$rc" -eq 124 ] && { cat "$WORK/typo.log"; fail "未知键被静默忽略(进程继续运行),拼错的 password 会回落到默认值"; }
[ "$rc" -eq 0 ] && { cat "$WORK/typo.log"; fail "未知键应被拒绝(否则拼错的 password 会静默使用默认值)"; }
assert_died_before_listen "$WORK/typo.log" "未知键"
# ---- 6. 配置文件缺失 -> 不算错误,回落到纯 flag 行为 ----
"$BIN" -config "$WORK/none.json" -addr "127.0.0.1:$(free_port)" -workdir "$WORK/d2" > "$WORK/miss.log" 2>&1 &
P2=$!
PIDS+=("$P2")
sleep 3
kill -0 "$P2" 2>/dev/null || { cat "$WORK/miss.log"; fail "配置文件缺失时应回落到纯 flag 行为并正常启动"; }
kill "$P2" 2>/dev/null; wait "$P2" 2>/dev/null
# ---- 7. 交付的单元/示例配置不得携带明文口令 ----
if grep -rn -- '-password' packaging/common/webui4frpc.service \
packaging/common/config.json scripts/release.sh 2>/dev/null \
| grep -v 'never as -password'; then
fail "打包产物(单元/示例配置/分发模板)仍含命令行口令"
fi
echo "OK: 配置文件启动与口令不泄漏的判据全部通过"