diff options
Diffstat (limited to 'backend/api')
| -rw-r--r-- | backend/api/auth_middleware.go | 34 | ||||
| -rw-r--r-- | backend/api/auth_middleware_test.go | 63 | ||||
| -rw-r--r-- | backend/api/convert.go | 160 | ||||
| -rw-r--r-- | backend/api/handler.go | 636 | ||||
| -rw-r--r-- | backend/api/handler_test.go | 829 | ||||
| -rw-r--r-- | backend/api/handler_wrapper.go | 37 |
6 files changed, 523 insertions, 1236 deletions
diff --git a/backend/api/auth_middleware.go b/backend/api/auth_middleware.go index f2a3987..a588185 100644 --- a/backend/api/auth_middleware.go +++ b/backend/api/auth_middleware.go @@ -1,17 +1,13 @@ package api import ( - "context" - "github.com/labstack/echo/v4" "albatross-2026-backend/auth" "albatross-2026-backend/db" + "albatross-2026-backend/session" ) -type sessionIDContextKey struct{} -type userContextKey struct{} - func SessionCookieMiddleware(q db.Querier) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { @@ -25,31 +21,14 @@ func SessionCookieMiddleware(q db.Querier) echo.MiddlewareFunc { return next(c) } ctx := c.Request().Context() - ctx = context.WithValue(ctx, sessionIDContextKey{}, hashedID) - ctx = context.WithValue(ctx, userContextKey{}, &user) + ctx = session.SetSessionIDInContext(ctx, hashedID) + ctx = session.SetUserInContext(ctx, &user) c.SetRequest(c.Request().WithContext(ctx)) return next(c) } } } -func GetSessionIDFromContext(ctx context.Context) (string, bool) { - sessionID, ok := ctx.Value(sessionIDContextKey{}).(string) - return sessionID, ok -} - -func GetUserFromContext(ctx context.Context) (*db.User, bool) { - user, ok := ctx.Value(userContextKey{}).(*db.User) - return user, ok -} - -// SetUserInContext sets a user in the context. Intended for testing. -func SetUserInContext(ctx context.Context, user *db.User) context.Context { - return context.WithValue(ctx, userContextKey{}, user) -} - -type clientIPContextKey struct{} - // ClientIPMiddleware extracts the client IP from echo.Context.RealIP() // and stores it in the request's context.Context so that handlers // receiving only context.Context (via generated code) can access it. @@ -57,14 +36,9 @@ func ClientIPMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { ip := c.RealIP() - ctx := context.WithValue(c.Request().Context(), clientIPContextKey{}, ip) + ctx := session.SetClientIPInContext(c.Request().Context(), ip) c.SetRequest(c.Request().WithContext(ctx)) return next(c) } } } - -func GetClientIPFromContext(ctx context.Context) string { - ip, _ := ctx.Value(clientIPContextKey{}).(string) - return ip -} diff --git a/backend/api/auth_middleware_test.go b/backend/api/auth_middleware_test.go index d84eef7..9fc20af 100644 --- a/backend/api/auth_middleware_test.go +++ b/backend/api/auth_middleware_test.go @@ -9,62 +9,9 @@ import ( "github.com/labstack/echo/v4" "albatross-2026-backend/db" + "albatross-2026-backend/session" ) -func TestGetSessionIDFromContext_NotSet(t *testing.T) { - ctx := context.Background() - _, ok := GetSessionIDFromContext(ctx) - if ok { - t.Error("expected ok=false when session ID is not set") - } -} - -func TestGetSessionIDFromContext_Set(t *testing.T) { - ctx := context.WithValue(context.Background(), sessionIDContextKey{}, "abc123") - id, ok := GetSessionIDFromContext(ctx) - if !ok { - t.Fatal("expected ok=true when session ID is set") - } - if id != "abc123" { - t.Errorf("expected session ID 'abc123', got %q", id) - } -} - -func TestGetUserFromContext_NotSet(t *testing.T) { - ctx := context.Background() - _, ok := GetUserFromContext(ctx) - if ok { - t.Error("expected ok=false when user is not set") - } -} - -func TestGetUserFromContext_Set(t *testing.T) { - user := &db.User{UserID: 42, Username: "testuser"} - ctx := context.WithValue(context.Background(), userContextKey{}, user) - u, ok := GetUserFromContext(ctx) - if !ok { - t.Fatal("expected ok=true when user is set") - } - if u.UserID != 42 { - t.Errorf("expected user ID 42, got %d", u.UserID) - } - if u.Username != "testuser" { - t.Errorf("expected username 'testuser', got %q", u.Username) - } -} - -func TestSetUserInContext(t *testing.T) { - user := &db.User{UserID: 7, Username: "admin"} - ctx := SetUserInContext(context.Background(), user) - u, ok := GetUserFromContext(ctx) - if !ok { - t.Fatal("expected ok=true after SetUserInContext") - } - if u.UserID != 7 { - t.Errorf("expected user ID 7, got %d", u.UserID) - } -} - // mockSessionQuerier implements the subset of db.Querier needed by SessionCookieMiddleware. type mockSessionQuerier struct { db.Querier @@ -89,7 +36,7 @@ func TestSessionCookieMiddleware_NoCookie(t *testing.T) { handler := mw(func(c echo.Context) error { called = true // User should not be set - _, ok := GetUserFromContext(c.Request().Context()) + _, ok := session.GetUserFromContext(c.Request().Context()) if ok { t.Error("expected no user in context when no cookie is present") } @@ -122,14 +69,14 @@ func TestSessionCookieMiddleware_ValidSession(t *testing.T) { var called bool handler := mw(func(c echo.Context) error { called = true - user, ok := GetUserFromContext(c.Request().Context()) + user, ok := session.GetUserFromContext(c.Request().Context()) if !ok { t.Fatal("expected user in context") } if user.UserID != 10 { t.Errorf("expected user ID 10, got %d", user.UserID) } - sid, ok := GetSessionIDFromContext(c.Request().Context()) + sid, ok := session.GetSessionIDFromContext(c.Request().Context()) if !ok { t.Fatal("expected session ID in context") } @@ -164,7 +111,7 @@ func TestSessionCookieMiddleware_InvalidSession(t *testing.T) { var called bool handler := mw(func(c echo.Context) error { called = true - _, ok := GetUserFromContext(c.Request().Context()) + _, ok := session.GetUserFromContext(c.Request().Context()) if ok { t.Error("expected no user in context for invalid session") } diff --git a/backend/api/convert.go b/backend/api/convert.go new file mode 100644 index 0000000..f05f3bf --- /dev/null +++ b/backend/api/convert.go @@ -0,0 +1,160 @@ +package api + +import ( + "github.com/oapi-codegen/nullable" + + "albatross-2026-backend/game" + "albatross-2026-backend/tournament" +) + +func toAPIUser(p game.Player) User { + return User{ + UserID: p.UserID, + Username: p.Username, + DisplayName: p.DisplayName, + IconPath: p.IconPath, + IsAdmin: p.IsAdmin, + Label: toNullable(p.Label), + } +} + +func toAPIGame(g game.GameDetail) Game { + var startedAt *int64 + if g.StartedAt != nil { + ts := g.StartedAt.Unix() + startedAt = &ts + } + mainPlayers := make([]User, len(g.MainPlayers)) + for i, p := range g.MainPlayers { + mainPlayers[i] = toAPIUser(p) + } + return Game{ + GameID: g.GameID, + GameType: GameType(g.GameType), + IsPublic: g.IsPublic, + DisplayName: g.DisplayName, + DurationSeconds: g.DurationSeconds, + StartedAt: startedAt, + Problem: Problem{ + ProblemID: g.Problem.ProblemID, + Title: g.Problem.Title, + Description: g.Problem.Description, + Language: ProblemLanguage(g.Problem.Language), + SampleCode: g.Problem.SampleCode, + }, + MainPlayers: mainPlayers, + } +} + +func toAPILatestState(s game.LatestState) LatestGameState { + var score nullable.Nullable[int] + if s.Score != nil { + score = nullable.NewNullableWithValue(*s.Score) + } else { + score = nullable.NewNullNullable[int]() + } + var submittedAt nullable.Nullable[int64] + if s.BestScoreSubmittedAt != nil { + submittedAt = nullable.NewNullableWithValue(*s.BestScoreSubmittedAt) + } else { + submittedAt = nullable.NewNullNullable[int64]() + } + return LatestGameState{ + Code: s.Code, + Score: score, + BestScoreSubmittedAt: submittedAt, + Status: ExecutionStatus(s.Status), + } +} + +func toAPIRankingEntry(r game.RankingEntry) RankingEntry { + var code nullable.Nullable[string] + if r.Code != nil { + code = nullable.NewNullableWithValue(*r.Code) + } else { + code = nullable.NewNullNullable[string]() + } + return RankingEntry{ + Player: toAPIUser(r.Player), + Score: r.Score, + SubmittedAt: r.SubmittedAt, + Code: code, + } +} + +func toAPISubmission(s game.SubmissionDetail) Submission { + return Submission{ + SubmissionID: s.SubmissionID, + GameID: s.GameID, + Code: s.Code, + CodeSize: s.CodeSize, + Status: ExecutionStatus(s.Status), + CreatedAt: s.CreatedAt, + } +} + +func toAPITournamentUser(p tournament.Player) User { + return User{ + UserID: p.UserID, + Username: p.Username, + DisplayName: p.DisplayName, + IconPath: p.IconPath, + IsAdmin: p.IsAdmin, + Label: toNullable(p.Label), + } +} + +func toAPITournamentPlayerPtr(p *tournament.Player) *User { + if p == nil { + return nil + } + u := toAPITournamentUser(*p) + return &u +} + +func toAPITournament(t tournament.TournamentBracket) Tournament { + entries := make([]TournamentEntry, len(t.Entries)) + for i, e := range t.Entries { + entries[i] = TournamentEntry{ + User: toAPITournamentUser(e.User), + Seed: e.Seed, + } + } + matches := make([]TournamentMatch, len(t.Matches)) + for i, m := range t.Matches { + matches[i] = TournamentMatch{ + TournamentMatchID: m.TournamentMatchID, + Round: m.Round, + Position: m.Position, + GameID: m.GameID, + Player1: toAPITournamentPlayerPtr(m.Player1), + Player2: toAPITournamentPlayerPtr(m.Player2), + Player1Score: m.Player1Score, + Player2Score: m.Player2Score, + WinnerUserID: m.WinnerUserID, + IsBye: m.IsBye, + } + } + return Tournament{ + TournamentID: t.TournamentID, + DisplayName: t.DisplayName, + BracketSize: t.BracketSize, + NumRounds: t.NumRounds, + Entries: entries, + Matches: matches, + } +} + +func toNullable[T any](p *T) nullable.Nullable[T] { + if p == nil { + return nullable.NewNullNullable[T]() + } + return nullable.NewNullableWithValue(*p) +} + +func toNullableWith[T, U any](p *T, f func(T) U) nullable.Nullable[U] { + if p == nil { + return nullable.NewNullNullable[U]() + } + return nullable.NewNullableWithValue(f(*p)) +} diff --git a/backend/api/handler.go b/backend/api/handler.go index dcebfa1..4d729f1 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -9,14 +9,15 @@ import ( "strconv" "time" - "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" - "github.com/oapi-codegen/nullable" "albatross-2026-backend/auth" "albatross-2026-backend/config" "albatross-2026-backend/db" + "albatross-2026-backend/game" + "albatross-2026-backend/session" + "albatross-2026-backend/tournament" ) type AuthenticatorInterface interface { @@ -24,16 +25,11 @@ type AuthenticatorInterface interface { } type Handler struct { - q db.Querier - txm db.TxManager - hub GameHubInterface - auth AuthenticatorInterface - conf *config.Config -} - -type GameHubInterface interface { - CalcCodeSize(code string, language string) int - EnqueueTestTasks(ctx context.Context, submissionID, gameID, userID int, language, code string) error + gameSvc *game.Service + tournamentSvc *tournament.Service + auth AuthenticatorInterface + conf *config.Config + q db.Querier // for session management (login/logout) } type postLoginCookieResponse struct { @@ -51,7 +47,7 @@ func (r postLoginCookieResponse) VisitPostLoginResponse(w http.ResponseWriter) e func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) { username := request.Body.Username password := request.Body.Password - ip := GetClientIPFromContext(ctx) + ip := session.GetClientIPFromContext(ctx) userID, err := h.auth.Login(ctx, username, password) if err != nil { @@ -138,7 +134,7 @@ func (r postLogoutCookieResponse) VisitPostLogoutResponse(w http.ResponseWriter) } func (h *Handler) PostLogout(ctx context.Context, _ PostLogoutRequestObject, _ *db.User) (PostLogoutResponseObject, error) { - if sessionID, ok := GetSessionIDFromContext(ctx); ok { + if sessionID, ok := session.GetSessionIDFromContext(ctx); ok { _ = h.q.DeleteSession(ctx, sessionID) } return postLogoutCookieResponse{ @@ -155,631 +151,125 @@ func (h *Handler) PostLogout(ctx context.Context, _ PostLogoutRequestObject, _ * } func (h *Handler) GetGames(ctx context.Context, _ GetGamesRequestObject, _ *db.User) (GetGamesResponseObject, error) { - gameRows, err := h.q.ListPublicGames(ctx) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - games := make([]Game, len(gameRows)) - gameIDs := make([]int32, len(gameRows)) - gameID2Index := make(map[int32]int, len(gameRows)) - for i, row := range gameRows { - var startedAt *int64 - if row.StartedAt.Valid { - startedAtTimestamp := row.StartedAt.Time.Unix() - startedAt = &startedAtTimestamp - } - games[i] = Game{ - GameID: int(row.GameID), - GameType: GameType(row.GameType), - IsPublic: row.IsPublic, - DisplayName: row.DisplayName, - DurationSeconds: int(row.DurationSeconds), - StartedAt: startedAt, - Problem: Problem{ - ProblemID: int(row.ProblemID), - Title: row.Title, - Description: row.Description, - Language: ProblemLanguage(row.Language), - SampleCode: row.SampleCode, - }, - } - gameIDs[i] = row.GameID - gameID2Index[row.GameID] = i - } - mainPlayerRows, err := h.q.ListMainPlayers(ctx, gameIDs) + games, err := h.gameSvc.ListPublicGames(ctx) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - for _, row := range mainPlayerRows { - idx := gameID2Index[row.GameID] - game := &games[idx] - game.MainPlayers = append(game.MainPlayers, User{ - UserID: int(row.UserID), - Username: row.Username, - DisplayName: row.DisplayName, - IconPath: row.IconPath, - IsAdmin: row.IsAdmin, - Label: toNullable(row.Label), - }) + apiGames := make([]Game, len(games)) + for i, g := range games { + apiGames[i] = toAPIGame(g) } - return GetGames200JSONResponse{ - Games: games, - }, nil + return GetGames200JSONResponse{Games: apiGames}, nil } func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, user *db.User) (GetGameResponseObject, error) { - gameID := request.GameID - row, err := h.q.GetGameByID(ctx, int32(gameID)) + isAdmin := user != nil && user.IsAdmin + g, err := h.gameSvc.GetGameByID(ctx, request.GameID, isAdmin) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return GetGame404JSONResponse{ - Message: "Game not found", - }, nil + if errors.Is(err, game.ErrNotFound) { + return GetGame404JSONResponse{Message: "Game not found"}, nil } return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - if !row.IsPublic && (user == nil || !user.IsAdmin) { - return GetGame404JSONResponse{ - Message: "Game not found", - }, nil - } - var startedAt *int64 - if row.StartedAt.Valid { - startedAtTimestamp := row.StartedAt.Time.Unix() - startedAt = &startedAtTimestamp - } - mainPlayerRows, err := h.q.ListMainPlayers(ctx, []int32{int32(gameID)}) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - mainPlayers := make([]User, len(mainPlayerRows)) - for i, playerRow := range mainPlayerRows { - mainPlayers[i] = User{ - UserID: int(playerRow.UserID), - Username: playerRow.Username, - DisplayName: playerRow.DisplayName, - IconPath: playerRow.IconPath, - IsAdmin: playerRow.IsAdmin, - Label: toNullable(playerRow.Label), - } - } - game := Game{ - GameID: int(row.GameID), - GameType: GameType(row.GameType), - IsPublic: row.IsPublic, - DisplayName: row.DisplayName, - DurationSeconds: int(row.DurationSeconds), - StartedAt: startedAt, - Problem: Problem{ - ProblemID: int(row.ProblemID), - Title: row.Title, - Description: row.Description, - Language: ProblemLanguage(row.Language), - SampleCode: row.SampleCode, - }, - MainPlayers: mainPlayers, - } - return GetGame200JSONResponse{ - Game: game, - }, nil + return GetGame200JSONResponse{Game: toAPIGame(g)}, nil } func (h *Handler) GetGamePlayLatestState(ctx context.Context, request GetGamePlayLatestStateRequestObject, user *db.User) (GetGamePlayLatestStateResponseObject, error) { - gameID := request.GameID - row, err := h.q.GetLatestState(ctx, db.GetLatestStateParams{ - GameID: int32(gameID), - UserID: user.UserID, - }) + state, err := h.gameSvc.GetLatestState(ctx, request.GameID, user.UserID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return GetGamePlayLatestState200JSONResponse{ - State: LatestGameState{ - Code: "", - Score: nullable.NewNullNullable[int](), - BestScoreSubmittedAt: nullable.NewNullNullable[int64](), - Status: None, - }, - }, nil - } return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - return GetGamePlayLatestState200JSONResponse{ - State: LatestGameState{ - Code: row.Code, - Score: toNullableWith(row.CodeSize, func(x int32) int { return int(x) }), - BestScoreSubmittedAt: nullable.NewNullableWithValue(row.CreatedAt.Time.Unix()), - Status: ExecutionStatus(row.Status), - }, - }, nil + return GetGamePlayLatestState200JSONResponse{State: toAPILatestState(state)}, nil } func (h *Handler) GetGameWatchLatestStates(ctx context.Context, request GetGameWatchLatestStatesRequestObject, user *db.User) (GetGameWatchLatestStatesResponseObject, error) { - gameID := request.GameID - rows, err := h.q.GetLatestStatesOfMainPlayers(ctx, int32(gameID)) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + var userID *int32 + var isAdmin bool + if user != nil { + userID = &user.UserID + isAdmin = user.IsAdmin } - states := make(map[string]LatestGameState, len(rows)) - for _, row := range rows { - var code string - if row.Code != nil { - code = *row.Code - } - var status ExecutionStatus - if row.Status != nil { - status = ExecutionStatus(*row.Status) - } else { - status = None - } - states[strconv.Itoa(int(row.UserID))] = LatestGameState{ - Code: code, - Score: toNullableWith(row.CodeSize, func(x int32) int { return int(x) }), - BestScoreSubmittedAt: nullable.NewNullableWithValue(row.CreatedAt.Time.Unix()), - Status: status, - } - - if user != nil && row.UserID == user.UserID && !user.IsAdmin { + stateMap, err := h.gameSvc.GetWatchLatestStates(ctx, request.GameID, userID, isAdmin) + if err != nil { + if errors.Is(err, game.ErrForbidden) { return GetGameWatchLatestStates403JSONResponse{ Message: "You are one of the main players of this game", }, nil } + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - return GetGameWatchLatestStates200JSONResponse{ - States: states, - }, nil + states := make(map[string]LatestGameState, len(stateMap)) + for uid, s := range stateMap { + states[strconv.Itoa(uid)] = toAPILatestState(s) + } + return GetGameWatchLatestStates200JSONResponse{States: states}, nil } func (h *Handler) GetGameWatchRanking(ctx context.Context, request GetGameWatchRankingRequestObject, _ *db.User) (GetGameWatchRankingResponseObject, error) { - gameID := request.GameID - - gameRow, err := h.q.GetGameByID(ctx, int32(gameID)) + ranking, _, err := h.gameSvc.GetRanking(ctx, request.GameID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return GetGameWatchRanking404JSONResponse{ - Message: "Game not found", - }, nil + if errors.Is(err, game.ErrNotFound) { + return GetGameWatchRanking404JSONResponse{Message: "Game not found"}, nil } return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - gameFinished := isGameFinished(gameRow) - - rows, err := h.q.GetRanking(ctx, int32(gameID)) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return GetGameWatchRanking200JSONResponse{}, nil - } - } - ranking := make([]RankingEntry, len(rows)) - for i, row := range rows { - var code nullable.Nullable[string] - if gameFinished { - code = nullable.NewNullableWithValue(row.Submission.Code) - } else { - code = nullable.NewNullNullable[string]() - } - ranking[i] = RankingEntry{ - Player: User{ - UserID: int(row.User.UserID), - Username: row.User.Username, - DisplayName: row.User.DisplayName, - IconPath: row.User.IconPath, - IsAdmin: row.User.IsAdmin, - Label: toNullable(row.User.Label), - }, - Score: int(row.Submission.CodeSize), - SubmittedAt: row.Submission.CreatedAt.Time.Unix(), - Code: code, - } + apiRanking := make([]RankingEntry, len(ranking)) + for i, r := range ranking { + apiRanking[i] = toAPIRankingEntry(r) } - return GetGameWatchRanking200JSONResponse{ - Ranking: ranking, - }, nil + return GetGameWatchRanking200JSONResponse{Ranking: apiRanking}, nil } func (h *Handler) GetGamePlaySubmissions(ctx context.Context, request GetGamePlaySubmissionsRequestObject, user *db.User) (GetGamePlaySubmissionsResponseObject, error) { - gameID := request.GameID - - _, err := h.q.GetGameByID(ctx, int32(gameID)) + submissions, err := h.gameSvc.GetSubmissions(ctx, request.GameID, user.UserID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return GetGamePlaySubmissions404JSONResponse{ - Message: "Game not found", - }, nil + if errors.Is(err, game.ErrNotFound) { + return GetGamePlaySubmissions404JSONResponse{Message: "Game not found"}, nil } return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - - rows, err := h.q.GetSubmissionsByGameIDAndUserID(ctx, db.GetSubmissionsByGameIDAndUserIDParams{ - GameID: int32(gameID), - UserID: user.UserID, - }) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - submissions := make([]Submission, len(rows)) - for i, row := range rows { - submissions[i] = Submission{ - SubmissionID: int(row.SubmissionID), - GameID: int(row.GameID), - Code: row.Code, - CodeSize: int(row.CodeSize), - Status: ExecutionStatus(row.Status), - CreatedAt: row.CreatedAt.Time.Unix(), - } + apiSubmissions := make([]Submission, len(submissions)) + for i, s := range submissions { + apiSubmissions[i] = toAPISubmission(s) } - - return GetGamePlaySubmissions200JSONResponse{ - Submissions: submissions, - }, nil + return GetGamePlaySubmissions200JSONResponse{Submissions: apiSubmissions}, nil } func (h *Handler) PostGamePlayCode(ctx context.Context, request PostGamePlayCodeRequestObject, user *db.User) (PostGamePlayCodeResponseObject, error) { - gameID := request.GameID - - gameRow, err := h.q.GetGameByID(ctx, int32(gameID)) + err := h.gameSvc.SaveCode(ctx, request.GameID, user.UserID, request.Body.Code) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return PostGamePlayCode404JSONResponse{ - Message: "Game not found", - }, nil + if errors.Is(err, game.ErrNotFound) { + return PostGamePlayCode404JSONResponse{Message: "Game not found"}, nil + } + if errors.Is(err, game.ErrGameNotRunning) { + return PostGamePlayCode403JSONResponse{Message: "Game is not running"}, nil } - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - if !isGameRunning(gameRow) { - return PostGamePlayCode403JSONResponse{ - Message: "Game is not running", - }, nil - } - - err = h.q.UpdateCode(ctx, db.UpdateCodeParams{ - GameID: int32(gameID), - UserID: user.UserID, - Code: request.Body.Code, - Status: "none", - }) - if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return PostGamePlayCode200Response{}, nil } func (h *Handler) PostGamePlaySubmit(ctx context.Context, request PostGamePlaySubmitRequestObject, user *db.User) (PostGamePlaySubmitResponseObject, error) { - gameID := request.GameID - code := request.Body.Code - - gameRow, err := h.q.GetGameByID(ctx, int32(gameID)) + err := h.gameSvc.SubmitCode(ctx, request.GameID, user.UserID, request.Body.Code) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { + if errors.Is(err, game.ErrNotFound) { return PostGamePlaySubmit404JSONResponse{}, nil } - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - language := gameRow.Language - codeSize := h.hub.CalcCodeSize(code, language) - - if !isGameRunning(gameRow) { - return PostGamePlaySubmit403JSONResponse{ - Message: "Game is not running", - }, nil - } - - var submissionID int32 - err = h.txm.RunInTx(ctx, func(qtx db.Querier) error { - if err := qtx.UpdateCodeAndStatus(ctx, db.UpdateCodeAndStatusParams{ - GameID: int32(gameID), - UserID: user.UserID, - Code: code, - Status: "running", - }); err != nil { - return err + if errors.Is(err, game.ErrGameNotRunning) { + return PostGamePlaySubmit403JSONResponse{Message: "Game is not running"}, nil } - var err error - submissionID, err = qtx.CreateSubmission(ctx, db.CreateSubmissionParams{ - GameID: int32(gameID), - UserID: user.UserID, - Code: code, - CodeSize: int32(codeSize), - }) - return err - }) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - err = h.hub.EnqueueTestTasks(ctx, int(submissionID), gameID, int(user.UserID), language, code) - if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return PostGamePlaySubmit200Response{}, nil } func (h *Handler) GetTournament(ctx context.Context, request GetTournamentRequestObject, _ *db.User) (GetTournamentResponseObject, error) { - tournamentID := int32(request.TournamentID) - - tournament, err := h.q.GetTournamentByID(ctx, tournamentID) + t, err := h.tournamentSvc.GetTournament(ctx, request.TournamentID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { + if errors.Is(err, game.ErrNotFound) { return GetTournament404JSONResponse{Message: "Tournament not found"}, nil } return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - - entryRows, err := h.q.ListTournamentEntries(ctx, tournamentID) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - seedToUser := make(map[int]User) - entries := make([]TournamentEntry, len(entryRows)) - for i, e := range entryRows { - u := User{ - UserID: int(e.UserID), - Username: e.Username, - DisplayName: e.DisplayName, - IconPath: e.IconPath, - IsAdmin: e.IsAdmin, - Label: toNullable(e.Label), - } - seedToUser[int(e.Seed)] = u - entries[i] = TournamentEntry{ - User: u, - Seed: int(e.Seed), - } - } - - matchRows, err := h.q.ListTournamentMatches(ctx, tournamentID) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - bracketSize := int(tournament.BracketSize) - numRounds := int(tournament.NumRounds) - bracketSeeds := standardBracketSeeds(bracketSize) - - // Index matches by (round, position) - type matchKey struct{ round, position int } - matchByKey := make(map[matchKey]db.TournamentMatch) - for _, m := range matchRows { - matchByKey[matchKey{int(m.Round), int(m.Position)}] = m - } - - // Collect game IDs for batch fetching - gameIDs := make(map[int32]bool) - for _, m := range matchRows { - if m.GameID != nil { - gameIDs[*m.GameID] = true - } - } - - // Fetch rankings for all games that have started - type rankingResult struct { - scores map[int]int // userID -> score - winnerID int - } - gameRankings := make(map[int32]*rankingResult) - for gid := range gameIDs { - gameRow, err := h.q.GetGameByID(ctx, gid) - if err != nil { - continue - } - if !gameRow.StartedAt.Valid { - continue - } - rankingRows, err := h.q.GetRanking(ctx, gid) - if err != nil || len(rankingRows) == 0 { - continue - } - rr := &rankingResult{scores: make(map[int]int)} - for i, r := range rankingRows { - rr.scores[int(r.User.UserID)] = int(r.Submission.CodeSize) - if i == 0 { - rr.winnerID = int(r.User.UserID) - } - } - gameRankings[gid] = rr - } - - // Build match results bottom-up - type matchResult struct { - player1 *User - player2 *User - p1Score *int - p2Score *int - winnerUID *int - isBye bool - } - resultByKey := make(map[matchKey]*matchResult) - - for round := range numRounds { - numPositions := bracketSize / (1 << (round + 1)) - for pos := range numPositions { - m, exists := matchByKey[matchKey{round, pos}] - mr := &matchResult{} - - if round == 0 { - // First round: resolve players from bracket seeds - slot1 := pos * 2 - slot2 := pos*2 + 1 - seed1 := bracketSeeds[slot1] - seed2 := bracketSeeds[slot2] - - if u, ok := seedToUser[seed1]; ok { - mr.player1 = &u - } - if u, ok := seedToUser[seed2]; ok { - mr.player2 = &u - } - } else { - // Later rounds: resolve from child match winners - child1 := resultByKey[matchKey{round - 1, pos * 2}] - child2 := resultByKey[matchKey{round - 1, pos*2 + 1}] - - if child1 != nil && child1.winnerUID != nil { - if u, ok := seedToUser[findSeedByUserID(entries, *child1.winnerUID)]; ok { - mr.player1 = &u - } - } - if child2 != nil && child2.winnerUID != nil { - if u, ok := seedToUser[findSeedByUserID(entries, *child2.winnerUID)]; ok { - mr.player2 = &u - } - } - } - - // Check for bye - if mr.player1 == nil && mr.player2 != nil { - mr.isBye = true - uid := mr.player2.UserID - mr.winnerUID = &uid - } else if mr.player1 != nil && mr.player2 == nil { - mr.isBye = true - uid := mr.player1.UserID - mr.winnerUID = &uid - } - - // Resolve scores from game - if exists && m.GameID != nil && !mr.isBye { - if rr, ok := gameRankings[*m.GameID]; ok { - if mr.player1 != nil { - if s, ok := rr.scores[mr.player1.UserID]; ok { - score := s - mr.p1Score = &score - } - } - if mr.player2 != nil { - if s, ok := rr.scores[mr.player2.UserID]; ok { - score := s - mr.p2Score = &score - } - } - // Winner is the one with the best (lowest) score in the ranking - if mr.player1 != nil && mr.player2 != nil { - if rr.winnerID == mr.player1.UserID || rr.winnerID == mr.player2.UserID { - w := rr.winnerID - mr.winnerUID = &w - } else { - // Both players have scores; pick the one with lower score - if mr.p1Score != nil && mr.p2Score != nil { - if *mr.p1Score <= *mr.p2Score { - w := mr.player1.UserID - mr.winnerUID = &w - } else { - w := mr.player2.UserID - mr.winnerUID = &w - } - } - } - } - } - } - - resultByKey[matchKey{round, pos}] = mr - } - } - - // Build API response matches - apiMatches := make([]TournamentMatch, 0, len(matchRows)) - for round := 0; round < numRounds; round++ { - numPositions := bracketSize / (1 << (round + 1)) - for pos := 0; pos < numPositions; pos++ { - m, exists := matchByKey[matchKey{round, pos}] - mr := resultByKey[matchKey{round, pos}] - - matchID := 0 - var gameID *int - if exists { - matchID = int(m.TournamentMatchID) - if m.GameID != nil { - gid := int(*m.GameID) - gameID = &gid - } - } - - apiMatches = append(apiMatches, TournamentMatch{ - TournamentMatchID: matchID, - Round: round, - Position: pos, - GameID: gameID, - Player1: mr.player1, - Player2: mr.player2, - Player1Score: mr.p1Score, - Player2Score: mr.p2Score, - WinnerUserID: mr.winnerUID, - IsBye: mr.isBye, - }) - } - } - - return GetTournament200JSONResponse{ - Tournament: Tournament{ - TournamentID: int(tournament.TournamentID), - DisplayName: tournament.DisplayName, - BracketSize: bracketSize, - NumRounds: numRounds, - Entries: entries, - Matches: apiMatches, - }, - }, nil -} - -func findSeedByUserID(entries []TournamentEntry, userID int) int { - for _, e := range entries { - if e.User.UserID == userID { - return e.Seed - } - } - return 0 -} - -// standardBracketSeeds returns the seed assignments for each slot in a standard -// single-elimination bracket. For bracket_size=8: -// Position: [0]=1, [1]=8, [2]=5, [3]=4, [4]=3, [5]=6, [6]=7, [7]=2 -// This ensures Seed 1 vs Seed 2 are on opposite sides, and higher seeds face lower seeds. -func standardBracketSeeds(bracketSize int) []int { - seeds := make([]int, bracketSize) - seeds[0] = 1 - // Build the bracket by repeatedly splitting - for size := 2; size <= bracketSize; size *= 2 { - // For each pair in the current level, the new opponent for seed[i] - // is (size + 1 - seed[i]) - temp := make([]int, size) - for i := 0; i < size/2; i++ { - temp[i*2] = seeds[i] - temp[i*2+1] = size + 1 - seeds[i] - } - copy(seeds, temp) - } - return seeds -} - -func isGameRunning(game db.GetGameByIDRow) bool { - if !game.StartedAt.Valid { - return false - } - endTime := game.StartedAt.Time.Add(time.Duration(game.DurationSeconds) * time.Second) - return time.Now().Before(endTime) -} - -func isGameFinished(game db.GetGameByIDRow) bool { - if !game.StartedAt.Valid { - return false - } - endTime := game.StartedAt.Time.Add(time.Duration(game.DurationSeconds) * time.Second) - return !time.Now().Before(endTime) -} - -func toNullable[T any](p *T) nullable.Nullable[T] { - if p == nil { - return nullable.NewNullNullable[T]() - } - return nullable.NewNullableWithValue(*p) -} - -func toNullableWith[T, U any](p *T, f func(T) U) nullable.Nullable[U] { - if p == nil { - return nullable.NewNullNullable[U]() - } - return nullable.NewNullableWithValue(f(*p)) + return GetTournament200JSONResponse{Tournament: toAPITournament(t)}, nil } 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") diff --git a/backend/api/handler_wrapper.go b/backend/api/handler_wrapper.go index 1c8bc83..6a6c724 100644 --- a/backend/api/handler_wrapper.go +++ b/backend/api/handler_wrapper.go @@ -7,6 +7,9 @@ import ( "albatross-2026-backend/config" "albatross-2026-backend/db" + "albatross-2026-backend/game" + "albatross-2026-backend/session" + "albatross-2026-backend/tournament" ) var _ StrictServerInterface = (*HandlerWrapper)(nil) @@ -15,25 +18,25 @@ type HandlerWrapper struct { impl Handler } -func NewHandler(queries db.Querier, txm db.TxManager, hub GameHubInterface, auth AuthenticatorInterface, conf *config.Config) *HandlerWrapper { +func NewHandler(gameSvc *game.Service, tournamentSvc *tournament.Service, auth AuthenticatorInterface, queries db.Querier, conf *config.Config) *HandlerWrapper { return &HandlerWrapper{ impl: Handler{ - q: queries, - txm: txm, - hub: hub, - auth: auth, - conf: conf, + gameSvc: gameSvc, + tournamentSvc: tournamentSvc, + auth: auth, + conf: conf, + q: queries, }, } } func (h *HandlerWrapper) GetGame(ctx context.Context, request GetGameRequestObject) (GetGameResponseObject, error) { - user, _ := GetUserFromContext(ctx) + user, _ := session.GetUserFromContext(ctx) return h.impl.GetGame(ctx, request, user) } func (h *HandlerWrapper) GetGamePlayLatestState(ctx context.Context, request GetGamePlayLatestStateRequestObject) (GetGamePlayLatestStateResponseObject, error) { - user, ok := GetUserFromContext(ctx) + user, ok := session.GetUserFromContext(ctx) if !ok { return GetGamePlayLatestState401JSONResponse{ Message: "Unauthorized", @@ -43,7 +46,7 @@ func (h *HandlerWrapper) GetGamePlayLatestState(ctx context.Context, request Get } func (h *HandlerWrapper) GetGamePlaySubmissions(ctx context.Context, request GetGamePlaySubmissionsRequestObject) (GetGamePlaySubmissionsResponseObject, error) { - user, ok := GetUserFromContext(ctx) + user, ok := session.GetUserFromContext(ctx) if !ok { return GetGamePlaySubmissions401JSONResponse{ Message: "Unauthorized", @@ -53,22 +56,22 @@ func (h *HandlerWrapper) GetGamePlaySubmissions(ctx context.Context, request Get } func (h *HandlerWrapper) GetGameWatchLatestStates(ctx context.Context, request GetGameWatchLatestStatesRequestObject) (GetGameWatchLatestStatesResponseObject, error) { - user, _ := GetUserFromContext(ctx) + user, _ := session.GetUserFromContext(ctx) return h.impl.GetGameWatchLatestStates(ctx, request, user) } func (h *HandlerWrapper) GetGameWatchRanking(ctx context.Context, request GetGameWatchRankingRequestObject) (GetGameWatchRankingResponseObject, error) { - user, _ := GetUserFromContext(ctx) + user, _ := session.GetUserFromContext(ctx) return h.impl.GetGameWatchRanking(ctx, request, user) } func (h *HandlerWrapper) GetGames(ctx context.Context, request GetGamesRequestObject) (GetGamesResponseObject, error) { - user, _ := GetUserFromContext(ctx) + user, _ := session.GetUserFromContext(ctx) return h.impl.GetGames(ctx, request, user) } func (h *HandlerWrapper) GetMe(ctx context.Context, request GetMeRequestObject) (GetMeResponseObject, error) { - user, ok := GetUserFromContext(ctx) + user, ok := session.GetUserFromContext(ctx) if !ok { return GetMe401JSONResponse{ Message: "Unauthorized", @@ -78,12 +81,12 @@ func (h *HandlerWrapper) GetMe(ctx context.Context, request GetMeRequestObject) } func (h *HandlerWrapper) GetTournament(ctx context.Context, request GetTournamentRequestObject) (GetTournamentResponseObject, error) { - user, _ := GetUserFromContext(ctx) + user, _ := session.GetUserFromContext(ctx) return h.impl.GetTournament(ctx, request, user) } func (h *HandlerWrapper) PostGamePlayCode(ctx context.Context, request PostGamePlayCodeRequestObject) (PostGamePlayCodeResponseObject, error) { - user, ok := GetUserFromContext(ctx) + user, ok := session.GetUserFromContext(ctx) if !ok { return PostGamePlayCode401JSONResponse{ Message: "Unauthorized", @@ -93,7 +96,7 @@ func (h *HandlerWrapper) PostGamePlayCode(ctx context.Context, request PostGameP } func (h *HandlerWrapper) PostGamePlaySubmit(ctx context.Context, request PostGamePlaySubmitRequestObject) (PostGamePlaySubmitResponseObject, error) { - user, ok := GetUserFromContext(ctx) + user, ok := session.GetUserFromContext(ctx) if !ok { return PostGamePlaySubmit401JSONResponse{ Message: "Unauthorized", @@ -107,7 +110,7 @@ func (h *HandlerWrapper) PostLogin(ctx context.Context, request PostLoginRequest } func (h *HandlerWrapper) PostLogout(ctx context.Context, request PostLogoutRequestObject) (PostLogoutResponseObject, error) { - user, ok := GetUserFromContext(ctx) + user, ok := session.GetUserFromContext(ctx) if !ok { return PostLogout401JSONResponse{ Message: "Unauthorized", |
