diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-21 10:29:21 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-21 10:29:21 +0900 |
| commit | e8db174d3e464a5764a9f4bfd82172261bd50519 (patch) | |
| tree | 68cb8f0713fcc1f960a650d879232cb4c20ca6cd /backend/api/handler_test.go | |
| parent | 1be106ac53caa019a8912af932a43570fa8c052d (diff) | |
| download | phperkaigi-2026-albatross-e8db174d3e464a5764a9f4bfd82172261bd50519.tar.gz phperkaigi-2026-albatross-e8db174d3e464a5764a9f4bfd82172261bd50519.tar.zst phperkaigi-2026-albatross-e8db174d3e464a5764a9f4bfd82172261bd50519.zip | |
refactor(api): separate business logic into game, tournament, session packages
Extract business logic from api/handler.go into dedicated service packages:
- session: context helpers (resolves admin → api import dependency)
- game: game state, code submission, ranking, watch logic
- tournament: bracket construction and seed ordering
- api/convert.go: domain → API type conversion functions
api/handler.go is now a thin adapter that delegates to services and
maps domain errors to HTTP status codes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'backend/api/handler_test.go')
| -rw-r--r-- | backend/api/handler_test.go | 829 |
1 files changed, 271 insertions, 558 deletions
diff --git a/backend/api/handler_test.go b/backend/api/handler_test.go index a28d605..2b8c06a 100644 --- a/backend/api/handler_test.go +++ b/backend/api/handler_test.go @@ -11,6 +11,9 @@ import ( "albatross-2026-backend/config" "albatross-2026-backend/db" + "albatross-2026-backend/game" + "albatross-2026-backend/session" + "albatross-2026-backend/tournament" ) // mockQuerier implements db.Querier for testing. @@ -28,6 +31,7 @@ type mockQuerier struct { listTournamentEntriesFunc func(ctx context.Context, tournamentID int32) ([]db.ListTournamentEntriesRow, error) listTournamentMatchesFunc func(ctx context.Context, tournamentID int32) ([]db.TournamentMatch, error) getSubmissionsByGameIDAndUserIDFunc func(ctx context.Context, arg db.GetSubmissionsByGameIDAndUserIDParams) ([]db.Submission, error) + getUserByIDFunc func(ctx context.Context, userID int32) (db.User, error) } func (m *mockQuerier) GetGameByID(ctx context.Context, gameID int32) (db.GetGameByIDRow, error) { @@ -114,6 +118,13 @@ func (m *mockQuerier) ListTournamentMatches(ctx context.Context, tournamentID in return nil, nil } +func (m *mockQuerier) GetUserByID(ctx context.Context, userID int32) (db.User, error) { + if m.getUserByIDFunc != nil { + return m.getUserByIDFunc(ctx, userID) + } + return db.User{}, pgx.ErrNoRows +} + // mockTxManager implements db.TxManager for testing. type mockTxManager struct{} @@ -121,7 +132,7 @@ func (m *mockTxManager) RunInTx(_ context.Context, fn func(q db.Querier) error) return fn(&mockQuerier{}) } -// mockGameHub implements GameHubInterface for testing. +// mockGameHub implements game.GameHubInterface for testing. type mockGameHub struct { calcCodeSizeResult int enqueueErr error @@ -145,14 +156,29 @@ func (m *mockAuthenticator) Login(_ context.Context, _, _ string) (int, error) { return m.loginResult, m.loginErr } -func TestGetGamePlaySubmissions_GameNotFound(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, +func newTestHandler(q *mockQuerier) Handler { + hub := &mockGameHub{} + return Handler{ + gameSvc: game.NewService(q, &mockTxManager{}, hub), + tournamentSvc: tournament.NewService(q), + auth: &mockAuthenticator{}, + conf: &config.Config{}, + q: q, } +} + +func newTestHandlerWithHub(q *mockQuerier, hub *mockGameHub) Handler { + return Handler{ + gameSvc: game.NewService(q, &mockTxManager{}, hub), + tournamentSvc: tournament.NewService(q), + auth: &mockAuthenticator{}, + conf: &config.Config{}, + q: q, + } +} + +func TestGetGamePlaySubmissions_GameNotFound(t *testing.T) { + h := newTestHandler(&mockQuerier{}) user := &db.User{UserID: 1} resp, err := h.GetGamePlaySubmissions(context.Background(), GetGamePlaySubmissionsRequestObject{ GameID: 999, @@ -166,20 +192,14 @@ func TestGetGamePlaySubmissions_GameNotFound(t *testing.T) { } func TestGetGamePlaySubmissions_Empty(t *testing.T) { - h := Handler{ - q: &mockQuerier{ - getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { - return db.GetGameByIDRow{ - GameID: 1, - Language: "php", - }, nil - }, + h := newTestHandler(&mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + Language: "php", + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + }) user := &db.User{UserID: 1} resp, err := h.GetGamePlaySubmissions(context.Background(), GetGamePlaySubmissionsRequestObject{ GameID: 1, @@ -198,45 +218,39 @@ func TestGetGamePlaySubmissions_Empty(t *testing.T) { func TestGetGamePlaySubmissions_WithSubmissions(t *testing.T) { now := time.Now() - h := Handler{ - q: &mockQuerier{ - getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { - return db.GetGameByIDRow{ - GameID: 1, - Language: "php", - }, nil - }, - getSubmissionsByGameIDAndUserIDFunc: func(_ context.Context, arg db.GetSubmissionsByGameIDAndUserIDParams) ([]db.Submission, error) { - if arg.GameID != 1 || arg.UserID != 42 { - t.Errorf("unexpected query params: game_id=%d, user_id=%d", arg.GameID, arg.UserID) - } - return []db.Submission{ - { - SubmissionID: 10, - GameID: 1, - UserID: 42, - Code: "<?php echo 1;", - CodeSize: 14, - Status: "success", - CreatedAt: pgtype.Timestamp{Time: now, Valid: true}, - }, - { - SubmissionID: 9, - GameID: 1, - UserID: 42, - Code: "<?php echo 'hello';", - CodeSize: 20, - Status: "wrong_answer", - CreatedAt: pgtype.Timestamp{Time: now.Add(-5 * time.Minute), Valid: true}, - }, - }, nil - }, + h := newTestHandler(&mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + Language: "php", + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + getSubmissionsByGameIDAndUserIDFunc: func(_ context.Context, arg db.GetSubmissionsByGameIDAndUserIDParams) ([]db.Submission, error) { + if arg.GameID != 1 || arg.UserID != 42 { + t.Errorf("unexpected query params: game_id=%d, user_id=%d", arg.GameID, arg.UserID) + } + return []db.Submission{ + { + SubmissionID: 10, + GameID: 1, + UserID: 42, + Code: "<?php echo 1;", + CodeSize: 14, + Status: "success", + CreatedAt: pgtype.Timestamp{Time: now, Valid: true}, + }, + { + SubmissionID: 9, + GameID: 1, + UserID: 42, + Code: "<?php echo 'hello';", + CodeSize: 20, + Status: "wrong_answer", + CreatedAt: pgtype.Timestamp{Time: now.Add(-5 * time.Minute), Valid: true}, + }, + }, nil + }, + }) user := &db.User{UserID: 42} resp, err := h.GetGamePlaySubmissions(context.Background(), GetGamePlaySubmissionsRequestObject{ GameID: 1, @@ -282,13 +296,7 @@ func TestGetGamePlaySubmissions_WithSubmissions(t *testing.T) { } func TestPostGamePlaySubmit_GameNotFound(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + h := newTestHandler(&mockQuerier{}) user := &db.User{UserID: 1} resp, err := h.PostGamePlaySubmit(context.Background(), PostGamePlaySubmitRequestObject{ GameID: 999, @@ -303,23 +311,17 @@ func TestPostGamePlaySubmit_GameNotFound(t *testing.T) { } func TestPostGamePlaySubmit_GameNotRunning(t *testing.T) { - h := Handler{ - q: &mockQuerier{ - getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { - return db.GetGameByIDRow{ - GameID: 1, - Language: "php", - StartedAt: pgtype.Timestamp{ - Valid: false, - }, - }, nil - }, + h := newTestHandlerWithHub(&mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + Language: "php", + StartedAt: pgtype.Timestamp{ + Valid: false, + }, + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{calcCodeSizeResult: 10}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + }, &mockGameHub{calcCodeSizeResult: 10}) user := &db.User{UserID: 1} resp, err := h.PostGamePlaySubmit(context.Background(), PostGamePlaySubmitRequestObject{ GameID: 1, @@ -335,121 +337,8 @@ func TestPostGamePlaySubmit_GameNotRunning(t *testing.T) { } } -func TestIsGameRunning(t *testing.T) { - now := time.Now() - tests := []struct { - name string - game db.GetGameByIDRow - want bool - }{ - { - name: "not started", - game: db.GetGameByIDRow{ - StartedAt: pgtype.Timestamp{Valid: false}, - DurationSeconds: 300, - }, - want: false, - }, - { - name: "running", - game: db.GetGameByIDRow{ - StartedAt: pgtype.Timestamp{Time: now.Add(-1 * time.Minute), Valid: true}, - DurationSeconds: 300, - }, - want: true, - }, - { - name: "finished", - game: db.GetGameByIDRow{ - StartedAt: pgtype.Timestamp{Time: now.Add(-10 * time.Minute), Valid: true}, - DurationSeconds: 300, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := isGameRunning(tt.game) - if got != tt.want { - t.Errorf("isGameRunning() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestIsGameFinished(t *testing.T) { - now := time.Now() - tests := []struct { - name string - game db.GetGameByIDRow - want bool - }{ - { - name: "not started", - game: db.GetGameByIDRow{ - StartedAt: pgtype.Timestamp{Valid: false}, - DurationSeconds: 300, - }, - want: false, - }, - { - name: "still running", - game: db.GetGameByIDRow{ - StartedAt: pgtype.Timestamp{Time: now.Add(-1 * time.Minute), Valid: true}, - DurationSeconds: 300, - }, - want: false, - }, - { - name: "finished", - game: db.GetGameByIDRow{ - StartedAt: pgtype.Timestamp{Time: now.Add(-10 * time.Minute), Valid: true}, - DurationSeconds: 300, - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := isGameFinished(tt.game) - if got != tt.want { - t.Errorf("isGameFinished() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestToNullable(t *testing.T) { - t.Run("nil value", func(t *testing.T) { - result := toNullable[string](nil) - if !result.IsNull() { - t.Error("expected null for nil input") - } - }) - t.Run("non-nil value", func(t *testing.T) { - s := "hello" - result := toNullable(&s) - if result.IsNull() { - t.Error("expected non-null for non-nil input") - } - v, err := result.Get() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if v != "hello" { - t.Errorf("expected 'hello', got %q", v) - } - }) -} - func TestGetMe(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + h := newTestHandler(&mockQuerier{}) user := &db.User{ UserID: 1, Username: "testuser", @@ -476,13 +365,7 @@ func TestGetMe(t *testing.T) { } func TestGetGame_NotFound(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + h := newTestHandler(&mockQuerier{}) user := &db.User{UserID: 1} resp, err := h.GetGame(context.Background(), GetGameRequestObject{GameID: 999}, user) if err != nil { @@ -494,21 +377,15 @@ func TestGetGame_NotFound(t *testing.T) { } func TestGetGame_NonPublicAsNonAdmin(t *testing.T) { - h := Handler{ - q: &mockQuerier{ - getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { - return db.GetGameByIDRow{ - GameID: 1, - IsPublic: false, - Language: "php", - }, nil - }, + h := newTestHandler(&mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + IsPublic: false, + Language: "php", + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + }) user := &db.User{UserID: 1, IsAdmin: false} resp, err := h.GetGame(context.Background(), GetGameRequestObject{GameID: 1}, user) if err != nil { @@ -521,34 +398,28 @@ func TestGetGame_NonPublicAsNonAdmin(t *testing.T) { func TestGetGame_PublicGameSuccess(t *testing.T) { now := time.Now() - h := Handler{ - q: &mockQuerier{ - getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { - return db.GetGameByIDRow{ - GameID: 1, - IsPublic: true, - Language: "php", - DisplayName: "Test Game", - DurationSeconds: 300, - StartedAt: pgtype.Timestamp{Time: now, Valid: true}, - GameType: "golf", - ProblemID: 10, - Title: "Test Problem", - Description: "desc", - SampleCode: "<?php", - }, nil - }, - listMainPlayersFunc: func(_ context.Context, _ []int32) ([]db.ListMainPlayersRow, error) { - return []db.ListMainPlayersRow{ - {UserID: 1, Username: "player1", DisplayName: "Player 1", IsAdmin: false}, - }, nil - }, + h := newTestHandler(&mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + IsPublic: true, + Language: "php", + DisplayName: "Test Game", + DurationSeconds: 300, + StartedAt: pgtype.Timestamp{Time: now, Valid: true}, + GameType: "golf", + ProblemID: 10, + Title: "Test Problem", + Description: "desc", + SampleCode: "<?php", + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + listMainPlayersFunc: func(_ context.Context, _ []int32) ([]db.ListMainPlayersRow, error) { + return []db.ListMainPlayersRow{ + {UserID: 1, Username: "player1", DisplayName: "Player 1", IsAdmin: false}, + }, nil + }, + }) user := &db.User{UserID: 1, IsAdmin: false} resp, err := h.GetGame(context.Background(), GetGameRequestObject{GameID: 1}, user) if err != nil { @@ -571,11 +442,11 @@ func TestGetGame_PublicGameSuccess(t *testing.T) { func TestPostLogin_AuthFailure(t *testing.T) { h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{loginErr: errors.New("invalid credentials")}, - conf: &config.Config{}, + gameSvc: game.NewService(&mockQuerier{}, &mockTxManager{}, &mockGameHub{}), + tournamentSvc: tournament.NewService(&mockQuerier{}), + auth: &mockAuthenticator{loginErr: errors.New("invalid credentials")}, + conf: &config.Config{}, + q: &mockQuerier{}, } resp, err := h.PostLogin(context.Background(), PostLoginRequestObject{ Body: &PostLoginJSONRequestBody{Username: "user", Password: "wrong"}, @@ -590,15 +461,14 @@ func TestPostLogin_AuthFailure(t *testing.T) { func TestPostLogout(t *testing.T) { h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{BasePath: "/"}, + gameSvc: game.NewService(&mockQuerier{}, &mockTxManager{}, &mockGameHub{}), + tournamentSvc: tournament.NewService(&mockQuerier{}), + auth: &mockAuthenticator{}, + conf: &config.Config{BasePath: "/"}, + q: &mockQuerier{}, } user := &db.User{UserID: 1} - // Set session ID in context - ctx := context.WithValue(context.Background(), sessionIDContextKey{}, "hashed-session") + ctx := session.SetSessionIDInContext(context.Background(), "hashed-session") resp, err := h.PostLogout(ctx, PostLogoutRequestObject{}, user) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -609,13 +479,7 @@ func TestPostLogout(t *testing.T) { } func TestGetGames_Empty(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + h := newTestHandler(&mockQuerier{}) user := &db.User{UserID: 1} resp, err := h.GetGames(context.Background(), GetGamesRequestObject{}, user) if err != nil { @@ -632,34 +496,28 @@ func TestGetGames_Empty(t *testing.T) { func TestGetGames_WithGames(t *testing.T) { now := time.Now() - h := Handler{ - q: &mockQuerier{ - listPublicGamesFunc: func(_ context.Context) ([]db.ListPublicGamesRow, error) { - return []db.ListPublicGamesRow{ - { - GameID: 1, - GameType: "golf", - IsPublic: true, - DisplayName: "Game 1", - DurationSeconds: 300, - StartedAt: pgtype.Timestamp{Time: now, Valid: true}, - ProblemID: 10, - Title: "Problem 1", - Description: "desc", - Language: "php", - SampleCode: "<?php", - }, - }, nil - }, - listMainPlayersFunc: func(_ context.Context, _ []int32) ([]db.ListMainPlayersRow, error) { - return nil, nil - }, + h := newTestHandler(&mockQuerier{ + listPublicGamesFunc: func(_ context.Context) ([]db.ListPublicGamesRow, error) { + return []db.ListPublicGamesRow{ + { + GameID: 1, + GameType: "golf", + IsPublic: true, + DisplayName: "Game 1", + DurationSeconds: 300, + StartedAt: pgtype.Timestamp{Time: now, Valid: true}, + ProblemID: 10, + Title: "Problem 1", + Description: "desc", + Language: "php", + SampleCode: "<?php", + }, + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + listMainPlayersFunc: func(_ context.Context, _ []int32) ([]db.ListMainPlayersRow, error) { + return nil, nil + }, + }) user := &db.User{UserID: 1} resp, err := h.GetGames(context.Background(), GetGamesRequestObject{}, user) if err != nil { @@ -681,13 +539,7 @@ func TestGetGames_WithGames(t *testing.T) { } func TestGetGamePlayLatestState_NoState(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + h := newTestHandler(&mockQuerier{}) user := &db.User{UserID: 1} resp, err := h.GetGamePlayLatestState(context.Background(), GetGamePlayLatestStateRequestObject{GameID: 1}, user) if err != nil { @@ -706,13 +558,7 @@ func TestGetGamePlayLatestState_NoState(t *testing.T) { } func TestPostGamePlayCode_GameNotFound(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + h := newTestHandler(&mockQuerier{}) user := &db.User{UserID: 1} resp, err := h.PostGamePlayCode(context.Background(), PostGamePlayCodeRequestObject{ GameID: 999, @@ -727,23 +573,17 @@ func TestPostGamePlayCode_GameNotFound(t *testing.T) { } func TestPostGamePlayCode_GameNotRunning(t *testing.T) { - h := Handler{ - q: &mockQuerier{ - getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { - return db.GetGameByIDRow{ - GameID: 1, - Language: "php", - StartedAt: pgtype.Timestamp{ - Valid: false, - }, - }, nil - }, + h := newTestHandler(&mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + Language: "php", + StartedAt: pgtype.Timestamp{ + Valid: false, + }, + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + }) user := &db.User{UserID: 1} resp, err := h.PostGamePlayCode(context.Background(), PostGamePlayCodeRequestObject{ GameID: 1, @@ -760,26 +600,20 @@ func TestPostGamePlayCode_GameNotRunning(t *testing.T) { func TestPostGamePlayCode_Success(t *testing.T) { now := time.Now() var updatedCode string - h := Handler{ - q: &mockQuerier{ - getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { - return db.GetGameByIDRow{ - GameID: 1, - Language: "php", - StartedAt: pgtype.Timestamp{Time: now, Valid: true}, - DurationSeconds: 600, - }, nil - }, - updateCodeFunc: func(_ context.Context, arg db.UpdateCodeParams) error { - updatedCode = arg.Code - return nil - }, + h := newTestHandler(&mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + Language: "php", + StartedAt: pgtype.Timestamp{Time: now, Valid: true}, + DurationSeconds: 600, + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + updateCodeFunc: func(_ context.Context, arg db.UpdateCodeParams) error { + updatedCode = arg.Code + return nil + }, + }) user := &db.User{UserID: 1} resp, err := h.PostGamePlayCode(context.Background(), PostGamePlayCodeRequestObject{ GameID: 1, @@ -797,13 +631,7 @@ func TestPostGamePlayCode_Success(t *testing.T) { } func TestGetGameWatchRanking_NotFound(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + h := newTestHandler(&mockQuerier{}) user := &db.User{UserID: 1} resp, err := h.GetGameWatchRanking(context.Background(), GetGameWatchRankingRequestObject{GameID: 999}, user) if err != nil { @@ -816,25 +644,19 @@ func TestGetGameWatchRanking_NotFound(t *testing.T) { func TestGetGameWatchRanking_EmptyRanking(t *testing.T) { now := time.Now() - h := Handler{ - q: &mockQuerier{ - getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { - return db.GetGameByIDRow{ - GameID: 1, - Language: "php", - StartedAt: pgtype.Timestamp{Time: now.Add(-10 * time.Minute), Valid: true}, - DurationSeconds: 300, - }, nil - }, - getRankingFunc: func(_ context.Context, _ int32) ([]db.GetRankingRow, error) { - return nil, nil - }, + h := newTestHandler(&mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + Language: "php", + StartedAt: pgtype.Timestamp{Time: now.Add(-10 * time.Minute), Valid: true}, + DurationSeconds: 300, + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + getRankingFunc: func(_ context.Context, _ int32) ([]db.GetRankingRow, error) { + return nil, nil + }, + }) user := &db.User{UserID: 1} resp, err := h.GetGameWatchRanking(context.Background(), GetGameWatchRankingRequestObject{GameID: 1}, user) if err != nil { @@ -850,13 +672,7 @@ func TestGetGameWatchRanking_EmptyRanking(t *testing.T) { } func TestGetGameWatchLatestStates_Empty(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + h := newTestHandler(&mockQuerier{}) user := &db.User{UserID: 1} resp, err := h.GetGameWatchLatestStates(context.Background(), GetGameWatchLatestStatesRequestObject{GameID: 1}, user) if err != nil { @@ -871,16 +687,16 @@ func TestGetGameWatchLatestStates_Empty(t *testing.T) { } } -func TestToNullableWith(t *testing.T) { +func TestToNullable(t *testing.T) { t.Run("nil value", func(t *testing.T) { - result := toNullableWith(nil, func(_ int) string { return "x" }) + result := toNullable[string](nil) if !result.IsNull() { t.Error("expected null for nil input") } }) t.Run("non-nil value", func(t *testing.T) { - x := 42 - result := toNullableWith(&x, func(_ int) string { return "hello" }) + s := "hello" + result := toNullable(&s) if result.IsNull() { t.Error("expected non-null for non-nil input") } @@ -894,111 +710,33 @@ func TestToNullableWith(t *testing.T) { }) } -// --- Tournament tests --- - -func TestStandardBracketSeeds(t *testing.T) { - tests := []struct { - name string - bracketSize int - expected []int - }{ - { - name: "bracket_size=2", - bracketSize: 2, - expected: []int{1, 2}, - }, - { - name: "bracket_size=4", - bracketSize: 4, - expected: []int{1, 4, 2, 3}, - }, - { - name: "bracket_size=8", - bracketSize: 8, - expected: []int{1, 8, 4, 5, 2, 7, 3, 6}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := standardBracketSeeds(tt.bracketSize) - if len(got) != len(tt.expected) { - t.Fatalf("expected length %d, got %d", len(tt.expected), len(got)) - } - for i, v := range tt.expected { - if got[i] != v { - t.Errorf("position %d: expected seed %d, got %d", i, v, got[i]) - } - } - }) - } -} - -func TestStandardBracketSeeds_Seed1And2OppositeSides(t *testing.T) { - seeds := standardBracketSeeds(8) - // Seed 1 should be in the first half, Seed 2 in the second half - seed1Pos := -1 - seed2Pos := -1 - for i, s := range seeds { - if s == 1 { - seed1Pos = i +func TestToNullableWith(t *testing.T) { + t.Run("nil value", func(t *testing.T) { + result := toNullableWith(nil, func(_ int) string { return "x" }) + if !result.IsNull() { + t.Error("expected null for nil input") } - if s == 2 { - seed2Pos = i + }) + t.Run("non-nil value", func(t *testing.T) { + x := 42 + result := toNullableWith(&x, func(_ int) string { return "hello" }) + if result.IsNull() { + t.Error("expected non-null for non-nil input") } - } - if seed1Pos >= 4 { - t.Errorf("Seed 1 should be in first half, but at position %d", seed1Pos) - } - if seed2Pos < 4 { - t.Errorf("Seed 2 should be in second half, but at position %d", seed2Pos) - } -} - -func TestStandardBracketSeeds_AllSeedsPresent(t *testing.T) { - for _, size := range []int{2, 4, 8, 16} { - seeds := standardBracketSeeds(size) - seen := make(map[int]bool) - for _, s := range seeds { - if s < 1 || s > size { - t.Errorf("bracket_size=%d: seed %d out of range", size, s) - } - if seen[s] { - t.Errorf("bracket_size=%d: duplicate seed %d", size, s) - } - seen[s] = true + v, err := result.Get() + if err != nil { + t.Fatalf("unexpected error: %v", err) } - if len(seen) != size { - t.Errorf("bracket_size=%d: expected %d unique seeds, got %d", size, size, len(seen)) + if v != "hello" { + t.Errorf("expected 'hello', got %q", v) } - } + }) } -func TestFindSeedByUserID(t *testing.T) { - entries := []TournamentEntry{ - {User: User{UserID: 10}, Seed: 1}, - {User: User{UserID: 20}, Seed: 2}, - {User: User{UserID: 30}, Seed: 3}, - } - - if got := findSeedByUserID(entries, 10); got != 1 { - t.Errorf("expected seed 1 for user 10, got %d", got) - } - if got := findSeedByUserID(entries, 20); got != 2 { - t.Errorf("expected seed 2 for user 20, got %d", got) - } - if got := findSeedByUserID(entries, 999); got != 0 { - t.Errorf("expected seed 0 for unknown user, got %d", got) - } -} +// --- Tournament tests --- func TestGetTournament_NotFound(t *testing.T) { - h := Handler{ - q: &mockQuerier{}, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + h := newTestHandler(&mockQuerier{}) user := &db.User{UserID: 1} resp, err := h.GetTournament(context.Background(), GetTournamentRequestObject{TournamentID: 999}, user) if err != nil { @@ -1010,22 +748,16 @@ func TestGetTournament_NotFound(t *testing.T) { } func TestGetTournament_Success_NoEntries(t *testing.T) { - h := Handler{ - q: &mockQuerier{ - getTournamentByIDFunc: func(_ context.Context, _ int32) (db.Tournament, error) { - return db.Tournament{ - TournamentID: 1, - DisplayName: "Test Tournament", - BracketSize: 4, - NumRounds: 2, - }, nil - }, + h := newTestHandler(&mockQuerier{ + getTournamentByIDFunc: func(_ context.Context, _ int32) (db.Tournament, error) { + return db.Tournament{ + TournamentID: 1, + DisplayName: "Test Tournament", + BracketSize: 4, + NumRounds: 2, + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + }) user := &db.User{UserID: 1} resp, err := h.GetTournament(context.Background(), GetTournamentRequestObject{TournamentID: 1}, user) if err != nil { @@ -1051,42 +783,36 @@ func TestGetTournament_Success_NoEntries(t *testing.T) { func TestGetTournament_WithEntriesAndMatches(t *testing.T) { gameID := int32(10) - h := Handler{ - q: &mockQuerier{ - getTournamentByIDFunc: func(_ context.Context, _ int32) (db.Tournament, error) { - return db.Tournament{ - TournamentID: 1, - DisplayName: "Test", - BracketSize: 4, - NumRounds: 2, - }, nil - }, - listTournamentEntriesFunc: func(_ context.Context, _ int32) ([]db.ListTournamentEntriesRow, error) { - return []db.ListTournamentEntriesRow{ - {Seed: 1, UserID: 100, Username: "alice", DisplayName: "Alice", IsAdmin: false}, - {Seed: 2, UserID: 200, Username: "bob", DisplayName: "Bob", IsAdmin: false}, - {Seed: 3, UserID: 300, Username: "carol", DisplayName: "Carol", IsAdmin: false}, - }, nil - }, - listTournamentMatchesFunc: func(_ context.Context, _ int32) ([]db.TournamentMatch, error) { - return []db.TournamentMatch{ - {TournamentMatchID: 1, TournamentID: 1, Round: 0, Position: 0, GameID: &gameID}, - {TournamentMatchID: 2, TournamentID: 1, Round: 0, Position: 1, GameID: nil}, - {TournamentMatchID: 3, TournamentID: 1, Round: 1, Position: 0, GameID: nil}, - }, nil - }, - getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { - return db.GetGameByIDRow{ - GameID: 10, - StartedAt: pgtype.Timestamp{Valid: false}, - }, nil - }, + h := newTestHandler(&mockQuerier{ + getTournamentByIDFunc: func(_ context.Context, _ int32) (db.Tournament, error) { + return db.Tournament{ + TournamentID: 1, + DisplayName: "Test", + BracketSize: 4, + NumRounds: 2, + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + listTournamentEntriesFunc: func(_ context.Context, _ int32) ([]db.ListTournamentEntriesRow, error) { + return []db.ListTournamentEntriesRow{ + {Seed: 1, UserID: 100, Username: "alice", DisplayName: "Alice", IsAdmin: false}, + {Seed: 2, UserID: 200, Username: "bob", DisplayName: "Bob", IsAdmin: false}, + {Seed: 3, UserID: 300, Username: "carol", DisplayName: "Carol", IsAdmin: false}, + }, nil + }, + listTournamentMatchesFunc: func(_ context.Context, _ int32) ([]db.TournamentMatch, error) { + return []db.TournamentMatch{ + {TournamentMatchID: 1, TournamentID: 1, Round: 0, Position: 0, GameID: &gameID}, + {TournamentMatchID: 2, TournamentID: 1, Round: 0, Position: 1, GameID: nil}, + {TournamentMatchID: 3, TournamentID: 1, Round: 1, Position: 0, GameID: nil}, + }, nil + }, + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 10, + StartedAt: pgtype.Timestamp{Valid: false}, + }, nil + }, + }) user := &db.User{UserID: 1} resp, err := h.GetTournament(context.Background(), GetTournamentRequestObject{TournamentID: 1}, user) if err != nil { @@ -1097,17 +823,14 @@ func TestGetTournament_WithEntriesAndMatches(t *testing.T) { t.Fatalf("expected 200 response, got %T", resp) } - // Check entries if len(okResp.Tournament.Entries) != 3 { t.Fatalf("expected 3 entries, got %d", len(okResp.Tournament.Entries)) } - // Check matches: bracket_size=4, num_rounds=2 → round 0: 2 matches, round 1: 1 match if len(okResp.Tournament.Matches) != 3 { t.Fatalf("expected 3 matches, got %d", len(okResp.Tournament.Matches)) } - // Round 0, Position 0: Seed 1 (Alice) vs Seed 4 (bye) m0 := okResp.Tournament.Matches[0] if m0.Round != 0 || m0.Position != 0 { t.Errorf("match 0: expected round=0, pos=0, got round=%d, pos=%d", m0.Round, m0.Position) @@ -1125,7 +848,6 @@ func TestGetTournament_WithEntriesAndMatches(t *testing.T) { t.Error("match 0: expected winner to be Alice (user_id=100)") } - // Round 0, Position 1: Seed 2 (Bob) vs Seed 3 (Carol) m1 := okResp.Tournament.Matches[1] if m1.Round != 0 || m1.Position != 1 { t.Errorf("match 1: expected round=0, pos=1, got round=%d, pos=%d", m1.Round, m1.Position) @@ -1142,38 +864,30 @@ func TestGetTournament_WithEntriesAndMatches(t *testing.T) { } func TestGetTournament_ByeAutoWinner(t *testing.T) { - // 3 players in bracket_size=4: seed 4 is empty → round 0, pos 0 is a bye - // The bye winner should propagate to round 1 - h := Handler{ - q: &mockQuerier{ - getTournamentByIDFunc: func(_ context.Context, _ int32) (db.Tournament, error) { - return db.Tournament{ - TournamentID: 1, - DisplayName: "Bye Test", - BracketSize: 4, - NumRounds: 2, - }, nil - }, - listTournamentEntriesFunc: func(_ context.Context, _ int32) ([]db.ListTournamentEntriesRow, error) { - return []db.ListTournamentEntriesRow{ - {Seed: 1, UserID: 100, Username: "alice", DisplayName: "Alice"}, - {Seed: 2, UserID: 200, Username: "bob", DisplayName: "Bob"}, - {Seed: 3, UserID: 300, Username: "carol", DisplayName: "Carol"}, - }, nil - }, - listTournamentMatchesFunc: func(_ context.Context, _ int32) ([]db.TournamentMatch, error) { - return []db.TournamentMatch{ - {TournamentMatchID: 1, Round: 0, Position: 0}, - {TournamentMatchID: 2, Round: 0, Position: 1}, - {TournamentMatchID: 3, Round: 1, Position: 0}, - }, nil - }, + h := newTestHandler(&mockQuerier{ + getTournamentByIDFunc: func(_ context.Context, _ int32) (db.Tournament, error) { + return db.Tournament{ + TournamentID: 1, + DisplayName: "Bye Test", + BracketSize: 4, + NumRounds: 2, + }, nil }, - txm: &mockTxManager{}, - hub: &mockGameHub{}, - auth: &mockAuthenticator{}, - conf: &config.Config{}, - } + listTournamentEntriesFunc: func(_ context.Context, _ int32) ([]db.ListTournamentEntriesRow, error) { + return []db.ListTournamentEntriesRow{ + {Seed: 1, UserID: 100, Username: "alice", DisplayName: "Alice"}, + {Seed: 2, UserID: 200, Username: "bob", DisplayName: "Bob"}, + {Seed: 3, UserID: 300, Username: "carol", DisplayName: "Carol"}, + }, nil + }, + listTournamentMatchesFunc: func(_ context.Context, _ int32) ([]db.TournamentMatch, error) { + return []db.TournamentMatch{ + {TournamentMatchID: 1, Round: 0, Position: 0}, + {TournamentMatchID: 2, Round: 0, Position: 1}, + {TournamentMatchID: 3, Round: 1, Position: 0}, + }, nil + }, + }) user := &db.User{UserID: 1} resp, err := h.GetTournament(context.Background(), GetTournamentRequestObject{TournamentID: 1}, user) if err != nil { @@ -1181,7 +895,6 @@ func TestGetTournament_ByeAutoWinner(t *testing.T) { } okResp := resp.(GetTournament200JSONResponse) - // Round 1, Position 0 (final): player1 should be Alice (bye winner from round 0 pos 0) final := okResp.Tournament.Matches[2] if final.Round != 1 || final.Position != 0 { t.Fatalf("expected final at round=1, pos=0") |
