//go:build integration package inttest import ( "context" "testing" "github.com/google/uuid" "scrabble/backend/internal/game" ) // TestForfeitAllResignsActiveGame checks the game-side effect of a block on a two-player game: // ForfeitAllForAccount resigns the blocked player, finishing the game with the opponent winning. func TestForfeitAllResignsActiveGame(t *testing.T) { ctx := context.Background() svc := newGameService() gid, seats := newGameWithSeats(t, 2) // seats[0] = blocked, seats[1] = opponent n, err := svc.ForfeitAllForAccount(ctx, seats[0]) if err != nil { t.Fatalf("forfeit all: %v", err) } if n < 1 { t.Fatalf("forfeit count = %d, want at least 1", n) } g, err := svc.GameByID(ctx, gid) if err != nil { t.Fatalf("get game: %v", err) } if g.Status != game.StatusFinished { t.Errorf("status = %q, want finished", g.Status) } if w := seatWinner(g, seats[0]); w { t.Error("the blocked player must not be the winner") } if w := seatWinner(g, seats[1]); !w { t.Error("the opponent must win when the blocked player forfeits") } } // TestForfeitAllCancelsOpenGame checks that a block deletes the blocked player's open auto-match // game (no opponent to resign against), clearing it from the matchmaking pool. func TestForfeitAllCancelsOpenGame(t *testing.T) { ctx := context.Background() clearOpenGames(t) svc := newGameService() starter := provisionAccount(t) g := openGame(t, svc, starter, evenOpeningSeed(t)) n, err := svc.ForfeitAllForAccount(ctx, starter) if err != nil { t.Fatalf("forfeit all: %v", err) } if n < 1 { t.Fatalf("forfeit count = %d, want at least 1", n) } if c := gameRowCount(t, g.ID); c != 0 { t.Errorf("open game rows after forfeit = %d, want 0 (deleted)", c) } } // TestForfeitAllResignsSeatInMultiplayerGame checks that in a 3-player game the blocked player's // seat is resigned while the game continues for the remaining players. func TestForfeitAllResignsSeatInMultiplayerGame(t *testing.T) { ctx := context.Background() svc := newGameService() gid, seats := newGameWithSeats(t, 3) n, err := svc.ForfeitAllForAccount(ctx, seats[0]) if err != nil { t.Fatalf("forfeit all: %v", err) } if n < 1 { t.Fatalf("forfeit count = %d, want at least 1", n) } g, err := svc.GameByID(ctx, gid) if err != nil { t.Fatalf("get game: %v", err) } if g.Status != game.StatusActive { t.Errorf("status = %q, want still active (two seats remain)", g.Status) } if c := resignMoveCount(t, gid, 0); c != 1 { t.Errorf("resign moves for seat 0 = %d, want 1", c) } } // seatWinner reports the is_winner flag for the given account's seat in g. func seatWinner(g game.Game, accountID uuid.UUID) bool { for _, s := range g.Seats { if s.AccountID == accountID { return s.IsWinner } } return false } // gameRowCount counts the rows in the games table for gameID (0 once it is deleted). func gameRowCount(t *testing.T, gameID uuid.UUID) int { t.Helper() var n int if err := testDB.QueryRowContext(context.Background(), `SELECT count(*) FROM backend.games WHERE game_id = $1`, gameID).Scan(&n); err != nil { t.Fatalf("count game rows: %v", err) } return n } // resignMoveCount counts the resign moves journalled for a seat in a game. func resignMoveCount(t *testing.T, gameID uuid.UUID, seat int) int { t.Helper() var n int if err := testDB.QueryRowContext(context.Background(), `SELECT count(*) FROM backend.game_moves WHERE game_id = $1 AND action = 'resign' AND seat = $2`, gameID, seat).Scan(&n); err != nil { t.Fatalf("count resign moves: %v", err) } return n }