diff --git a/go.mod b/go.mod index eca8b06..432a71f 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,14 @@ go 1.24 require ( github.com/BurntSushi/toml v1.5.0 - github.com/jackc/pgx/v5 v5.7.5 + github.com/jmoiron/sqlx v1.4.0 + github.com/mattn/go-sqlite3 v1.14.28 github.com/rs/xid v1.6.0 + github.com/stretchr/testify v1.10.0 ) require ( - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/text v0.24.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2d701d0..2572d82 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,25 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= -github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gralias.db b/gralias.db index 47b356f..2c8a5e1 100644 Binary files a/gralias.db and b/gralias.db differ diff --git a/handlers/actions.go b/handlers/actions.go index 6ba2082..ce77b8e 100644 --- a/handlers/actions.go +++ b/handlers/actions.go @@ -72,6 +72,10 @@ func getStateByCtx(ctx context.Context) (*models.UserState, error) { return us, nil } +// func dbCreate(fi *models.FullInfo) error{ +// repo.CreateRoom() +// } + func saveFullInfo(fi *models.FullInfo) error { // INFO: no transactions; so case is possible where first object is updated but the second is not if err := saveState(fi.State.Username, fi.State); err != nil { diff --git a/handlers/game.go b/handlers/game.go index 57648df..e5073ba 100644 --- a/handlers/game.go +++ b/handlers/game.go @@ -43,6 +43,10 @@ func HandleCreateRoom(w http.ResponseWriter, r *http.Request) { fi.State.RoomID = room.ID fi.Room = room fi.Room.IsPublic = true // hardcode for local test; move to form + if err := repo.CreateRoom(r.Context(), room); err != nil { + abortWithError(w, err.Error()) + return + } if err := saveFullInfo(fi); err != nil { msg := "failed to set current room to session" log.Error(msg, "error", err) diff --git a/handlers/handlers.go b/handlers/handlers.go index 72d0585..4a0c22e 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -5,6 +5,7 @@ import ( "gralias/config" "gralias/models" "gralias/pkg/cache" + "gralias/repos" "html/template" "log/slog" "net/http" @@ -17,6 +18,7 @@ var ( cfg *config.Config memcache cache.Cache Notifier *broker.Broker + repo repos.AllRepos ) func init() { @@ -30,6 +32,7 @@ func init() { cache.MemCache.StartBackupRoutine(15 * time.Second) // Reduced backup interval // bot loader // check the rooms if it has bot_{digits} in them, create bots if have + repo = repos.NewRepoProvider("sqlite3://../gralias.db") recoverBots() // if player has a roomID, but no team and role, try to recover recoverPlayers() @@ -57,7 +60,11 @@ func HandleHome(w http.ResponseWriter, r *http.Request) { } } if fi != nil && fi.Room == nil { - fi.List = listRooms(false) + rooms, err := repo.ListRooms(r.Context()) + if err != nil { + log.Error("failed to list rooms;", "error", err) + } + fi.List = rooms } if err := tmpl.ExecuteTemplate(w, "base", fi); err != nil { log.Error("failed to exec templ;", "error", err, "templ", "base") diff --git a/handlers/sqlite:gralias.db b/handlers/sqlite:gralias.db new file mode 100644 index 0000000..e69de29 diff --git a/handlers/timer.go b/handlers/timer.go index 4cce145..7564d0e 100644 --- a/handlers/timer.go +++ b/handlers/timer.go @@ -54,9 +54,9 @@ func StartTurnTimer(roomID string, duration time.Duration) { return } room.Settings.TurnSecondsLeft-- - if err := saveRoom(room); err != nil { - log.Error("failed to save room", "error", err) - } + // if err := saveRoom(room); err != nil { + // log.Error("failed to save room", "error", err) + // } notify(models.NotifyRoomUpdatePrefix+room.ID, "") } } diff --git a/models/main.go b/models/main.go index fd10f79..de5de91 100644 --- a/models/main.go +++ b/models/main.go @@ -58,7 +58,8 @@ type Action struct { Word string `json:"word" db:"word"` WordColor string `json:"word_color" db:"word_color"` Number string `json:"number_associated" db:"number_associated"` - CreatedAt time.Time `json:"created_at" db:"created_at"` + CreatedAt time.Time `json:"created_at" db:"-"` + CreatedAtUnix int64 `db:"created_at"` } type Player struct { diff --git a/repos/actions.go b/repos/actions.go index f2fa1ed..27da40a 100644 --- a/repos/actions.go +++ b/repos/actions.go @@ -3,8 +3,7 @@ package repos import ( "context" "gralias/models" - - "github.com/jackc/pgx/v5" + "time" ) type ActionsRepo interface { @@ -15,32 +14,33 @@ type ActionsRepo interface { } func (p *RepoProvider) ListActions(ctx context.Context, roomID string) ([]models.Action, error) { - rows, err := p.DB.Query(ctx, `SELECT actor, actor_color, action_type, word, word_color, number_associated FROM actions WHERE room_id = $1 ORDER BY created_at ASC`, roomID) + actions := []models.Action{} + err := p.DB.SelectContext(ctx, &actions, `SELECT actor, actor_color, action_type, word, word_color, number_associated, created_at FROM actions WHERE room_id = ? ORDER BY created_at ASC`, roomID) if err != nil { return nil, err } - return pgx.CollectRows(rows, pgx.RowToStructByName[models.Action]) + for i := range actions { + actions[i].CreatedAt = time.Unix(0, actions[i].CreatedAtUnix) + } + return actions, nil } func (p *RepoProvider) CreateAction(ctx context.Context, roomID string, a *models.Action) error { - _, err := p.DB.Exec(ctx, `INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated) VALUES ($1, $2, $3, $4, $5, $6, $7)`, - roomID, a.Actor, a.ActorColor, a.Action, a.Word, a.WordColor, a.Number) + _, err := p.DB.ExecContext(ctx, `INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, a.Actor, a.ActorColor, a.Action, a.Word, a.WordColor, a.Number, a.CreatedAt.UnixNano()) return err } func (p *RepoProvider) GetLastClue(ctx context.Context, roomID string) (*models.Action, error) { - rows, err := p.DB.Query(ctx, `SELECT actor, actor_color, action_type, word, word_color, number_associated FROM actions WHERE room_id = $1 AND action_type = 'gave_clue' ORDER BY created_at DESC LIMIT 1`, roomID) + action := &models.Action{} + err := p.DB.GetContext(ctx, action, `SELECT actor, actor_color, action_type, word, word_color, number_associated, created_at FROM actions WHERE room_id = ? AND action_type = 'gave_clue' ORDER BY created_at DESC LIMIT 1`, roomID) if err != nil { return nil, err } - action, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[models.Action]) - if err != nil { - return nil, err - } - return &action, nil + action.CreatedAt = time.Unix(0, action.CreatedAtUnix) + return action, nil } func (p *RepoProvider) DeleteActionsByRoomID(ctx context.Context, roomID string) error { - _, err := p.DB.Exec(ctx, `DELETE FROM actions WHERE room_id = $1`, roomID) + _, err := p.DB.ExecContext(ctx, `DELETE FROM actions WHERE room_id = ?`, roomID) return err } \ No newline at end of file diff --git a/repos/actions_test.go b/repos/actions_test.go new file mode 100644 index 0000000..93fc2c3 --- /dev/null +++ b/repos/actions_test.go @@ -0,0 +1,177 @@ +package repos + +import ( + "context" + "gralias/models" + "testing" + "time" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + _ "github.com/mattn/go-sqlite3" +) + +func setupActionsTestDB(t *testing.T) (*sqlx.DB, func()) { + db, err := sqlx.Connect("sqlite3", ":memory:") + assert.NoError(t, err) + + schema := ` + CREATE TABLE IF NOT EXISTS actions ( + room_id TEXT, + actor TEXT, + actor_color TEXT, + action_type TEXT, + word TEXT, + word_color TEXT, + number_associated TEXT, + created_at INTEGER + ); + ` + _, err = db.Exec(schema) + assert.NoError(t, err) + + return db, func() { + db.Close() + } +} + +func TestActionsRepo_CreateAction(t *testing.T) { + db, teardown := setupActionsTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + roomID := "test_room_actions_1" + action := &models.Action{ + Actor: "player1", + ActorColor: "blue", + Action: "gave_clue", + Word: "apple", + WordColor: "red", + Number: "3", + CreatedAt: time.Now(), + } + + err := repo.CreateAction(context.Background(), roomID, action) + assert.NoError(t, err) + + var retrievedAction models.Action + err = db.Get(&retrievedAction, "SELECT * FROM actions WHERE room_id = ? AND actor = ?", roomID, action.Actor) + assert.NoError(t, err) + assert.Equal(t, action.Word, retrievedAction.Word) +} + +func TestActionsRepo_ListActions(t *testing.T) { + db, teardown := setupActionsTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + roomID := "test_room_actions_2" + action1 := &models.Action{ + Actor: "player1", + ActorColor: "blue", + Action: "gave_clue", + Word: "apple", + WordColor: "red", + Number: "3", + CreatedAt: time.Now().Add(-2 * time.Second), + } + action2 := &models.Action{ + Actor: "player2", + ActorColor: "red", + Action: "guessed", + Word: "banana", + WordColor: "blue", + Number: "0", + CreatedAt: time.Now().Add(-1 * time.Second), + } + + _, err := db.Exec(`INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, action1.Actor, action1.ActorColor, action1.Action, action1.Word, action1.WordColor, action1.Number, action1.CreatedAt.UnixNano()) + assert.NoError(t, err) + _, err = db.Exec(`INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, action2.Actor, action2.ActorColor, action2.Action, action2.Word, action2.WordColor, action2.Number, action2.CreatedAt.UnixNano()) + assert.NoError(t, err) + + actions, err := repo.ListActions(context.Background(), roomID) + assert.NoError(t, err) + assert.Len(t, actions, 2) + assert.Equal(t, action1.Word, actions[0].Word) + assert.Equal(t, action2.Word, actions[1].Word) +} + +func TestActionsRepo_GetLastClue(t *testing.T) { + db, teardown := setupActionsTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + roomID := "test_room_actions_3" + action1 := &models.Action{ + Actor: "player1", + ActorColor: "blue", + Action: "gave_clue", + Word: "apple", + WordColor: "red", + Number: "3", + CreatedAt: time.Now().Add(-3 * time.Second), + } + action2 := &models.Action{ + Actor: "player2", + ActorColor: "red", + Action: "gave_clue", + Word: "banana", + WordColor: "blue", + Number: "2", + CreatedAt: time.Now().Add(-2 * time.Second), + } + // Non-clue action + action3 := &models.Action{ + Actor: "player3", + ActorColor: "blue", + Action: "guessed", + Word: "orange", + WordColor: "blue", + Number: "0", + CreatedAt: time.Now().Add(-1 * time.Second), + } + + _, err := db.Exec(`INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, action1.Actor, action1.ActorColor, action1.Action, action1.Word, action1.WordColor, action1.Number, action1.CreatedAt.UnixNano()) + assert.NoError(t, err) + _, err = db.Exec(`INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, action2.Actor, action2.ActorColor, action2.Action, action2.Word, action2.WordColor, action2.Number, action2.CreatedAt.UnixNano()) + assert.NoError(t, err) + _, err = db.Exec(`INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, action3.Actor, action3.ActorColor, action3.Action, action3.Word, action3.WordColor, action3.Number, action3.CreatedAt.UnixNano()) + assert.NoError(t, err) + + lastClue, err := repo.GetLastClue(context.Background(), roomID) + assert.NoError(t, err) + assert.NotNil(t, lastClue) + assert.Equal(t, action2.Word, lastClue.Word) +} + +func TestActionsRepo_DeleteActionsByRoomID(t *testing.T) { + db, teardown := setupActionsTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + roomID := "test_room_actions_4" + action1 := &models.Action{ + Actor: "player1", + ActorColor: "blue", + Action: "gave_clue", + Word: "apple", + WordColor: "red", + Number: "3", + CreatedAt: time.Now(), + } + _, err := db.Exec(`INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, action1.Actor, action1.ActorColor, action1.Action, action1.Word, action1.WordColor, action1.Number, action1.CreatedAt.UnixNano()) + assert.NoError(t, err) + + err = repo.DeleteActionsByRoomID(context.Background(), roomID) + assert.NoError(t, err) + + var count int + err = db.Get(&count, "SELECT COUNT(*) FROM actions WHERE room_id = ?", roomID) + assert.NoError(t, err) + assert.Equal(t, 0, count) +} \ No newline at end of file diff --git a/repos/main.go b/repos/main.go index 04bf44b..07db228 100644 --- a/repos/main.go +++ b/repos/main.go @@ -1,11 +1,11 @@ package repos import ( - "context" "log/slog" "os" - "github.com/jackc/pgx/v5/pgxpool" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" ) type AllRepos interface { @@ -15,17 +15,17 @@ type AllRepos interface { } type RepoProvider struct { - DB *pgxpool.Pool + DB *sqlx.DB } func NewRepoProvider(pathToDB string) *RepoProvider { - dbpool, err := pgxpool.New(context.Background(), pathToDB) + db, err := sqlx.Connect("sqlite3", pathToDB) if err != nil { slog.Error("Unable to connect to database", "error", err) os.Exit(1) } slog.Info("Successfully connected to database") return &RepoProvider{ - DB: dbpool, + DB: db, } } diff --git a/repos/main_test.go b/repos/main_test.go new file mode 100644 index 0000000..557f483 --- /dev/null +++ b/repos/main_test.go @@ -0,0 +1,24 @@ +package repos + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewRepoProvider(t *testing.T) { + // Create a temporary SQLite database file for testing + tmpDBFile := "./test_gralias.db" + defer os.Remove(tmpDBFile) // Clean up the temporary file after the test + + // Initialize a new RepoProvider + repoProvider := NewRepoProvider(tmpDBFile) + + // Assert that the DB connection is not nil + assert.NotNil(t, repoProvider.DB, "DB connection should not be nil") + + // Close the database connection + err := repoProvider.DB.Close() + assert.NoError(t, err, "Error closing database connection") +} \ No newline at end of file diff --git a/repos/players.go b/repos/players.go index d96b947..5453adc 100644 --- a/repos/players.go +++ b/repos/players.go @@ -13,7 +13,7 @@ type PlayersRepo interface { func (p *RepoProvider) GetPlayer(roomID, username string) (*models.Player, error) { var player models.Player - err := p.DB.QueryRow(context.Background(), "SELECT id, room_id, username, team, role, is_bot FROM players WHERE room_id = $1 AND username = $2", roomID, username).Scan(&player.ID, &player.RoomID, &player.Username, &player.Team, &player.Role, &player.IsBot) + err := p.DB.GetContext(context.Background(), &player, "SELECT id, room_id, username, team, role, is_bot FROM players WHERE room_id = ? AND username = ?", roomID, username) if err != nil { return nil, err } @@ -21,11 +21,11 @@ func (p *RepoProvider) GetPlayer(roomID, username string) (*models.Player, error } func (p *RepoProvider) AddPlayer(player *models.Player) error { - _, err := p.DB.Exec(context.Background(), "INSERT INTO players (room_id, username, team, role, is_bot) VALUES ($1, $2, $3, $4, $5)", player.RoomID, player.Username, player.Team, player.Role, player.IsBot) + _, err := p.DB.ExecContext(context.Background(), "INSERT INTO players (room_id, username, team, role, is_bot) VALUES (?, ?, ?, ?, ?)", player.RoomID, player.Username, player.Team, player.Role, player.IsBot) return err } func (p *RepoProvider) DeletePlayer(roomID, username string) error { - _, err := p.DB.Exec(context.Background(), "DELETE FROM players WHERE room_id = $1 AND username = $2", roomID, username) + _, err := p.DB.ExecContext(context.Background(), "DELETE FROM players WHERE room_id = ? AND username = ?", roomID, username) return err } diff --git a/repos/players_test.go b/repos/players_test.go new file mode 100644 index 0000000..73bc9bd --- /dev/null +++ b/repos/players_test.go @@ -0,0 +1,104 @@ +package repos + +import ( + "gralias/models" + "testing" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + _ "github.com/mattn/go-sqlite3" +) + +func setupPlayersTestDB(t *testing.T) (*sqlx.DB, func()) { + db, err := sqlx.Connect("sqlite3", ":memory:") + assert.NoError(t, err) + + schema := ` + CREATE TABLE IF NOT EXISTS players ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT, + username TEXT, + team TEXT, + role TEXT, + is_bot BOOLEAN + ); + ` + _, err = db.Exec(schema) + assert.NoError(t, err) + + return db, func() { + db.Close() + } +} + +func TestPlayersRepo_AddPlayer(t *testing.T) { + db, teardown := setupPlayersTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + player := &models.Player{ + RoomID: "test_room_player_1", + Username: "test_player_1", + Team: "blue", + Role: "player", + IsBot: false, + } + + err := repo.AddPlayer(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} + + player := &models.Player{ + RoomID: "test_room_player_2", + 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.GetPlayer(player.RoomID, 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} + + player := &models.Player{ + RoomID: "test_room_player_3", + 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.DeletePlayer(player.RoomID, 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) +} \ No newline at end of file diff --git a/repos/rooms.go b/repos/rooms.go index 0daa377..af8a68d 100644 --- a/repos/rooms.go +++ b/repos/rooms.go @@ -3,24 +3,19 @@ package repos import ( "context" "gralias/models" - - "github.com/jackc/pgx/v5" ) type RoomsRepo interface { - ListRooms(ctx context.Context) ([]models.Room, error) + ListRooms(ctx context.Context) ([]*models.Room, error) GetRoomByID(ctx context.Context, id string) (*models.Room, error) CreateRoom(ctx context.Context, room *models.Room) error DeleteRoomByID(ctx context.Context, id string) error UpdateRoom(ctx context.Context, room *models.Room) error } -func (p *RepoProvider) ListRooms(ctx context.Context) ([]models.Room, error) { - rows, err := p.DB.Query(ctx, `SELECT * FROM rooms`) - if err != nil { - return nil, err - } - rooms, err := pgx.CollectRows(rows, pgx.RowToStructByName[models.Room]) +func (p *RepoProvider) ListRooms(ctx context.Context) ([]*models.Room, error) { + rooms := []*models.Room{} + err := p.DB.SelectContext(ctx, &rooms, `SELECT * FROM rooms`) if err != nil { return nil, err } @@ -28,28 +23,25 @@ func (p *RepoProvider) ListRooms(ctx context.Context) ([]models.Room, error) { } func (p *RepoProvider) GetRoomByID(ctx context.Context, id string) (*models.Room, error) { - rows, err := p.DB.Query(ctx, `SELECT * FROM rooms WHERE id = $1`, id) + room := &models.Room{} + err := p.DB.GetContext(ctx, room, `SELECT * FROM rooms WHERE id = ?`, id) if err != nil { return nil, err } - room, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[models.Room]) - if err != nil { - return nil, err - } - return &room, nil + return room, nil } func (p *RepoProvider) CreateRoom(ctx context.Context, r *models.Room) error { - _, err := p.DB.Exec(ctx, `INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`, r.ID, r.CreatedAt, r.CreatorName, r.TeamTurn, r.ThisTurnLimit, r.OpenedThisTurn, r.BlueCounter, r.RedCounter, r.RedTurn, r.MimeDone, r.IsPublic, r.IsRunning, r.Language, r.RoundTime, r.IsOver, r.TeamWon, r.Settings.RoomPass) + _, err := p.DB.ExecContext(ctx, `INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, r.ID, r.CreatedAt, r.CreatorName, r.TeamTurn, r.ThisTurnLimit, r.OpenedThisTurn, r.BlueCounter, r.RedCounter, r.RedTurn, r.MimeDone, r.IsPublic, r.IsRunning, r.Language, r.RoundTime, r.IsOver, r.TeamWon, r.RoomPass) return err } func (p *RepoProvider) DeleteRoomByID(ctx context.Context, id string) error { - _, err := p.DB.Exec(ctx, `DELETE FROM rooms WHERE id = $1`, id) + _, err := p.DB.ExecContext(ctx, `DELETE FROM rooms WHERE id = ?`, id) return err } func (p *RepoProvider) UpdateRoom(ctx context.Context, r *models.Room) error { - _, err := p.DB.Exec(ctx, `UPDATE rooms SET team_turn = $1, this_turn_limit = $2, opened_this_turn = $3, blue_counter = $4, red_counter = $5, red_turn = $6, mime_done = $7, is_public = $8, is_running = $9, language = $10, round_time = $11, is_over = $12, team_won = $13, room_pass = $14 WHERE id = $15`, r.TeamTurn, r.ThisTurnLimit, r.OpenedThisTurn, r.BlueCounter, r.RedCounter, r.RedTurn, r.MimeDone, r.IsPublic, r.IsRunning, r.Language, r.RoundTime, r.IsOver, r.TeamWon, r.Settings.RoomPass, r.ID) + _, err := p.DB.ExecContext(ctx, `UPDATE rooms SET team_turn = ?, this_turn_limit = ?, opened_this_turn = ?, blue_counter = ?, red_counter = ?, red_turn = ?, mime_done = ?, is_public = ?, is_running = ?, language = ?, round_time = ?, is_over = ?, team_won = ?, room_pass = ? WHERE id = ?`, r.TeamTurn, r.ThisTurnLimit, r.OpenedThisTurn, r.BlueCounter, r.RedCounter, r.RedTurn, r.MimeDone, r.IsPublic, r.IsRunning, r.Language, r.RoundTime, r.IsOver, r.TeamWon, r.RoomPass, r.ID) return err } diff --git a/repos/rooms_test.go b/repos/rooms_test.go new file mode 100644 index 0000000..32d0bfb --- /dev/null +++ b/repos/rooms_test.go @@ -0,0 +1,254 @@ +package repos + +import ( + "context" + "gralias/models" + "testing" + "time" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + _ "github.com/mattn/go-sqlite3" +) + +func setupTestDB(t *testing.T) (*sqlx.DB, func()) { + db, err := sqlx.Connect("sqlite3", ":memory:") + assert.NoError(t, err) + + schema := ` + CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + created_at DATETIME, + creator_name TEXT, + team_turn TEXT, + this_turn_limit INTEGER, + opened_this_turn INTEGER, + blue_counter INTEGER, + red_counter INTEGER, + red_turn BOOLEAN, + mime_done BOOLEAN, + is_public BOOLEAN, + is_running BOOLEAN, + language TEXT, + round_time INTEGER, + is_over BOOLEAN, + team_won TEXT, + room_pass TEXT + ); + ` + _, err = db.Exec(schema) + assert.NoError(t, err) + + return db, func() { + db.Close() + } +} + +func TestRoomsRepo_CreateRoom(t *testing.T) { + db, teardown := setupTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + room := &models.Room{ + ID: "test_room_1", + CreatedAt: time.Now(), + CreatorName: "test_creator", + TeamTurn: "blue", + ThisTurnLimit: 5, + OpenedThisTurn: 0, + BlueCounter: 0, + RedCounter: 0, + RedTurn: false, + MimeDone: false, + IsPublic: true, + IsRunning: false, + Language: "en", + RoundTime: 60, + IsOver: false, + TeamWon: "", + RoomPass: "", + } + + err := repo.CreateRoom(context.Background(), room) + assert.NoError(t, err) + + // Verify the room was created + var retrievedRoom models.Room + err = db.Get(&retrievedRoom, "SELECT * FROM rooms WHERE id = ?", room.ID) + assert.NoError(t, err) + assert.Equal(t, room.ID, retrievedRoom.ID) + assert.Equal(t, room.CreatorName, retrievedRoom.CreatorName) +} + +func TestRoomsRepo_GetRoomByID(t *testing.T) { + db, teardown := setupTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + room := &models.Room{ + ID: "test_room_2", + CreatedAt: time.Now(), + CreatorName: "test_creator_2", + TeamTurn: "red", + ThisTurnLimit: 5, + OpenedThisTurn: 0, + BlueCounter: 0, + RedCounter: 0, + RedTurn: true, + MimeDone: false, + IsPublic: true, + IsRunning: false, + Language: "en", + RoundTime: 60, + IsOver: false, + TeamWon: "", + RoomPass: "", + } + + // Insert a room directly into the database for testing GetRoomByID + _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsPublic, room.IsRunning, room.Language, room.RoundTime, room.IsOver, room.TeamWon, room.RoomPass) + assert.NoError(t, err) + + retrievedRoom, err := repo.GetRoomByID(context.Background(), room.ID) + assert.NoError(t, err) + assert.NotNil(t, retrievedRoom) + assert.Equal(t, room.ID, retrievedRoom.ID) + assert.Equal(t, room.CreatorName, retrievedRoom.CreatorName) +} + +func TestRoomsRepo_ListRooms(t *testing.T) { + db, teardown := setupTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + room1 := &models.Room{ + ID: "list_room_1", + CreatedAt: time.Now(), + CreatorName: "list_creator_1", + TeamTurn: "blue", + ThisTurnLimit: 5, + OpenedThisTurn: 0, + BlueCounter: 0, + RedCounter: 0, + RedTurn: false, + MimeDone: false, + IsPublic: true, + IsRunning: false, + Language: "en", + RoundTime: 60, + IsOver: false, + TeamWon: "", + RoomPass: "", + } + room2 := &models.Room{ + ID: "list_room_2", + CreatedAt: time.Now(), + CreatorName: "list_creator_2", + TeamTurn: "red", + ThisTurnLimit: 5, + OpenedThisTurn: 0, + BlueCounter: 0, + RedCounter: 0, + RedTurn: true, + MimeDone: false, + IsPublic: true, + IsRunning: false, + Language: "en", + RoundTime: 60, + IsOver: false, + TeamWon: "", + RoomPass: "", + } + + _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room1.ID, room1.CreatedAt, room1.CreatorName, room1.TeamTurn, room1.ThisTurnLimit, room1.OpenedThisTurn, room1.BlueCounter, room1.RedCounter, room1.RedTurn, room1.MimeDone, room1.IsPublic, room1.IsRunning, room1.Language, room1.RoundTime, room1.IsOver, room1.TeamWon, room1.RoomPass) + assert.NoError(t, err) + _, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room2.ID, room2.CreatedAt, room2.CreatorName, room2.TeamTurn, room2.ThisTurnLimit, room2.OpenedThisTurn, room2.BlueCounter, room2.RedCounter, room2.RedTurn, room2.MimeDone, room2.IsPublic, room2.IsRunning, room2.Language, room2.RoundTime, room2.IsOver, room2.TeamWon, room2.RoomPass) + assert.NoError(t, err) + + rooms, err := repo.ListRooms(context.Background()) + assert.NoError(t, err) + assert.Len(t, rooms, 2) +} + +func TestRoomsRepo_DeleteRoomByID(t *testing.T) { + db, teardown := setupTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + room := &models.Room{ + ID: "delete_room_1", + CreatedAt: time.Now(), + CreatorName: "delete_creator_1", + TeamTurn: "blue", + ThisTurnLimit: 5, + OpenedThisTurn: 0, + BlueCounter: 0, + RedCounter: 0, + RedTurn: false, + MimeDone: false, + IsPublic: true, + IsRunning: false, + Language: "en", + RoundTime: 60, + IsOver: false, + TeamWon: "", + RoomPass: "", + } + + _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsPublic, room.IsRunning, room.Language, room.RoundTime, room.IsOver, room.TeamWon, room.RoomPass) + assert.NoError(t, err) + + err = repo.DeleteRoomByID(context.Background(), room.ID) + assert.NoError(t, err) + + var count int + err = db.Get(&count, "SELECT COUNT(*) FROM rooms WHERE id = ?", room.ID) + assert.NoError(t, err) + assert.Equal(t, 0, count) +} + +func TestRoomsRepo_UpdateRoom(t *testing.T) { + db, teardown := setupTestDB(t) + defer teardown() + + repo := &RepoProvider{DB: db} + + room := &models.Room{ + ID: "update_room_1", + CreatedAt: time.Now(), + CreatorName: "update_creator_1", + TeamTurn: "blue", + ThisTurnLimit: 5, + OpenedThisTurn: 0, + BlueCounter: 0, + RedCounter: 0, + RedTurn: false, + MimeDone: false, + IsPublic: true, + IsRunning: false, + Language: "en", + RoundTime: 60, + IsOver: false, + TeamWon: "", + RoomPass: "", + } + + _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsPublic, room.IsRunning, room.Language, room.RoundTime, room.IsOver, room.TeamWon, room.RoomPass) + assert.NoError(t, err) + + room.TeamTurn = "red" + room.BlueCounter = 10 + + err = repo.UpdateRoom(context.Background(), room) + assert.NoError(t, err) + + var updatedRoom models.Room + err = db.Get(&updatedRoom, "SELECT * FROM rooms WHERE id = ?", room.ID) + assert.NoError(t, err) + assert.Equal(t, models.UserTeam("red"), updatedRoom.TeamTurn) + assert.Equal(t, uint8(10), updatedRoom.BlueCounter) +} \ No newline at end of file diff --git a/sqlite:gralias.db b/sqlite:gralias.db new file mode 100644 index 0000000..e69de29