games-development-ai/deploy/tests/test-import-newapi-pool.sh

135 lines
7.9 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.

#!/usr/bin/env bash
# new-api 额度池导入回归:使用假 hostname/SSH不连接 mini-desktop、不写真实 MySQL。
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT="$ROOT_DIR/deploy/import-newapi-pool.sh"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
fail() { printf 'TEST_FAIL %s\n' "$*" >&2; exit 1; }
mode_of() { stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"; }
[ -x "$SCRIPT" ] || fail "缺少可执行额度池导入器"
bash -n "$SCRIPT"
rg -q "readonly MYSQL_DATABASE='ruoyi-vue-pro'" "$SCRIPT" || fail "目标数据库不是固定值"
rg -q "readonly POOL_TABLE='newapi_quota_pool'" "$SCRIPT" || fail "目标表不是固定值"
rg -q 'START TRANSACTION' "$SCRIPT" || fail "导入未显式开启事务"
rg -q 'COMMIT' "$SCRIPT" || fail "导入未显式提交事务"
rg -q "stat -c '%a'.*stat -f '%Lp'" "$SCRIPT" || fail "权限检查未优先使用 GNU stat"
cat >"$TMP_DIR/hostname" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "${MOCK_HOSTNAME_VALUE:-mini-infra}"
SH
cat >"$TMP_DIR/ssh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >>"$MOCK_SSH_ARGS"
cat >"$MOCK_SQL_CAPTURE"
rg -q 'sk-import-secret-must-not-leak' "$MOCK_SQL_CAPTURE"
rows="$(rg -c '^INSERT INTO newapi_quota_pool ' "$MOCK_SQL_CAPTURE")"
[ "$rows" = 4 ]
before="$(cat "$MOCK_IMPORT_STATE" 2>/dev/null || printf 0)"
printf 'BEFORE\t%s\t%s\t0\t0\t%s\n' "$before" "$before" "$before"
printf '%s\n' 4 >"$MOCK_IMPORT_STATE"
printf 'AFTER\t4\t4\t0\t0\t4\n'
SH
chmod +x "$TMP_DIR/hostname" "$TMP_DIR/ssh"
cat >"$TMP_DIR/pool.sql" <<'SQL'
-- 仅供本地假 SSH 回归,不接触真实数据库。
-- pool_entry username=neice_051 newapi_user_id=51
INSERT INTO newapi_quota_pool (newapi_user_id, newapi_token_id, newapi_token_key, grant_quota, quota_per_unit_snapshot, usd_rate_snapshot, status) VALUES (51, 151, 'sk-import-secret-must-not-leak-51', 6849315, 500000, 7.3, 'FREE') ON DUPLICATE KEY UPDATE newapi_token_id=VALUES(newapi_token_id), newapi_token_key=VALUES(newapi_token_key), grant_quota=VALUES(grant_quota), quota_per_unit_snapshot=VALUES(quota_per_unit_snapshot), usd_rate_snapshot=VALUES(usd_rate_snapshot);
-- pool_entry username=neice_052 newapi_user_id=52
INSERT INTO newapi_quota_pool (newapi_user_id, newapi_token_id, newapi_token_key, grant_quota, quota_per_unit_snapshot, usd_rate_snapshot, status) VALUES (52, 152, 'sk-import-secret-must-not-leak-52', 6849315, 500000, 7.3, 'FREE') ON DUPLICATE KEY UPDATE newapi_token_id=VALUES(newapi_token_id), newapi_token_key=VALUES(newapi_token_key), grant_quota=VALUES(grant_quota), quota_per_unit_snapshot=VALUES(quota_per_unit_snapshot), usd_rate_snapshot=VALUES(usd_rate_snapshot);
-- pool_entry username=neice_053 newapi_user_id=53
INSERT INTO newapi_quota_pool (newapi_user_id, newapi_token_id, newapi_token_key, grant_quota, quota_per_unit_snapshot, usd_rate_snapshot, status) VALUES (53, 153, 'sk-import-secret-must-not-leak-53', 6849315, 500000, 7.3, 'FREE') ON DUPLICATE KEY UPDATE newapi_token_id=VALUES(newapi_token_id), newapi_token_key=VALUES(newapi_token_key), grant_quota=VALUES(grant_quota), quota_per_unit_snapshot=VALUES(quota_per_unit_snapshot), usd_rate_snapshot=VALUES(usd_rate_snapshot);
-- pool_entry username=neice_054 newapi_user_id=54
INSERT INTO newapi_quota_pool (newapi_user_id, newapi_token_id, newapi_token_key, grant_quota, quota_per_unit_snapshot, usd_rate_snapshot, status) VALUES (54, 154, 'sk-import-secret-must-not-leak-54', 6849315, 500000, 7.3, 'FREE') ON DUPLICATE KEY UPDATE newapi_token_id=VALUES(newapi_token_id), newapi_token_key=VALUES(newapi_token_key), grant_quota=VALUES(grant_quota), quota_per_unit_snapshot=VALUES(quota_per_unit_snapshot), usd_rate_snapshot=VALUES(usd_rate_snapshot);
SQL
chmod 600 "$TMP_DIR/pool.sql"
run_import() {
local evidence="$1"
NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' \
NEWAPI_POOL_SQL_FILE="$TMP_DIR/pool.sql" \
NEWAPI_POOL_IMPORT_EVIDENCE="$evidence" \
NEWAPI_POOL_IMPORT_HOST='root@100.64.0.7' \
HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" \
MOCK_HOSTNAME_VALUE="${MOCK_HOSTNAME_VALUE:-mini-infra}" \
MOCK_SSH_ARGS="$TMP_DIR/ssh-args" MOCK_SQL_CAPTURE="$TMP_DIR/captured.sql" \
MOCK_IMPORT_STATE="$TMP_DIR/import-state" \
"$SCRIPT"
}
run_import "$TMP_DIR/import-first.evidence" >"$TMP_DIR/first.log"
run_import "$TMP_DIR/import-second.evidence" >"$TMP_DIR/second.log"
MOCK_HOSTNAME_VALUE=minione-ubuntu-infra run_import "$TMP_DIR/import-runtime-host.evidence" \
>"$TMP_DIR/runtime-host.log"
for evidence in "$TMP_DIR/import-first.evidence" "$TMP_DIR/import-second.evidence" \
"$TMP_DIR/import-runtime-host.evidence"; do
[ "$(mode_of "$evidence")" = 400 ] || fail "脱敏证据权限不是 400$evidence"
rg -q '^status=PASS$' "$evidence" || fail "证据未记录 PASS"
rg -q '^source_row_count=4$' "$evidence" || fail "证据未记录四条输入"
rg -q '^after_target_total=4$' "$evidence" || fail "导入后目标聚合不为四条"
rg -q '^source_sql_sha256=[0-9a-f]{64}$' "$evidence" || fail "证据缺 SQL 哈希"
! rg -q 'sk-import-secret-must-not-leak' "$evidence" || fail "证据泄漏 token"
done
rg -q '^before_target_total=4$' "$TMP_DIR/import-second.evidence" || fail "第二次导入未证明幂等"
rg -q 'ProxyCommand=none' "$TMP_DIR/ssh-args" || fail "SSH 未禁用代理"
rg -q 'root@100\.64\.0\.7' "$TMP_DIR/ssh-args" || fail "未限定 mini-desktop Tailscale 主机"
! rg -q 'sk-import-secret-must-not-leak' "$TMP_DIR/first.log" "$TMP_DIR/second.log" || fail "日志泄漏 token"
# ACK、执行宿主、远端目标和 SQL 结构均必须在 SSH 前 fail-closed。
calls_before="$(wc -l <"$TMP_DIR/ssh-args" | tr -d ' ')"
if NEWAPI_POOL_IMPORT_ACK=wrong NEWAPI_POOL_SQL_FILE="$TMP_DIR/pool.sql" \
HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" "$SCRIPT" >/dev/null 2>&1; then
fail "错误 ACK 未被拒绝"
fi
if MOCK_HOSTNAME_VALUE=mini-desktop NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' \
NEWAPI_POOL_SQL_FILE="$TMP_DIR/pool.sql" HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" \
"$SCRIPT" >/dev/null 2>&1; then
fail "非 mini-infra 宿主未被拒绝"
fi
if NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' NEWAPI_POOL_SQL_FILE="$TMP_DIR/pool.sql" \
NEWAPI_POOL_IMPORT_HOST='root@100.64.0.8' HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" \
"$SCRIPT" >/dev/null 2>&1; then
fail "非 mini-desktop 目标未被拒绝"
fi
cp "$TMP_DIR/pool.sql" "$TMP_DIR/bad.sql"
printf 'DROP TABLE newapi_quota_pool;\n' >>"$TMP_DIR/bad.sql"
if NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' NEWAPI_POOL_SQL_FILE="$TMP_DIR/bad.sql" \
HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" "$SCRIPT" >/dev/null 2>&1; then
fail "越界 SQL 未被拒绝"
fi
python3 - "$TMP_DIR/pool.sql" "$TMP_DIR/inline-injection.sql" <<'PY'
import sys
source, target = sys.argv[1:]
lines = open(source, encoding="utf-8").read().splitlines()
for index, line in enumerate(lines):
if line.startswith("INSERT INTO newapi_quota_pool "):
lines[index] = line.replace(
" ON DUPLICATE KEY UPDATE ",
"; DROP TABLE newapi_quota_pool; -- ON DUPLICATE KEY UPDATE ",
1,
)
break
open(target, "w", encoding="utf-8").write("\n".join(lines) + "\n")
PY
chmod 600 "$TMP_DIR/inline-injection.sql"
if NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' \
NEWAPI_POOL_SQL_FILE="$TMP_DIR/inline-injection.sql" \
NEWAPI_POOL_IMPORT_EVIDENCE="$TMP_DIR/inline-injection.evidence" \
HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" \
MOCK_SSH_ARGS="$TMP_DIR/ssh-args" MOCK_SQL_CAPTURE="$TMP_DIR/captured-injection.sql" \
MOCK_IMPORT_STATE="$TMP_DIR/import-state" "$SCRIPT" >/dev/null 2>&1; then
fail "同一 INSERT 行内夹带额外 SQL 未被拒绝"
fi
calls_after="$(wc -l <"$TMP_DIR/ssh-args" | tr -d ' ')"
[ "$calls_before" = "$calls_after" ] || fail "本地前置校验失败后仍发生 SSH"
printf 'TEST_PASS new-api pool 导入宿主、ACK、事务、哈希、聚合、幂等与脱敏校验通过\n'