196 lines
10 KiB
Bash
Executable File
196 lines
10 KiB
Bash
Executable File
#!/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 + build =="
|
||
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
|
||
BIN="$WEB_REPO/backend/target/debug/tssbot-backend"
|
||
[[ -x "$BIN" ]] || bad "backend binary missing at $BIN"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
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=$!
|
||
PORT="$WEB_PORT" API_UPSTREAM="http://127.0.0.1:$BE_PORT" VEHICLE_ICONS_DIR="$ICONS" \
|
||
UPTIME_STORAGE_DIR="$STORE" 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"
|