From 8b81e2e2c4f439499ac046e22509ead9dd4a8e24 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 4 Jul 2025 07:00:16 +0300 Subject: [PATCH] Enha: create tx; cardword test --- handlers/game.go | 49 +++- repos/main.go | 9 + repos/word_cards_test.go | 496 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 repos/word_cards_test.go diff --git a/handlers/game.go b/handlers/game.go index c892c7e..a3b5de9 100644 --- a/handlers/game.go +++ b/handlers/game.go @@ -159,6 +159,21 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) { abortWithError(w, err.Error()) return } + + // Initialize transaction + ctx, tx, err := repo.InitTx(r.Context()) + if err != nil { + log.Error("failed to init transaction", "error", err) + abortWithError(w, err.Error()) + return + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + panic(r) + } + }() + fi.Room.IsRunning = true fi.Room.IsOver = false fi.Room.TeamTurn = "blue" @@ -174,10 +189,42 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) { Action: models.ActionTypeGameStarted, } fi.Room.ActionHistory = append(fi.Room.ActionHistory, action) - if err := saveFullInfo(r.Context(), fi); err != nil { + + // Use the new context with transaction + if err := saveFullInfo(ctx, fi); err != nil { + tx.Rollback() abortWithError(w, err.Error()) return } + + // Save action history + action.RoomID = fi.Room.ID + action.CreatedAtUnix = time.Now().Unix() + if err := repo.CreateAction(ctx, fi.Room.ID, &action); err != nil { + tx.Rollback() + log.Error("failed to save action", "error", err) + abortWithError(w, err.Error()) + return + } + + // Save word cards + for _, card := range fi.Room.Cards { + card.RoomID = fi.Room.ID // Ensure RoomID is set for each card + if err := repo.WordCardsCreate(ctx, &card); err != nil { + tx.Rollback() + log.Error("failed to save word card", "error", err) + abortWithError(w, err.Error()) + return + } + } + + // Commit the transaction + if err := tx.Commit(); err != nil { + log.Error("failed to commit transaction", "error", err) + abortWithError(w, err.Error()) + return + } + // reveal all cards if fi.State.Role == "mime" { fi.Room.MimeView() diff --git a/repos/main.go b/repos/main.go index 30bb096..1c1cd22 100644 --- a/repos/main.go +++ b/repos/main.go @@ -17,6 +17,7 @@ type AllRepos interface { PlayersRepo SessionsRepo WordCardsRepo + InitTx(ctx context.Context) (context.Context, *sqlx.Tx, error) } type RepoProvider struct { @@ -95,3 +96,11 @@ func getDB(ctx context.Context, db *sqlx.DB) sqlx.ExtContext { } return db } + +func (p *RepoProvider) InitTx(ctx context.Context) (context.Context, *sqlx.Tx, error) { + tx, err := p.DB.BeginTxx(ctx, nil) + if err != nil { + return nil, nil, err + } + return context.WithValue(ctx, "tx", tx), tx, nil +} diff --git a/repos/word_cards_test.go b/repos/word_cards_test.go new file mode 100644 index 0000000..9b8f882 --- /dev/null +++ b/repos/word_cards_test.go @@ -0,0 +1,496 @@ +package repos + +import ( + "context" + "gralias/models" + "os" + "testing" + "time" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + _ "github.com/mattn/go-sqlite3" +) + +func TestWordCardsRepo_Create(t *testing.T) { + // Setup temporary file-based SQLite database for this test + tempFile, err := os.CreateTemp("", "test_db_*.db") + assert.NoError(t, err) + tempFile.Close() + defer os.Remove(tempFile.Name()) + + db, err := sqlx.Connect("sqlite3", tempFile.Name()) + assert.NoError(t, err) + defer db.Close() + + // Enable foreign key constraints + _, err = db.Exec("PRAGMA foreign_keys = ON;") + assert.NoError(t, err) + + // Apply schema + schema := ` + CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + creator_name TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS 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 + ); + ` + _, err = db.Exec(schema) + assert.NoError(t, err) + + ctx := context.Background() + + roomID := "test_room_1" + // Insert a room first, as word_cards has a foreign key to rooms + _, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name) VALUES (?, ?, ?)`, roomID, time.Now(), "test_creator") + assert.NoError(t, err) + + // Test single card creation + card1 := &models.WordCard{ + RoomID: roomID, + Word: "apple", + Color: models.WordColorRed, + Revealed: false, + Mime: false, + } + _, err = db.Exec(`INSERT INTO word_cards (room_id, word, color, revealed, mime_view) VALUES (?, ?, ?, ?, ?)`, card1.RoomID, card1.Word, card1.Color, card1.Revealed, card1.Mime) + assert.NoError(t, err) + + var count int + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ?", roomID) + assert.NoError(t, err) + assert.Equal(t, 1, count) + + // Test batch card creation with transaction commit + tx, err := db.BeginTxx(ctx, nil) + assert.NoError(t, err) + + card2 := &models.WordCard{ + RoomID: roomID, + Word: "banana", + Color: models.WordColorBlue, + Revealed: false, + Mime: false, + } + card3 := &models.WordCard{ + RoomID: roomID, + Word: "cherry", + Color: models.WordColorBlack, + Revealed: false, + Mime: false, + } + + _, err = tx.Exec(`INSERT INTO word_cards (room_id, word, color, revealed, mime_view) VALUES (?, ?, ?, ?, ?)`, card2.RoomID, card2.Word, card2.Color, card2.Revealed, card2.Mime) + assert.NoError(t, err) + _, err = tx.Exec(`INSERT INTO word_cards (room_id, word, color, revealed, mime_view) VALUES (?, ?, ?, ?, ?)`, card3.RoomID, card3.Word, card3.Color, card3.Revealed, card3.Mime) + assert.NoError(t, err) + + // Before commit, count should not reflect changes if using a transaction context + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ?", roomID) + assert.NoError(t, err) + assert.Equal(t, 1, count) // Should still be 1 if not committed + + err = tx.Commit() + assert.NoError(t, err) + + // After commit, count should reflect changes + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ?", roomID) + assert.NoError(t, err) + assert.Equal(t, 3, count) + + // Test transaction rollback + tx2, err := db.BeginTxx(ctx, nil) + assert.NoError(t, err) + + card4 := &models.WordCard{ + RoomID: roomID, + Word: "date", + Color: models.WordColorWhite, + Revealed: false, + Mime: false, + } + _, err = tx2.Exec(`INSERT INTO word_cards (room_id, word, color, revealed, mime_view) VALUES (?, ?, ?, ?, ?)`, card4.RoomID, card4.Word, card4.Color, card4.Revealed, card4.Mime) + assert.NoError(t, err) + + err = tx2.Rollback() + assert.NoError(t, err) + + // After rollback, count should not reflect changes + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ?", roomID) + assert.NoError(t, err) + assert.Equal(t, 3, count) +} + + + +func TestWordCardsRepo_GetByWordAndRoomID(t *testing.T) { + // Setup temporary file-based SQLite database for this test + tempFile, err := os.CreateTemp("", "test_db_*.db") + assert.NoError(t, err) + tempFile.Close() + defer os.Remove(tempFile.Name()) + + db, err := sqlx.Connect("sqlite3", tempFile.Name()) + assert.NoError(t, err) + defer db.Close() + + // Enable foreign key constraints + _, err = db.Exec("PRAGMA foreign_keys = ON;") + assert.NoError(t, err) + + // Apply schema + schema := ` + CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + creator_name TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS 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 + ); + ` + _, err = db.Exec(schema) + assert.NoError(t, err) + + repo := &RepoProvider{DB: db} + ctx := context.Background() + + roomID := "test_room_3" + _, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name) VALUES (?, ?, ?)`, roomID, time.Now(), "test_creator") + assert.NoError(t, err) + + card := &models.WordCard{ + RoomID: roomID, + Word: "gamma", + Color: models.WordColorRed, + Revealed: false, + Mime: false, + } + err = repo.WordCardsCreate(ctx, card) + assert.NoError(t, err) + + retrievedCard, err := repo.WordCardGetByWordAndRoomID(ctx, "gamma", roomID) + assert.NoError(t, err) + assert.NotNil(t, retrievedCard) + assert.Equal(t, "gamma", retrievedCard.Word) + assert.Equal(t, roomID, retrievedCard.RoomID) + + // Test non-existent card + _, err = repo.WordCardGetByWordAndRoomID(ctx, "non_existent", roomID) + assert.Error(t, err) +} + +func TestWordCardsRepo_Reveal(t *testing.T) { + // Setup temporary file-based SQLite database for this test + tempFile, err := os.CreateTemp("", "test_db_*.db") + assert.NoError(t, err) + tempFile.Close() + defer os.Remove(tempFile.Name()) + + db, err := sqlx.Connect("sqlite3", tempFile.Name()) + assert.NoError(t, err) + defer db.Close() + + // Enable foreign key constraints + _, err = db.Exec("PRAGMA foreign_keys = ON;") + assert.NoError(t, err) + + // Apply schema + schema := ` + CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + creator_name TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS 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 + ); + ` + _, err = db.Exec(schema) + assert.NoError(t, err) + + repo := &RepoProvider{DB: db} + ctx := context.Background() + + roomID := "test_room_4" + _, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name) VALUES (?, ?, ?)`, roomID, time.Now(), "test_creator") + assert.NoError(t, err) + + card := &models.WordCard{ + RoomID: roomID, + Word: "delta", + Color: models.WordColorRed, + Revealed: false, + Mime: false, + } + err = repo.WordCardsCreate(ctx, card) + assert.NoError(t, err) + + // Verify initial state + var revealed bool + err = db.Get(&revealed, "SELECT revealed FROM word_cards WHERE word = ? AND room_id = ?", "delta", roomID) + assert.NoError(t, err) + assert.False(t, revealed) + + // Reveal the card + err = repo.WordCardReveal(ctx, "delta", roomID) + assert.NoError(t, err) + + // Verify revealed state + err = db.Get(&revealed, "SELECT revealed FROM word_cards WHERE word = ? AND room_id = ?", "delta", roomID) + assert.NoError(t, err) + assert.True(t, revealed) +} + +func TestWordCardsRepo_RevealAll(t *testing.T) { + // Setup temporary file-based SQLite database for this test + tempFile, err := os.CreateTemp("", "test_db_*.db") + assert.NoError(t, err) + tempFile.Close() + defer os.Remove(tempFile.Name()) + + db, err := sqlx.Connect("sqlite3", tempFile.Name()) + assert.NoError(t, err) + defer db.Close() + + // Enable foreign key constraints + _, err = db.Exec("PRAGMA foreign_keys = ON;") + assert.NoError(t, err) + + // Apply schema + schema := ` + CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + creator_name TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS 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 + ); + ` + _, err = db.Exec(schema) + assert.NoError(t, err) + + repo := &RepoProvider{DB: db} + ctx := context.Background() + + roomID := "test_room_5" + _, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name) VALUES (?, ?, ?)`, roomID, time.Now(), "test_creator") + assert.NoError(t, err) + + cardsToInsert := []*models.WordCard{ + { + RoomID: roomID, + Word: "echo", + Color: models.WordColorRed, + Revealed: false, + Mime: false, + }, + { + RoomID: roomID, + Color: models.WordColorBlue, + Revealed: false, + Mime: false, + }, + } + + for _, card := range cardsToInsert { + err = repo.WordCardsCreate(ctx, card) + assert.NoError(t, err) + } + + // Verify initial state + var count int + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ? AND revealed = FALSE", roomID) + assert.NoError(t, err) + assert.Equal(t, 2, count) + + // Reveal all cards + err = repo.WordCardsRevealAll(ctx, roomID) + assert.NoError(t, err) + + // Verify all cards are revealed + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ? AND revealed = FALSE", roomID) + assert.NoError(t, err) + assert.Equal(t, 0, count) +} + +func TestWordCardsRepo_DeleteByRoomID(t *testing.T) { + // Setup temporary file-based SQLite database for this test + tempFile, err := os.CreateTemp("", "test_db_*.db") + assert.NoError(t, err) + tempFile.Close() + defer os.Remove(tempFile.Name()) + + db, err := sqlx.Connect("sqlite3", tempFile.Name()) + assert.NoError(t, err) + defer db.Close() + + // Enable foreign key constraints + _, err = db.Exec("PRAGMA foreign_keys = ON;") + assert.NoError(t, err) + + // Apply schema + schema := ` + CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + creator_name TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS 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 + ); + ` + _, err = db.Exec(schema) + assert.NoError(t, err) + + repo := &RepoProvider{DB: db} + ctx := context.Background() + + roomID := "test_room_6" + _, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name) VALUES (?, ?, ?)`, roomID, time.Now(), "test_creator") + assert.NoError(t, err) + + cardsToInsert := []*models.WordCard{ + { + RoomID: roomID, + Word: "golf", + Color: models.WordColorRed, + Revealed: false, + Mime: false, + }, + { + RoomID: roomID, + Word: "hotel", + Color: models.WordColorBlue, + Revealed: false, + Mime: false, + }, + } + + for _, card := range cardsToInsert { + err = repo.WordCardsCreate(ctx, card) + assert.NoError(t, err) + } + + // Verify initial state + var count int + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ?", roomID) + assert.NoError(t, err) + assert.Equal(t, 2, count) + + // Delete cards by room ID + err = repo.WordCardsDeleteByRoomID(ctx, roomID) + assert.NoError(t, err) + + // Verify cards are deleted + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ?", roomID) + assert.NoError(t, err) + assert.Equal(t, 0, count) +} + +func TestWordCardsRepo_CascadeDeleteRoom(t *testing.T) { + // Setup temporary file-based SQLite database for this test + tempFile, err := os.CreateTemp("", "test_db_*.db") + assert.NoError(t, err) + tempFile.Close() + defer os.Remove(tempFile.Name()) + + db, err := sqlx.Connect("sqlite3", tempFile.Name()) + assert.NoError(t, err) + defer db.Close() + + // Enable foreign key constraints + _, err = db.Exec("PRAGMA foreign_keys = ON;") + assert.NoError(t, err) + + // Apply schema + schema := ` + CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + creator_name TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS 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 + ); + ` + _, err = db.Exec(schema) + assert.NoError(t, err) + + repo := &RepoProvider{DB: db} + ctx := context.Background() + + roomID := "test_room_7" + _, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name) VALUES (?, ?, ?)`, roomID, time.Now(), "test_creator") + assert.NoError(t, err) + + card := &models.WordCard{ + RoomID: roomID, + Word: "india", + Color: models.WordColorRed, + Revealed: false, + Mime: false, + } + err = repo.WordCardsCreate(ctx, card) + assert.NoError(t, err) + + var count int + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ?", roomID) + assert.NoError(t, err) + assert.Equal(t, 1, count) + + _, err = db.Exec(`DELETE FROM rooms WHERE id = ?`, roomID) + assert.NoError(t, err) + + err = db.Get(&count, "SELECT COUNT(*) FROM word_cards WHERE room_id = ?", roomID) + assert.NoError(t, err) + assert.Equal(t, 0, count) +} + +