package repos import ( "context" "gralias/models" "testing" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/assert" ) func setupPlayersTestDB(t *testing.T) (*sqlx.DB, func()) { db, err := sqlx.Connect("sqlite3", ":memory:") assert.NoError(t, err) // Load schema from migration files schema001 := ` -- migrations/001_initial_schema.up.sql CREATE TABLE rooms ( id TEXT PRIMARY KEY, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, creator_name TEXT NOT NULL, team_turn TEXT NOT NULL DEFAULT '', this_turn_limit INTEGER NOT NULL DEFAULT 0, opened_this_turn INTEGER NOT NULL DEFAULT 0, blue_counter INTEGER NOT NULL DEFAULT 0, red_counter INTEGER NOT NULL DEFAULT 0, red_turn BOOLEAN NOT NULL DEFAULT FALSE, mime_done BOOLEAN NOT NULL DEFAULT FALSE, is_running BOOLEAN NOT NULL DEFAULT FALSE, is_over BOOLEAN NOT NULL DEFAULT FALSE, team_won TEXT NOT NULL DEFAULT '', room_link TEXT NOT NULL DEFAULT '' ); CREATE TABLE players ( id INTEGER PRIMARY KEY AUTOINCREMENT, room_id TEXT, -- nullable username TEXT NOT NULL UNIQUE, password TEXT NOT NULL DEFAULT '', team TEXT NOT NULL DEFAULT '', -- 'red' or 'blue' role TEXT NOT NULL DEFAULT '', -- 'guesser' or 'mime' is_bot BOOLEAN NOT NULL DEFAULT FALSE, FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE ); CREATE TABLE word_cards ( id INTEGER PRIMARY KEY AUTOINCREMENT, room_id TEXT NOT NULL, word TEXT NOT NULL, color TEXT NOT NULL DEFAULT '', revealed BOOLEAN NOT NULL DEFAULT FALSE, mime_view BOOLEAN NOT NULL DEFAULT FALSE, FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE ); CREATE TABLE card_marks ( card_id INTEGER NOT NULL, username TEXT NOT NULL, FOREIGN KEY (card_id) REFERENCES word_cards(id) ON DELETE CASCADE, FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE, PRIMARY KEY (card_id, username) ); CREATE TABLE actions ( id INTEGER PRIMARY KEY AUTOINCREMENT, room_id TEXT NOT NULL, actor TEXT NOT NULL, actor_color TEXT NOT NULL DEFAULT '', action_type TEXT NOT NULL, word TEXT NOT NULL DEFAULT '', word_color TEXT NOT NULL DEFAULT '', number_associated TEXT NOT NULL DEFAULT '', -- for clues created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE ); CREATE TABLE settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, room_id TEXT NOT NULL, language TEXT NOT NULL DEFAULT 'en', room_pass TEXT NOT NULL DEFAULT '', turn_time INTEGER NOT NULL DEFAULT 60, -- seconds created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE ); CREATE TABLE sessions( id INTEGER PRIMARY KEY AUTOINCREMENT, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, lifetime INTEGER NOT NULL DEFAULT 3600, token_key TEXT NOT NULL DEFAULT '' UNIQUE, -- encoded value username TEXT NOT NULL, FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE ); CREATE TABLE journal( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, entry TEXT NOT NULL DEFAULT '', username TEXT NOT NULL, room_id TEXT NOT NULL, FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE, FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE ); CREATE TABLE player_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, games_played INTEGER NOT NULL DEFAULT 0, games_won INTEGER NOT NULL DEFAULT 0, games_lost INTEGER NOT NULL DEFAULT 0, opened_opposite_words INTEGER NOT NULL DEFAULT 0, opened_white_words INTEGER NOT NULL DEFAULT 0, opened_black_words INTEGER NOT NULL DEFAULT 0, mime_winrate REAL NOT NULL DEFAULT 0.0, guesser_winrate REAL NOT NULL DEFAULT 0.0, played_as_mime INTEGER NOT NULL DEFAULT 0, played_as_guesser INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE ); ` _, err = db.Exec(schema001) assert.NoError(t, err) schema002 := ` ALTER TABLE player_stats ADD COLUMN rating REAL NOT NULL DEFAULT 1000.0; CREATE TRIGGER update_player_rating AFTER UPDATE OF games_played, games_won ON player_stats WHEN NEW.games_played = OLD.games_played + 1 BEGIN UPDATE player_stats SET rating = OLD.rating + 32.0 * ( CASE WHEN NEW.games_won = OLD.games_won + 1 THEN 1.0 - 0.5 -- Win term: 0.5 ELSE 0.0 - 0.5 -- Loss term: -0.5 END ) + 0.05 * (1000.0 - OLD.rating) WHERE id = OLD.id; END; ` _, err = db.Exec(schema002) assert.NoError(t, err) return db, func() { db.Close() } } func TestPlayerStatsRatingUpdate(t *testing.T) { db, teardown := setupPlayersTestDB(t) defer teardown() username := "test_player_rating" _, err := db.Exec(`INSERT INTO players (username) VALUES (?)`, username) assert.NoError(t, err) _, err = db.Exec(`INSERT INTO player_stats (username, games_played, games_won, rating) VALUES (?, 0, 0, 1000.0)`, username) assert.NoError(t, err) // Simulate a win _, err = db.Exec(`UPDATE player_stats SET games_played = 1, games_won = 1 WHERE username = ?`, username) assert.NoError(t, err) var ratingAfterWin float64 err = db.Get(&ratingAfterWin, `SELECT rating FROM player_stats WHERE username = ?`, username) assert.NoError(t, err) // Expected: 1000 + 32 * (1 - 0.5) + 0.05 * (1000 - 1000) = 1000 + 16 = 1016 assert.InDelta(t, 1016.0, ratingAfterWin, 0.001) // Simulate a loss _, err = db.Exec(`UPDATE player_stats SET games_played = 2, games_won = 1 WHERE username = ?`, username) assert.NoError(t, err) var ratingAfterLoss float64 err = db.Get(&ratingAfterLoss, `SELECT rating FROM player_stats WHERE username = ?`, username) assert.NoError(t, err) // Expected: 1016 + 32 * (0 - 0.5) + 0.05 * (1000 - 1016) = 1016 - 16 + 0.05 * (-16) = 1000 - 0.8 = 999.2 assert.InDelta(t, 999.2, ratingAfterLoss, 0.001) } func TestPlayersRepo_AddPlayer(t *testing.T) { db, teardown := setupPlayersTestDB(t) defer teardown() repo := &RepoProvider{DB: db} roomID := "test_room_player_1" player := &models.Player{ RoomID: &roomID, Username: "test_player_1", Team: "blue", Role: "player", IsBot: false, } err := repo.PlayerAdd(context.Background(), player) assert.NoError(t, err) var retrievedPlayer models.Player err = db.Get(&retrievedPlayer, "SELECT * FROM players WHERE room_id = ? AND username = ?", player.RoomID, player.Username) assert.NoError(t, err) assert.Equal(t, player.Username, retrievedPlayer.Username) } func TestPlayersRepo_GetPlayer(t *testing.T) { db, teardown := setupPlayersTestDB(t) defer teardown() repo := &RepoProvider{DB: db} roomID := "test_room_player_2" player := &models.Player{ RoomID: &roomID, Username: "test_player_2", Team: "red", Role: "player", IsBot: false, } _, err := db.Exec(`INSERT INTO players (room_id, username, team, role, is_bot) VALUES (?, ?, ?, ?, ?)`, player.RoomID, player.Username, player.Team, player.Role, player.IsBot) assert.NoError(t, err) retrievedPlayer, err := repo.PlayerGetByName(context.Background(), player.Username) assert.NoError(t, err) assert.NotNil(t, retrievedPlayer) assert.Equal(t, player.Username, retrievedPlayer.Username) } func TestPlayersRepo_DeletePlayer(t *testing.T) { db, teardown := setupPlayersTestDB(t) defer teardown() repo := &RepoProvider{DB: db} roomID := "test_room_player_3" player := &models.Player{ RoomID: &roomID, Username: "test_player_3", Team: "blue", Role: "player", IsBot: false, } _, err := db.Exec(`INSERT INTO players (room_id, username, team, role, is_bot) VALUES (?, ?, ?, ?, ?)`, player.RoomID, player.Username, player.Team, player.Role, player.IsBot) assert.NoError(t, err) err = repo.PlayerDelete(context.Background(), player.Username) assert.NoError(t, err) var count int err = db.Get(&count, "SELECT COUNT(*) FROM players WHERE room_id = ? AND username = ?", player.RoomID, player.Username) assert.NoError(t, err) assert.Equal(t, 0, count) }