243 lines
10 KiB
Bash
Executable File
243 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 内部狗粮 smoke 回归:用假 HTTP 服务锁住默认无副作用与写模式 fail-closed 前置门。
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||
SMOKE="$ROOT_DIR/deploy/smoke-test.sh"
|
||
TMP_DIR="$(mktemp -d)"
|
||
REQUEST_LOG="$TMP_DIR/requests.log"
|
||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||
|
||
fail() {
|
||
printf 'FAIL: %s\n' "$1" >&2
|
||
exit 1
|
||
}
|
||
|
||
assert_contains() {
|
||
local file="$1" expected="$2"
|
||
grep -Fq "$expected" "$file" || fail "$file 缺少:$expected"
|
||
}
|
||
|
||
assert_exit_code() {
|
||
local expected="$1" actual="$2" message="$3"
|
||
[ "$actual" -eq "$expected" ] || fail "${message}(expected=${expected} actual=${actual})"
|
||
}
|
||
|
||
# 假 curl 实现完整写链状态机,并记录方法与 URL;测试不访问任何真实服务。
|
||
mkdir -p "$TMP_DIR/bin"
|
||
cat >"$TMP_DIR/bin/curl" <<'FAKE_CURL'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
method="GET"
|
||
output=""
|
||
url=""
|
||
auth=""
|
||
data=""
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
-X) method="$2"; shift 2 ;;
|
||
-o) output="$2"; shift 2 ;;
|
||
-H)
|
||
[[ "$2" == Authorization:* ]] && auth="${2#Authorization: Bearer }"
|
||
shift 2 ;;
|
||
-w|--connect-timeout|--max-time) shift 2 ;;
|
||
--data) data="$2"; shift 2 ;;
|
||
-sS) shift ;;
|
||
http://*|https://*) url="$1"; shift ;;
|
||
*) shift ;;
|
||
esac
|
||
done
|
||
printf '%s %s\n' "$method" "$url" >>"$REQUEST_LOG"
|
||
approved_file="$REQUEST_LOG.approved"
|
||
telemetry_file="$REQUEST_LOG.telemetry"
|
||
case "$method $url" in
|
||
"POST "*/app-api/passport/password-login)
|
||
body='{"code":0,"data":{"accessToken":"creator-token","userId":101,"creatorFlag":1}}' ;;
|
||
"POST "*/admin-api/system/auth/login)
|
||
if grep -q reviewer <<<"$data"; then token=reviewer-token; else token=readonly-token; fi
|
||
body="{\"code\":0,\"data\":{\"accessToken\":\"$token\"}}" ;;
|
||
"POST "*/app-api/passport/password-register)
|
||
body='{"code":1002090007,"data":null}' ;;
|
||
"POST "*/app-api/passport/send-sms-code)
|
||
body='{"code":1002090008,"data":null}' ;;
|
||
"GET "*/app-api/ad/slot/list-enabled)
|
||
body='{"code":1111002006,"data":null}' ;;
|
||
"GET "*/app-api/trade/charge/points-balance|"GET "*/app-api/trade/withdraw/page*)
|
||
body='{"code":1106008000,"data":null}' ;;
|
||
"POST "*/app-api/studio/create)
|
||
body='{"code":0,"data":{"id":31,"aigcTaskId":32,"gameId":11}}' ;;
|
||
"GET "*/app-api/studio/task/31)
|
||
body='{"code":0,"data":{"status":"2","aigcTaskId":32,"gameId":11,"versionId":22}}' ;;
|
||
"GET "*/app-api/runtime/package/22/manifest*)
|
||
body='{"entry":"index.html"}' ;;
|
||
"GET "*/app-api/runtime/package/22*)
|
||
body='{"code":0,"data":{"gameId":11,"versionId":22,"checksum":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' ;;
|
||
"POST "*/app-api/project/11/publish)
|
||
body='{"code":0,"data":{"admitted":true,"gates":[]}}' ;;
|
||
"GET "*/admin-api/project/review/page*)
|
||
body='{"code":0,"data":{"list":[{"id":11}]}}' ;;
|
||
"POST "*/admin-api/project/review)
|
||
if [ "$auth" = reviewer-token ]; then : >"$approved_file"; body='{"code":0,"data":true}';
|
||
else body='{"code":1004030000,"data":null}'; fi ;;
|
||
"GET "*/app-api/project/11)
|
||
body='{"code":0,"data":{"status":"4"}}' ;;
|
||
"POST "*/app-api/runtime/session/start)
|
||
body='{"code":0,"data":{"sessionId":41}}' ;;
|
||
"POST "*/app-api/runtime/session/end)
|
||
body='{"code":0,"data":true}' ;;
|
||
"POST "*/app-api/telemetry/events/batch)
|
||
: >"$telemetry_file"; body='{"code":0,"data":{"accepted":2,"rejected":0}}' ;;
|
||
"GET "*/admin-api/telemetry/game-stat/11)
|
||
if [ -f "$telemetry_file" ]; then body='{"code":0,"data":{"playCount":1,"playEndCount":1}}';
|
||
else body='{"code":0,"data":{"playCount":0,"playEndCount":0}}'; fi ;;
|
||
"GET "*/actuator/health)
|
||
body='{"status":"UP"}' ;;
|
||
"GET "*/app-api/feed/zones)
|
||
body='{"code":0,"data":[{"id":1}]}' ;;
|
||
"GET "*/app-api/feed/stream*)
|
||
if [ -f "$approved_file" ]; then
|
||
body='{"code":0,"data":{"list":[{"gameId":11,"versionId":22}],"hasMore":false}}'
|
||
else
|
||
body='{"code":0,"data":{"list":[{"gameId":99,"versionId":98}],"hasMore":false}}'
|
||
fi ;;
|
||
"GET "*/app-api/runtime/package/98*)
|
||
body='{"code":0,"data":{"gameId":99,"versionId":98,"checksum":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}' ;;
|
||
*)
|
||
body='{"code":404,"data":null}' ;;
|
||
esac
|
||
printf '%s' "$body" >"$output"
|
||
printf '200'
|
||
FAKE_CURL
|
||
chmod +x "$TMP_DIR/bin/curl"
|
||
|
||
export PATH="$TMP_DIR/bin:$PATH"
|
||
export REQUEST_LOG
|
||
|
||
cat >"$TMP_DIR/bin/hostname" <<'SH'
|
||
#!/usr/bin/env bash
|
||
printf 'mini-desktop\n'
|
||
SH
|
||
chmod +x "$TMP_DIR/bin/hostname"
|
||
|
||
cat >"$TMP_DIR/bin/systemctl" <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
if [ "$*" = 'is-active --quiet gda-staging-backend' ]; then
|
||
exit 0
|
||
fi
|
||
if [ "$*" = 'show --property=MainPID --value gda-staging-backend' ]; then
|
||
printf '4242\n'
|
||
exit 0
|
||
fi
|
||
exit 2
|
||
SH
|
||
chmod +x "$TMP_DIR/bin/systemctl"
|
||
|
||
bash -n "$SMOKE"
|
||
|
||
# 构建配置只能包含内网地址和模式开关,不能烘焙登录 token。
|
||
assert_contains "$ROOT_DIR/game-studio/.env.staging" 'VITE_STUDIO_TOKEN='
|
||
assert_contains "$ROOT_DIR/game-studio/.env.staging" 'VITE_DOGFOOD_MODE=true'
|
||
if grep -Eq '^VITE_STUDIO_TOKEN=.+$' "$ROOT_DIR/game-studio/.env.staging"; then
|
||
fail 'game-studio staging 构建烘焙了 token'
|
||
fi
|
||
assert_contains "$ROOT_DIR/game-admin/.env.stage" "VITE_BASE_URL='http://100.64.0.7:48080'"
|
||
if find "$ROOT_DIR/game-studio/public/mock-manifests" -type f -print -quit 2>/dev/null | grep -q .; then
|
||
fail 'staging public 目录仍包含退役 mock/CDP 走查资产'
|
||
fi
|
||
|
||
# 写模式必须显式确认副作用,且在任何 HTTP 请求前 fail-closed。
|
||
assert_contains "$SMOKE" 'DOGFOOD_E2E_ACK'
|
||
set +e
|
||
PATH="$TMP_DIR/bin:$PATH" "$SMOKE" --dogfood-e2e >"$TMP_DIR/missing-env.out" 2>&1
|
||
missing_env_rc=$?
|
||
set -e
|
||
assert_exit_code 2 "$missing_env_rc" '缺凭据时没有 fail-closed'
|
||
[ ! -s "$REQUEST_LOG" ] || fail '缺凭据时已发出网络请求'
|
||
|
||
set +e
|
||
DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
|
||
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
|
||
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
|
||
PATH="$TMP_DIR/bin:$PATH" "$SMOKE" --dogfood-e2e >"$TMP_DIR/missing-ack.out" 2>&1
|
||
missing_ack_rc=$?
|
||
set -e
|
||
assert_exit_code 2 "$missing_ack_rc" '缺 ACK 时没有 fail-closed'
|
||
[ ! -s "$REQUEST_LOG" ] || fail '缺 ACK 时已发出网络请求'
|
||
|
||
# 写模式固定到 mini-desktop staging;ACK 不能把 BASE 重定向到任意远端。
|
||
set +e
|
||
BASE=http://example.invalid DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
|
||
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
|
||
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
|
||
DOGFOOD_E2E_ACK=I_ACKNOWLEDGE_DOGFOOD_WRITES EXPECTED_RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
PATH="$TMP_DIR/bin:$PATH" "$SMOKE" --dogfood-e2e >"$TMP_DIR/wrong-target.out" 2>&1
|
||
wrong_target_rc=$?
|
||
set -e
|
||
assert_exit_code 2 "$wrong_target_rc" '写模式允许重定向到非 staging BASE'
|
||
|
||
# 默认 readiness 只能发送 GET,且假服务完整返回时应通过。
|
||
PATH="$TMP_DIR/bin:$PATH" "$SMOKE" >"$TMP_DIR/readiness.out" 2>&1
|
||
assert_contains "$TMP_DIR/readiness.out" 'SMOKE_VERDICT=PASS'
|
||
if grep -Ev '^GET ' "$REQUEST_LOG" | grep -q .; then
|
||
fail '默认 readiness 发出了非 GET 请求'
|
||
fi
|
||
|
||
# 未知参数同样必须在发请求前拒绝。
|
||
: >"$REQUEST_LOG"
|
||
set +e
|
||
PATH="$TMP_DIR/bin:$PATH" "$SMOKE" --deep >"$TMP_DIR/unknown.out" 2>&1
|
||
unknown_rc=$?
|
||
set -e
|
||
assert_exit_code 2 "$unknown_rc" '未知参数未拒绝'
|
||
[ ! -s "$REQUEST_LOG" ] || fail '未知参数触发了网络请求'
|
||
|
||
# 完整写模式回归:审核前 scan_main_feed 返回 1 是正确缺席,bash -e 不得提前退出。
|
||
: >"$REQUEST_LOG"
|
||
rm -f "$REQUEST_LOG.approved" "$REQUEST_LOG.telemetry"
|
||
active_root="$TMP_DIR/releases"
|
||
active_release="$active_root/0000000000000000000000000000000000000000"
|
||
mkdir -p "$active_release"
|
||
ln -s "$active_release" "$active_root/active"
|
||
printf 'active commit=0000000000000000000000000000000000000000 at=test evidence=test\n' >"$active_release/RC_STATUS"
|
||
active_release="$(readlink -f "$active_root/active")"
|
||
proc_root="$TMP_DIR/proc"
|
||
mkdir -p "$proc_root/4242"
|
||
printf 'java\0-jar\0%s\0' "$TMP_DIR/releases/wrong/game-cloud/huijing-server/target/huijing-server.jar" \
|
||
>"$proc_root/4242/cmdline"
|
||
set +e
|
||
DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
|
||
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
|
||
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
|
||
DOGFOOD_E2E_ACK=I_ACKNOWLEDGE_DOGFOOD_WRITES EXPECTED_RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
STAGING_ACTIVE_POINTER="$active_root/active" SYSTEMCTL_BIN="$TMP_DIR/bin/systemctl" PROC_ROOT="$proc_root" \
|
||
BASE=http://127.0.0.1:48080 STUDIO_BASE=http://127.0.0.1:4173 \
|
||
PATH="$TMP_DIR/bin:$PATH" bash "$SMOKE" --dogfood-e2e >"$TMP_DIR/wrong-process.out" 2>&1
|
||
wrong_process_rc=$?
|
||
set -e
|
||
assert_exit_code 2 "$wrong_process_rc" '写 smoke 未拒绝与 active RC 不一致的 backend 进程'
|
||
|
||
mkdir -p "$active_release/game-cloud/huijing-server/target"
|
||
printf 'java\0-jar\0%s\0' "$active_release/game-cloud/huijing-server/target/huijing-server.jar" \
|
||
>"$proc_root/4242/cmdline"
|
||
set +e
|
||
DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
|
||
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
|
||
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
|
||
DOGFOOD_E2E_ACK=I_ACKNOWLEDGE_DOGFOOD_WRITES EXPECTED_RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
STAGING_ACTIVE_POINTER="$active_root/active" SYSTEMCTL_BIN="$TMP_DIR/bin/systemctl" PROC_ROOT="$proc_root" \
|
||
BASE=http://127.0.0.1:48080 STUDIO_BASE=http://127.0.0.1:4173 \
|
||
E2E_TIMEOUT_SEC=5 POLL_INTERVAL_SEC=0 EVIDENCE_OUT="$TMP_DIR/e2e.json" \
|
||
PATH="$TMP_DIR/bin:$PATH" bash -e "$SMOKE" --dogfood-e2e >"$TMP_DIR/e2e.out" 2>&1
|
||
e2e_rc=$?
|
||
set -e
|
||
if [ "$e2e_rc" -ne 0 ]; then
|
||
sed -n '1,200p' "$TMP_DIR/e2e.out" >&2
|
||
fail "完整写模式回归失败 rc=$e2e_rc"
|
||
fi
|
||
assert_contains "$TMP_DIR/e2e.out" 'pre-approve game absent from main feed'
|
||
assert_contains "$TMP_DIR/e2e.out" 'DOGFOOD_E2E_VERDICT=PASS'
|
||
assert_contains "$TMP_DIR/e2e.json" '"status": "passed"'
|
||
|
||
printf 'PASS: dogfood smoke fail-closed regression\n'
|