Files
TSSBOT-web/scripts/verify_game_detail.sh
T

213 lines
11 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
#
# verify_game_detail.sh — end-to-end check for the TSSBOT game-detail parity work.
#
# Spins up a SELF-CONTAINED fixture (temp SQLite DBs, vehicle caches, icons) and
# exercises the whole stack: the Python log builder, the Rust backend (dedup fix,
# vehicle translation, logs endpoint), and the Node prod server (icon serving +
# API proxy allowlist). Touches NO production data. Exits non-zero on any failure.
#
# Usage:
# bash scripts/verify_game_detail.sh
#
# Env overrides:
# BOTS_REPO path to the BOTS repo (default: ~/GitHub/BOTS, fallback ~/BOTS, /home/deploy/BOTS)
# PYTHON python interpreter (default: BOTS venv if present, else python3)
# BE_PORT backend port (default 6071)
# WEB_PORT node server port (default 3071)
set -uo pipefail
WEB_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BE_PORT="${BE_PORT:-6071}"
WEB_PORT="${WEB_PORT:-3071}"
# ---- locate BOTS repo + python --------------------------------------------
if [[ -z "${BOTS_REPO:-}" ]]; then
for c in "$HOME/GitHub/BOTS" "$HOME/BOTS" "/home/deploy/BOTS"; do
[[ -d "$c" ]] && BOTS_REPO="$c" && break
done
fi
if [[ -z "${BOTS_REPO:-}" || ! -d "$BOTS_REPO" ]]; then
echo "FAIL: could not find BOTS repo; set BOTS_REPO=..." >&2
exit 1
fi
if [[ -z "${PYTHON:-}" ]]; then
if [[ -x "$BOTS_REPO/SHARED/.venv/bin/python" ]]; then
PYTHON="$BOTS_REPO/SHARED/.venv/bin/python"
else
PYTHON="python3"
fi
fi
PASS=0
FAIL=0
ok() { echo " PASS: $1"; PASS=$((PASS+1)); }
bad() { echo " FAIL: $1" >&2; FAIL=$((FAIL+1)); }
# assert_eq <label> <actual> <expected>
assert_eq() { if [[ "$2" == "$3" ]]; then ok "$1 ($2)"; else bad "$1: got '$2' expected '$3'"; fi; }
WD="$(mktemp -d)"
ICONS="$(mktemp -d)"
STORE="$(mktemp -d)"
BE_PID=""
WEB_PID=""
cleanup() {
[[ -n "$BE_PID" ]] && kill "$BE_PID" 2>/dev/null
[[ -n "$WEB_PID" ]] && kill "$WEB_PID" 2>/dev/null
rm -rf "$WD" "$ICONS" "$STORE"
}
trap cleanup EXIT
echo "== TSSBOT game-detail verification =="
echo "WEB_REPO=$WEB_REPO BOTS_REPO=$BOTS_REPO PYTHON=$PYTHON"
# ---------------------------------------------------------------------------
echo "== 1. Python log-builder unit tests =="
if "$PYTHON" "$BOTS_REPO/TSSBOT/tests/test_match_logs.py"; then ok "build_match_logs tests"; else bad "build_match_logs tests"; fi
# ---------------------------------------------------------------------------
echo "== 2. Backfill dry-run against a synthetic replay =="
SYN="$(mktemp -d)"
STORAGE_VOL_PATH="$SYN" "$PYTHON" - "$SYN" <<'PY'
import gzip, json, os, pathlib, sys
d = pathlib.Path(sys.argv[1]) / "REPLAYS" / "TSS" / "feed"
d.mkdir(parents=True)
game = {"_id": "feed", "winner": "[WIN]", "loser": "[LOS]",
"players": {"1": {"name": "a", "tag": "[WIN]", "team": "1",
"units": [{"unit": "ussr_t_34", "used": True}]}},
"chat": [{"uid": "1", "type": "ALL", "message": "gg", "time": 1000}],
"events": {"kills": []}}
gzip.open(d / "replay_data.json.gz", "wb").write(json.dumps(game).encode())
PY
if STORAGE_VOL_PATH="$SYN" "$PYTHON" "$BOTS_REPO/TSSBOT/scripts/backfill_match_logs.py" --dry-run | grep -q "Would backfill"; then
ok "backfill --dry-run runs"
else
bad "backfill --dry-run"
fi
rm -rf "$SYN"
# ---------------------------------------------------------------------------
echo "== 3. cargo tests + binary =="
if cargo test --manifest-path "$WEB_REPO/backend/Cargo.toml" >/tmp/vgd_cargo.log 2>&1; then ok "cargo test"; else bad "cargo test (see /tmp/vgd_cargo.log)"; fi
# `cargo test` only builds the test harness, not the standalone binary. Prefer the
# release binary (built at deploy); otherwise build a debug one.
BIN=""
if [[ -x "$WEB_REPO/backend/target/release/tssbot-backend" ]]; then
BIN="$WEB_REPO/backend/target/release/tssbot-backend"
ok "using release binary"
else
if cargo build --manifest-path "$WEB_REPO/backend/Cargo.toml" >/tmp/vgd_build.log 2>&1; then
BIN="$WEB_REPO/backend/target/debug/tssbot-backend"
ok "built debug binary"
else
bad "cargo build (see /tmp/vgd_build.log)"
fi
fi
[[ -n "$BIN" && -x "$BIN" ]] || bad "backend binary unavailable"
# ---------------------------------------------------------------------------
echo "== 4. Build fixture (multi-vehicle player to catch the double-count bug) =="
"$PYTHON" - "$WD" <<'PY'
import sqlite3, json, sys
wd = sys.argv[1]
b = sqlite3.connect(f"{wd}/tss_battles.db")
b.executescript("""
CREATE TABLE match_summary (session_id TEXT PRIMARY KEY, mission_mode TEXT, mission_name TEXT,
level_path TEXT, mission_path TEXT, difficulty TEXT, starttime_unix INT, endtime_unix INT,
duration REAL, draw INT DEFAULT 0, winning_slot TEXT, losing_slot TEXT, received_unix INT,
tournament_id INT, tournament_name TEXT, match_id TEXT, bracket TEXT);
CREATE TABLE player_games_hist (UID TEXT, nick TEXT, team_name TEXT, team_slot TEXT,
session_id TEXT, vehicle TEXT, vehicle_internal TEXT, ground_kills INT, air_kills INT,
assists INT, captures INT, deaths INT, score INT, missile_evades INT, shell_interceptions INT,
team_kills_stat INT, country_id INT, victor_bool TEXT, endtime_unix INT, team_id INT,
tss_role TEXT, pvp_ratio REAL);
CREATE TABLE match_logs (session_id TEXT PRIMARY KEY, chat_log_json TEXT, battle_log_json TEXT, built_unix INT);
""")
b.execute("INSERT INTO match_summary VALUES ('abc','Dom','Test Map','','','',0,1000,420.0,0,'1','2',0,0,'Cup Finals','','')")
# alice used TWO vehicles -> two rows, identical per-player stats (score 100).
b.execute("INSERT INTO player_games_hist VALUES ('1','alice','TeamWin','1','abc','t34','ussr_t_34',2,0,1,0,0,100,0,0,0,0,'Win',1000,10,'',1.5)")
b.execute("INSERT INTO player_games_hist VALUES ('1','alice','TeamWin','1','abc','is2','ussr_is_2',2,0,1,0,0,100,0,0,0,0,'Win',1000,10,'',1.5)")
b.execute("INSERT INTO player_games_hist VALUES ('2','bob','TeamLose','2','abc','pz','germ_pz_iv',0,0,0,0,1,10,0,0,0,0,'Loss',1000,11,'',0.5)")
b.execute("INSERT INTO match_logs VALUES ('abc', ?, ?, 1000)",
(json.dumps(["[00:01] [ALL] [WIN] `alice`: gg"]),
json.dumps(["+[00:30] [WIN] alice (T-34) destroyed bob (Pz.IV)"])))
b.commit()
t = sqlite3.connect(f"{wd}/tss_teams.db")
t.executescript("CREATE TABLE teams_data (team_id INT PRIMARY KEY, name TEXT, members INT DEFAULT 0, captain_uid TEXT);")
t.commit()
json.dump({"ussr_t_34": {"en": "T-34", "ru": "Т-34"}, "ussr_is_2": {"en": "IS-2"}}, open(f"{wd}/vt.json", "w"))
json.dump([["ussr_t_34", "T-34", "ussr_t_34.png", {}], ["ussr_is_2", "IS-2", "ussr_is_2.png", {}]], open(f"{wd}/vc.json", "w"))
print("fixture ready")
PY
printf '\x89PNG' > "$ICONS/ussr_t_34.png"
# ---------------------------------------------------------------------------
echo "== 5. Start backend + node server against the fixture =="
TSS_BATTLES_DB="$WD/tss_battles.db" TSS_TEAMS_DB="$WD/tss_teams.db" \
VEHICLE_TRANSLATIONS_JSON="$WD/vt.json" VEHICLE_DATA_CACHE_JSON="$WD/vc.json" \
BACKEND_PORT="$BE_PORT" "$BIN" >/tmp/vgd_be.log 2>&1 &
BE_PID=$!
# Override PUBLIC_ORIGIN/comingsoon so the server's same-origin guard accepts the
# localhost test origin and serves normally. server.cjs's loadEnvFile only fills
# unset/empty vars, so these non-empty values win over the prod .env.
PORT="$WEB_PORT" API_UPSTREAM="http://127.0.0.1:$BE_PORT" VEHICLE_ICONS_DIR="$ICONS" \
UPTIME_STORAGE_DIR="$STORE" PUBLIC_ORIGIN="http://localhost:$WEB_PORT" comingsoon="FALSE" \
node "$WEB_REPO/server.cjs" >/tmp/vgd_web.log 2>&1 &
WEB_PID=$!
# wait for both to listen
for _ in $(seq 1 20); do
curl -sf "localhost:$BE_PORT/health" >/dev/null 2>&1 && curl -sf "localhost:$WEB_PORT/health" >/dev/null 2>&1 && break
sleep 0.5
done
# ---------------------------------------------------------------------------
echo "== 6. Backend: dedup, vehicle translation, logs =="
GAME_EN="$(curl -s "localhost:$BE_PORT/api/tss/games/abc?lang=en")"
echo "$GAME_EN" | "$PYTHON" - <<PY
import json, sys
d = json.loads('''$GAME_EN''')
def field(name, actual, expected):
print((" PASS" if actual==expected else " FAIL")+f": {name}: {actual!r} (expected {expected!r})")
return actual==expected
alice = [pl for part in d["participants"] for pl in part["players"] if pl["uid"]=="1"][0]
win = [p for p in d["participants"] if p["team_name"]=="TeamWin"][0]
oks = []
oks.append(field("alice score deduped", alice["stats"]["score"], 100))
oks.append(field("alice vehicle count", len(alice["vehicles"]), 2))
oks.append(field("team total score", win["stats"]["score"], 100))
oks.append(field("game total score", d["game"]["stats"]["score"], 110))
oks.append(field("tournament_name", d["game"].get("tournament_name"), "Cup Finals"))
oks.append(field("duration", d["game"].get("duration"), 420.0))
oks.append(field("draw", d["game"]["draw"], False))
sys.exit(0 if all(oks) else 1)
PY
if [[ $? -eq 0 ]]; then ok "backend game detail (en)"; else bad "backend game detail (en)"; fi
RU_NAME="$(curl -s "localhost:$BE_PORT/api/tss/games/abc?lang=ru" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print([v['name'] for p in d['participants'] for pl in p['players'] for v in pl['vehicles'] if v['cdk']=='ussr_t_34'][0])")"
assert_eq "ru translation of ussr_t_34" "$RU_NAME" "Т-34"
LOG_COUNTS="$(curl -s "localhost:$BE_PORT/api/tss/games/abc/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print(len(d['chat_log']), len(d['battle_log']))")"
assert_eq "logs chat/battle counts" "$LOG_COUNTS" "1 1"
MISS="$(curl -s "localhost:$BE_PORT/api/tss/games/deadbeef/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print(len(d['chat_log'])+len(d['battle_log']))")"
assert_eq "missing-session logs empty" "$MISS" "0"
# ---------------------------------------------------------------------------
echo "== 7. Node prod server: icons + proxy allowlist =="
H=(-H "Origin: http://localhost:$WEB_PORT" -H "Referer: http://localhost:$WEB_PORT/games/abc" -H "Sec-Fetch-Site: same-origin")
assert_eq "icon served" "$(curl -s -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/vehicle-icons/ussr_t_34.png")" "200"
assert_eq "icon traversal blocked" "$(curl -s --path-as-is -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/vehicle-icons/..%2f..%2fserver.cjs")" "403"
assert_eq "proxy game?lang=en" "$(curl -s "${H[@]}" -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/api/tss/games/abc?lang=en")" "200"
assert_eq "proxy logs" "$(curl -s "${H[@]}" -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/api/tss/games/abc/logs")" "200"
assert_eq "proxy SPA shell" "$(curl -s -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/games/abc")" "200"
# bad param must NOT be 200 (allowlist rejects)
BADCODE="$(curl -s "${H[@]}" -o /dev/null -w '%{http_code}' "localhost:$WEB_PORT/api/tss/games/abc?evil=1")"
if [[ "$BADCODE" != "200" ]]; then ok "proxy blocks unknown param ($BADCODE)"; else bad "proxy allowed unknown param"; fi
# ---------------------------------------------------------------------------
echo
echo "== RESULT: $PASS passed, $FAIL failed =="
[[ $FAIL -eq 0 ]] || exit 1
echo "ALL CHECKS PASSED"