package storage import ( "errors" "testing" ) // openTestTable stands up a table with a full ring of bots and returns it. Six // seats, because that is hold'em's ring and the most seats any game here has. func openTestTable(t *testing.T, id, game string) Table { t.Helper() tbl := Table{ ID: id, Game: game, Tier: "1-2", State: []byte(`{}`), Seed1: 1, Seed2: 2, Phase: "betting", HandNo: 1, } seats := make([]Seat, 6) for i := range seats { seats[i] = Seat{Seat: i, Name: "bot"} } if err := OpenTable(tbl, seats); err != nil { t.Fatal(err) } return tbl } // reload reads a table back and fails the test if it is gone. func reload(t *testing.T, id string) (Table, []Seat) { t.Helper() tbl, seats, err := LoadTable(id) if err != nil { t.Fatal(err) } return tbl, seats } func TestOpenTable_SeatsAreAllBots(t *testing.T) { setupTestDB(t) openTestTable(t, "t1", "holdem") _, seats := reload(t, "t1") if len(seats) != 6 { t.Fatalf("want 6 seats, got %d", len(seats)) } for _, s := range seats { if !s.Bot() { t.Errorf("seat %d should be a bot", s.Seat) } } } func TestSitDown_MovesChipsOntoTheTable(t *testing.T) { setupTestDB(t) tbl := openTestTable(t, "t1", "holdem") fund(t, player, 5000) if err := SitDown(Sit{ Table: tbl, Seat: Seat{Seat: 2, MatrixUser: player, Name: "reala"}, BuyIn: 1000, }); err != nil { t.Fatal(err) } // The chips are off the stack... if got := chipsOf(t, player); got != 4000 { t.Errorf("stack: want 4000, got %d", got) } // ...and onto the seat. _, seats := reload(t, "t1") seat := seats[2] if seat.MatrixUser != player || seat.Staked != 1000 { t.Errorf("seat 2: want reala staked 1000, got %q staked %d", seat.MatrixUser, seat.Staked) } // The occupancy claim points at the table. id, err := TableOf(player) if err != nil || id != "t1" { t.Errorf("TableOf: want t1, got %q err %v", id, err) } } func TestSitDown_CannotTakeATakenSeat(t *testing.T) { setupTestDB(t) tbl := openTestTable(t, "t1", "holdem") fund(t, player, 5000) fund(t, "@bob:parodia.dev", 5000) if err := SitDown(Sit{Table: tbl, Seat: Seat{Seat: 2, MatrixUser: player, Name: "reala"}, BuyIn: 1000}); err != nil { t.Fatal(err) } tbl2, _ := reload(t, "t1") err := SitDown(Sit{Table: tbl2, Seat: Seat{Seat: 2, MatrixUser: "@bob:parodia.dev", Name: "bob"}, BuyIn: 1000}) if !errors.Is(err, ErrSeatTaken) { t.Fatalf("want ErrSeatTaken, got %v", err) } // Bob's chips did not move. if got := chipsOf(t, "@bob:parodia.dev"); got != 5000 { t.Errorf("bob's stack should be untouched, got %d", got) } } func TestSitDown_CannotSitAtTwoTables(t *testing.T) { setupTestDB(t) tbl1 := openTestTable(t, "t1", "holdem") tbl2 := openTestTable(t, "t2", "holdem") fund(t, player, 5000) if err := SitDown(Sit{Table: tbl1, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000}); err != nil { t.Fatal(err) } err := SitDown(Sit{Table: tbl2, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000}) if !errors.Is(err, ErrHandInProgress) { t.Fatalf("want ErrHandInProgress, got %v", err) } // The buy-in for the second table rolled back. if got := chipsOf(t, player); got != 4000 { t.Errorf("only the first buy-in should have left the stack, got %d", got) } } func TestSitDown_InsufficientChipsRollsBack(t *testing.T) { setupTestDB(t) tbl := openTestTable(t, "t1", "holdem") fund(t, player, 500) err := SitDown(Sit{Table: tbl, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000}) if !errors.Is(err, ErrInsufficientChips) { t.Fatalf("want ErrInsufficientChips, got %v", err) } if _, err := TableOf(player); !errors.Is(err, ErrNoLiveHand) { t.Errorf("no seat should have been claimed, got %v", err) } _, seats := reload(t, "t1") if seats[0].MatrixUser != "" { t.Errorf("seat should still be a bot") } } func TestLeaveTable_BringsChipsHome(t *testing.T) { setupTestDB(t) tbl := openTestTable(t, "t1", "holdem") fund(t, player, 5000) if err := SitDown(Sit{Table: tbl, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000}); err != nil { t.Fatal(err) } tbl2, _ := reload(t, "t1") // Got up with 1,240 — up on the session. if err := LeaveTable(Leave{Table: tbl2, Seat: 0, MatrixUser: player, Bot: "bot", Amount: 1240}); err != nil { t.Fatal(err) } if got := chipsOf(t, player); got != 5240 { t.Errorf("want 5240 back on the stack, got %d", got) } if _, err := TableOf(player); !errors.Is(err, ErrNoLiveHand) { t.Errorf("seat claim should be gone, got %v", err) } _, seats := reload(t, "t1") if seats[0].MatrixUser != "" { t.Errorf("a bot should have taken the empty chair") } } func TestSaveTable_VersionGuardsTheWrite(t *testing.T) { setupTestDB(t) openTestTable(t, "t1", "holdem") a, _ := reload(t, "t1") // both read version 0 b, _ := reload(t, "t1") a.State = []byte(`{"a":1}`) if err := CommitTable(TableCommit{Table: a}); err != nil { t.Fatalf("first write should win: %v", err) } b.State = []byte(`{"b":2}`) if err := CommitTable(TableCommit{Table: b}); !errors.Is(err, ErrStaleTable) { t.Fatalf("second write should be stale, got %v", err) } after, _ := reload(t, "t1") if string(after.State) != `{"a":1}` { t.Errorf("the winning write should stand, got %s", after.State) } if after.Version != 1 { t.Errorf("version should have bumped once, got %d", after.Version) } } func TestDueTables_OnlyExpiredClocks(t *testing.T) { setupTestDB(t) now := nowUnix() past := openTestTable(t, "past", "holdem") past.Deadline = now - 5 if err := CommitTable(TableCommit{Table: past}); err != nil { t.Fatal(err) } future := openTestTable(t, "future", "holdem") future.Deadline = now + 60 if err := CommitTable(TableCommit{Table: future}); err != nil { t.Fatal(err) } openTestTable(t, "noclock", "holdem") // deadline 0 due, err := DueTables(now) if err != nil { t.Fatal(err) } if len(due) != 1 || due[0].ID != "past" { t.Fatalf("only the past-due table should show, got %+v", due) } } func TestChat_KeepsTheHandItWasSaidDuring(t *testing.T) { setupTestDB(t) openTestTable(t, "t1", "holdem") // hand_no 1 if _, err := Say("t1", player, "reala", "nice hand"); err != nil { t.Fatal(err) } // The table moves to the next hand. tbl2, _ := reload(t, "t1") tbl2.HandNo = 2 if err := CommitTable(TableCommit{Table: tbl2}); err != nil { t.Fatal(err) } if _, err := Say("t1", player, "reala", "and another"); err != nil { t.Fatal(err) } lines, err := Chat("t1", 50) if err != nil { t.Fatal(err) } if len(lines) != 2 { t.Fatalf("want 2 lines, got %d", len(lines)) } if lines[0].Body != "nice hand" || lines[0].HandNo != 1 { t.Errorf("first line should be hand 1: %+v", lines[0]) } if lines[1].HandNo != 2 { t.Errorf("second line should be hand 2: %+v", lines[1]) } } func TestCloseTable_KeepsATableWithAHumanAtIt(t *testing.T) { setupTestDB(t) tbl := openTestTable(t, "t1", "holdem") fund(t, player, 5000) if err := SitDown(Sit{Table: tbl, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000}); err != nil { t.Fatal(err) } if err := CloseTable("t1"); err != nil { t.Fatal(err) } if _, _, err := LoadTable("t1"); err != nil { t.Errorf("a table with a human should survive close, got %v", err) } // Everyone leaves; now it goes. tbl2, _ := reload(t, "t1") if err := LeaveTable(Leave{Table: tbl2, Seat: 0, MatrixUser: player, Bot: "bot", Amount: 1000}); err != nil { t.Fatal(err) } if err := CloseTable("t1"); err != nil { t.Fatal(err) } if _, _, err := LoadTable("t1"); !errors.Is(err, ErrNoSuchTable) { t.Errorf("an all-bot table should close, got %v", err) } }