Files
derekandClaude Opus 5.5 022d161934 One deck form per session; rename leagues; e2e test script
- A player's deck for a night is edited in a single form and written to
  their match rows and their standings row, so the two can't disagree.
  Matches, standings and hand imports all fill blank decks from the decks
  already entered, whatever order the files are imported in. The imported
  standings table shows decks read-only.
- Admins can rename a league.
- test/e2e.sh runs end-to-end checks against the local podman stack
  (fixtures by default, or real exports: test/e2e.sh sampledata).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:20:45 +00:00

100 lines
6.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# End-to-end checks against the local podman stack: imports, the single deck form, hand import, league rename/delete.
# usage: scripts/local.sh up && test/e2e.sh [dir]
# dir holds Melee exports named *matches*.json / *tandings*.json (default test/fixtures; real ones: test/e2e.sh sampledata).
# Creates a throwaway league and deletes it again at the end.
set -euo pipefail
cd "$(dirname "$0")/.."
DIR=${1:-test/fixtures}
set -a; source .env.local; set +a
B=http://127.0.0.1:$HOST_PORT
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
fails=0
check() { if [[ "$2" == "$3" ]]; then echo "ok $1"; else echo "FAIL $1: got '$2', want '$3'"; fails=$((fails + 1)); fi; }
req() { curl -s -b "$T/jar" -c "$T/jar" -o "$T/out" -w '%{http_code}' "$@"; }
anon() { curl -s -o /dev/null -w '%{http_code}' "$@"; }
db() { podman exec mtg-local-db-1 psql -U league -tAc "$1"; }
files() { for f in "$DIR"/$1; do printf -- '-F\nfiles=@%s\n' "$f"; done; }
mapfile -t MATCHES < <(files '*atches*.json')
mapfile -t STANDINGS < <(files '*tandings*.json')
decks_of() { # distinct decks of player $2 in session $1, across match rows and the standings row
db "SELECT string_agg(DISTINCT coalesce(d, '-'), ',') FROM (SELECT p1_deck d FROM matches WHERE session_id=$1 AND p1_id=$2
UNION ALL SELECT p2_deck FROM matches WHERE session_id=$1 AND p2_id=$2
UNION ALL SELECT deck FROM session_results WHERE session_id=$1 AND player_id=$2) x"
}
echo "== auth and leagues"
check "admin route needs login" "$(anon -d name=x "$B/admin/league")" 403
check "login" "$(req -d "password=$ADMIN_PASSWORD" "$B/login")" 303
req -d name=e2e-league -d best_n=6 "$B/admin/league" >/dev/null
LG=$(db "SELECT max(id) FROM leagues")
check "rename needs login" "$(anon -d name=x "$B/admin/league/$LG/rename")" 403
check "rename rejects empty name" "$(req -d 'name= ' "$B/admin/league/$LG/rename")" 400
check "rename" "$(req -d 'name=e2e renamed' "$B/admin/league/$LG/rename")" 303
check "renamed in db" "$(db "SELECT name FROM leagues WHERE id=$LG")" "e2e renamed"
req "$B/league/$LG" >/dev/null; check "league page shows new name" "$(grep -c '<h1>e2e renamed</h1>' "$T/out")" 1
check "rename unknown league" "$(req -d name=x "$B/admin/league/2147483647/rename")" 404
echo "== session A: standings, then a deck, then matches"
req -d date=2026-10-14 "$B/admin/league/$LG/session" >/dev/null; SA=$(db "SELECT max(id) FROM sessions")
check "import standings" "$(req "${STANDINGS[@]}" "$B/admin/session/$SA/import-melee")" 303
P1=$(db "SELECT player_id FROM session_results r JOIN players p ON p.id=r.player_id WHERE session_id=$SA ORDER BY p.name LIMIT 1")
check "deck form saves to standings" "$(req -d "deck_$P1=Burn" "$B/admin/session/$SA/decks")" 303
check "standings deck" "$(decks_of "$SA" "$P1")" Burn
check "import all matches at once" "$(req "${MATCHES[@]}" "$B/admin/session/$SA/import-melee")" 303
check "matches picked up the standings deck" "$(decks_of "$SA" "$P1")" Burn
req "$B/session/$SA" >/dev/null
check "one deck form on the page" "$(grep -c '/decks"' "$T/out")" 1
check "one deck input per session player" "$(grep -o 'name="deck_' "$T/out" | wc -l)" "$(db "SELECT count(*) FROM ($(sed 's/\$1/'"$SA"'/g' <<<"SELECT pid FROM (SELECT p1_id pid FROM matches WHERE session_id=\$1 UNION SELECT p2_id FROM matches WHERE session_id=\$1 AND p2_id IS NOT NULL UNION SELECT player_id FROM session_results WHERE session_id=\$1) x")) y")"
check "deck form saves to matches and standings" "$(req -d "deck_$P1=Control" "$B/admin/session/$SA/decks")" 303
check "both updated" "$(decks_of "$SA" "$P1")" Control
check "clearing a deck" "$(req -d "deck_$P1=" "$B/admin/session/$SA/decks")" 303
check "cleared everywhere" "$(decks_of "$SA" "$P1")" -
req -d "deck_$P1=Control" "$B/admin/session/$SA/decks" >/dev/null
echo "== session A: hand import"
csv() { db "COPY (SELECT m.round, a.name, CASE WHEN m.p1_id=$P1 THEN '$1' ELSE '' END, coalesce(b.name, ''),
CASE WHEN m.p2_id=$P1 THEN '$1' ELSE '' END, m.p1_wins, m.p2_wins, m.draws FROM matches m JOIN players a ON a.id=m.p1_id
LEFT JOIN players b ON b.id=m.p2_id WHERE m.session_id=$SA ORDER BY m.round, m.id) TO STDOUT WITH CSV"; }
before=$(db "SELECT count(*) FROM matches WHERE session_id=$SA")
check "hand import with blank decks" "$(req --data-urlencode "data=$(csv '')" "$B/admin/session/$SA/import")" 303
check "match count unchanged" "$(db "SELECT count(*) FROM matches WHERE session_id=$SA")" "$before"
check "blank keeps the entered deck" "$(decks_of "$SA" "$P1")" Control
check "hand import with a new deck" "$(req --data-urlencode "data=$(csv Elves)" "$B/admin/session/$SA/import")" 303
check "new deck reaches standings too" "$(decks_of "$SA" "$P1")" Elves
echo "== session B: matches, then a deck, then standings"
req -d date=2026-10-15 "$B/admin/league/$LG/session" >/dev/null; SB=$(db "SELECT max(id) FROM sessions")
check "import matches" "$(req "${MATCHES[@]}" "$B/admin/session/$SB/import-melee")" 303
P2=$(db "SELECT p1_id FROM matches WHERE session_id=$SB AND p2_id IS NOT NULL ORDER BY id LIMIT 1")
check "deck on matches" "$(req -d "deck_$P2=Elves" "$B/admin/session/$SB/decks")" 303
check "import standings" "$(req "${STANDINGS[@]}" "$B/admin/session/$SB/import-melee")" 303
if [[ -n "$(db "SELECT 1 FROM session_results WHERE session_id=$SB AND player_id=$P2")" ]]; then
check "standings picked up the match deck" "$(decks_of "$SB" "$P2")" Elves
else echo "skip standings deck check: player not in the standings export"; fi
echo "== scores"
# only meaningful when the matches exports cover every round of the standings export
python3 -I - "$DIR" "$(curl -s "$B/api/league/$LG/leaderboard")" "$(db "SELECT json_agg(json_build_object('name', p.name, 'points', r.points))
FROM session_results r JOIN players p ON p.id=r.player_id WHERE r.session_id=$SA")" <<'EOF' || fails=$((fails + 1))
import glob, json, sys
d, board, final = sys.argv[1], json.loads(sys.argv[2]), json.loads(sys.argv[3])
rounds = {m["RoundNumber"] for f in glob.glob(d + "/*atches*.json") for m in json.load(open(f))}
last = max(r["RoundNumber"] for f in glob.glob(d + "/*tandings*.json") for r in json.load(open(f)))
if rounds != set(range(1, last + 1)):
print("skip leaderboard check: matches exports don't cover rounds 1 to", last); sys.exit(0)
got = {r["name"]: r["total"] / 2 for r in board} # sessions A and B hold the same night
bad = [f"{r['name']} {got.get(r['name'])} != {r['points']}" for r in final if got.get(r["name"]) != r["points"]]
if bad: print("FAIL leaderboard vs Melee standings:", bad); sys.exit(1)
print("ok leaderboard matches Melee's points for", len(final), "players")
EOF
echo "== delete league"
check "delete league" "$(req -X POST "$B/admin/league/$LG/delete")" 303
check "league gone" "$(db "SELECT count(*) FROM leagues WHERE id=$LG")" 0
check "its sessions gone" "$(db "SELECT count(*) FROM sessions WHERE id IN ($SA, $SB)")" 0
check "its matches gone" "$(db "SELECT count(*) FROM matches WHERE session_id IN ($SA, $SB)")" 0
echo; [[ $fails == 0 ]] && echo "all e2e checks passed" || { echo "$fails e2e check(s) failed"; exit 1; }