package web import ( "encoding/json" "strings" "testing" "pete/internal/games/holdem" ) // The security boundary of the whole multiplayer table, tested the way the plan // insists it must be: render every seat's view at every street, and string-search // the JSON for any card that belongs to another seat. Zero hits, for every seat, // at every point before a showdown turns cards over. // // This is not belt-and-braces. After SSE the view a seat renders fans to that // seat's stream, so a single missed redaction does not leak one card to one // player through one handler bug — it broadcasts a hole card to the whole felt, // continuously. The engine deliberately emits every seat's cards now (it cannot // know who a shared stream is for), which makes this function the only thing // standing between the deck and the network tab. // holeLabel is a seat's two cards as they would be rendered — the exact strings a // leak would put in the JSON. func holeLabels(g holdem.State, seat int) []string { h := g.Seats[seat].Hole return []string{viewCard(h[0]).Label, viewCard(h[1]).Label} } func TestHoldemViewNeverLeaksAnotherSeatsCards(t *testing.T) { tier := holdem.Tiers[0] // Two humans (0, 2) and two bots (1, 3), so the test covers a seat seeing both // another human and a bot, and a human sitting at a non-zero index. seats := []holdem.SeatConfig{ {Name: "Ana", Stack: tier.MaxBuy}, {Name: "Bot A", Bot: true, Stack: tier.MaxBuy}, {Name: "Bo", Stack: tier.MaxBuy}, {Name: "Bot B", Bot: true, Stack: tier.MaxBuy}, } g, _, err := holdem.New(tier, seats, tier.RakePct, 5, 6) if err != nil { t.Fatal(err) } // Deal, then walk the hand to the river without anyone folding — every seat // stays in, so every seat has cards that must not leak to the others. Checking // and calling keeps everyone live; the redaction is checked after every step. g, dealEvs, err := holdem.ApplyMove(g, 0, holdem.Move{Kind: holdem.Deal}) if err != nil { t.Fatal(err) } // The deal script carries every hole, so it is the sharpest test of event // redaction: for each viewer, no other seat's cards may survive. assertEventsRedacted(t, g, dealEvs) assertViewsRedacted(t, g) for step := 0; g.Phase == holdem.PhaseBetting && step < 80; step++ { seat := g.ToAct if g.Seats[seat].Bot { t.Fatalf("advance stopped on a bot at seat %d", seat) } move := holdem.Move{Kind: holdem.Check} if g.Owed(seat) > 0 { move = holdem.Move{Kind: holdem.Call} } var evs []holdem.Event g, evs, err = holdem.ApplyMove(g, seat, move) if err != nil { t.Fatalf("seat %d %s: %v", seat, move.Kind, err) } assertEventsRedacted(t, g, evs) assertViewsRedacted(t, g) } } // assertViewsRedacted renders every seat's table view and fails if any of them // carries a card belonging to a seat that is still hiding it. func assertViewsRedacted(t *testing.T, g holdem.State) { t.Helper() shown := g.Street == holdem.Showdown for viewer := range g.Seats { blob, err := json.Marshal(viewHoldem(g, viewer)) if err != nil { t.Fatal(err) } payload := string(blob) for other := range g.Seats { if other == viewer { continue // your own cards are yours to see } // A seat's cards are legitimately visible only at a showdown, and then only // if they did not fold. Everywhere else, a hit is a leak. if shown && g.Seats[other].State != holdem.Folded { continue } if g.Seats[other].State == holdem.Out { continue } for _, label := range holeLabels(g, other) { if strings.Contains(payload, label) { t.Fatalf("seat %d's view (%s street) leaks seat %d's card %q", viewer, g.Street, other, label) } } } } } // assertEventsRedacted renders the event script for every viewer and fails if a // beat carries another seat's card outside a showdown "show". func assertEventsRedacted(t *testing.T, g holdem.State, evs []holdem.Event) { t.Helper() for viewer := range g.Seats { blob, err := json.Marshal(viewHoldemEvents(evs, viewer)) if err != nil { t.Fatal(err) } payload := string(blob) // The engine only puts a non-viewer's cards in a "show" beat and in the hole // beats it emits for everyone. If any card label of another seat survives the // redaction *and* there is no show event in this batch, it leaked. hasShow := false for _, e := range evs { if e.Kind == "show" { hasShow = true } } if hasShow { continue // a showdown legitimately turns cards over } for other := range g.Seats { if other == viewer || g.Seats[other].State == holdem.Out { continue } for _, label := range holeLabels(g, other) { if strings.Contains(payload, label) { t.Fatalf("seat %d's event stream leaks seat %d's card %q", viewer, other, label) } } } } }