aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/admin
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-21 10:46:02 +0900
committernsfisis <nsfisis@gmail.com>2026-02-21 10:46:02 +0900
commit642d3b4e1d33afd521f315b9aa99b8993d252902 (patch)
treef39e7c191033354db56675d9d3dc4c4be17d7aba /backend/admin
parente8db174d3e464a5764a9f4bfd82172261bd50519 (diff)
downloadphperkaigi-2026-albatross-642d3b4e1d33afd521f315b9aa99b8993d252902.tar.gz
phperkaigi-2026-albatross-642d3b4e1d33afd521f315b9aa99b8993d252902.tar.zst
phperkaigi-2026-albatross-642d3b4e1d33afd521f315b9aa99b8993d252902.zip
refactor(admin): separate business logic into game and tournament services
Move transaction handling, rejudge workflow, tournament bracket creation, and data repair logic from admin handler into game.Service and tournament.Service, mirroring the earlier api package separation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'backend/admin')
-rw-r--r--backend/admin/handler.go314
-rw-r--r--backend/admin/handler_test.go194
2 files changed, 149 insertions, 359 deletions
diff --git a/backend/admin/handler.go b/backend/admin/handler.go
index 09303ac..f115745 100644
--- a/backend/admin/handler.go
+++ b/backend/admin/handler.go
@@ -16,24 +16,22 @@ import (
"albatross-2026-backend/account"
"albatross-2026-backend/config"
"albatross-2026-backend/db"
+ "albatross-2026-backend/game"
"albatross-2026-backend/session"
+ "albatross-2026-backend/tournament"
)
var jst = time.FixedZone("Asia/Tokyo", 9*60*60)
-type GameHub interface {
- EnqueueTestTasks(ctx context.Context, submissionID, gameID, userID int, language, code string) error
-}
-
type Handler struct {
- q db.Querier
- txm db.TxManager
- hub GameHub
- conf *config.Config
+ gameSvc *game.Service
+ tournamentSvc *tournament.Service
+ q db.Querier
+ conf *config.Config
}
-func NewHandler(q db.Querier, txm db.TxManager, hub GameHub, conf *config.Config) *Handler {
- return &Handler{q: q, txm: txm, hub: hub, conf: conf}
+func NewHandler(gameSvc *game.Service, tournamentSvc *tournament.Service, q db.Querier, conf *config.Config) *Handler {
+ return &Handler{gameSvc: gameSvc, tournamentSvc: tournamentSvc, q: q, conf: conf}
}
func (h *Handler) newAdminMiddleware() echo.MiddlewareFunc {
@@ -149,39 +147,9 @@ func (h *Handler) getOnlineQualifyingRanking(c echo.Context) error {
}
func (h *Handler) postFix(c echo.Context) error {
- rows, err := h.q.ListSubmissionIDs(c.Request().Context())
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- for _, submissionID := range rows {
- as, err := h.q.AggregateTestcaseResults(c.Request().Context(), submissionID)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- err = h.q.UpdateSubmissionStatus(c.Request().Context(), db.UpdateSubmissionStatusParams{
- SubmissionID: submissionID,
- Status: as,
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- }
-
- rows2, err := h.q.ListGameStateIDs(c.Request().Context())
- if err != nil {
+ if err := h.gameSvc.FixSubmissionStatuses(c.Request().Context()); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- for _, r := range rows2 {
- gameID := r.GameID
- userID := r.UserID
- err := h.q.SyncGameStateBestScoreSubmission(c.Request().Context(), db.SyncGameStateBestScoreSubmissionParams{
- GameID: gameID,
- UserID: userID,
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- }
return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/dashboard")
}
@@ -519,31 +487,15 @@ func (h *Handler) postGameEdit(c echo.Context) error {
mainPlayers = append(mainPlayers, mainPlayer2)
}
- ctx := c.Request().Context()
- err = h.txm.RunInTx(ctx, func(qtx db.Querier) error {
- if err := qtx.UpdateGame(ctx, db.UpdateGameParams{
- GameID: int32(gameID),
- GameType: gameType,
- IsPublic: isPublic,
- DisplayName: displayName,
- DurationSeconds: int32(durationSeconds),
- StartedAt: changedStartedAt,
- ProblemID: int32(problemID),
- }); err != nil {
- return err
- }
- if err := qtx.RemoveAllMainPlayers(ctx, int32(gameID)); err != nil {
- return err
- }
- for _, userID := range mainPlayers {
- if err := qtx.AddMainPlayer(ctx, db.AddMainPlayerParams{
- GameID: int32(gameID),
- UserID: int32(userID),
- }); err != nil {
- return err
- }
- }
- return nil
+ err = h.gameSvc.UpdateGameWithPlayers(c.Request().Context(), game.UpdateGameParams{
+ GameID: gameID,
+ GameType: gameType,
+ IsPublic: isPublic,
+ DisplayName: displayName,
+ DurationSeconds: durationSeconds,
+ StartedAt: changedStartedAt,
+ ProblemID: problemID,
+ MainPlayerIDs: mainPlayers,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
@@ -558,36 +510,16 @@ func (h *Handler) postGameStart(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id")
}
- game, err := h.q.GetGameByID(c.Request().Context(), int32(gameID))
+ err = h.gameSvc.StartGame(c.Request().Context(), gameID)
if err != nil {
- if errors.Is(err, pgx.ErrNoRows) {
+ if errors.Is(err, game.ErrNotFound) {
return echo.NewHTTPError(http.StatusNotFound, "Game not found")
}
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- testcases, err := h.q.ListTestcasesByProblemID(c.Request().Context(), game.ProblemID)
- if err != nil {
- if errors.Is(err, pgx.ErrNoRows) {
+ if errors.Is(err, game.ErrNoTestcases) {
return echo.NewHTTPError(http.StatusBadRequest, "No testcases")
}
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- if len(testcases) == 0 {
- return echo.NewHTTPError(http.StatusBadRequest, "No testcases")
- }
-
- startedAt := time.Now().Add(10 * time.Second)
-
- err = h.q.UpdateGameStartedAt(c.Request().Context(), db.UpdateGameStartedAtParams{
- GameID: int32(gameID),
- StartedAt: pgtype.Timestamp{
- Time: startedAt,
- Valid: true,
- },
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/games")
}
@@ -673,22 +605,6 @@ func (h *Handler) getSubmissionDetail(c echo.Context) error {
})
}
-func (h *Handler) rejudgeSubmission(ctx context.Context, submission db.Submission, language string) error {
- err := h.txm.RunInTx(ctx, func(qtx db.Querier) error {
- if err := qtx.DeleteTestcaseResultsBySubmissionID(ctx, submission.SubmissionID); err != nil {
- return err
- }
- return qtx.UpdateSubmissionStatus(ctx, db.UpdateSubmissionStatusParams{
- SubmissionID: submission.SubmissionID,
- Status: "running",
- })
- })
- if err != nil {
- return err
- }
- return h.hub.EnqueueTestTasks(ctx, int(submission.SubmissionID), int(submission.GameID), int(submission.UserID), language, submission.Code)
-}
-
func (h *Handler) postSubmissionRejudge(c echo.Context) error {
gameID, err := strconv.Atoi(c.Param("gameID"))
if err != nil {
@@ -710,7 +626,7 @@ func (h *Handler) postSubmissionRejudge(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- game, err := h.q.GetGameByID(ctx, int32(gameID))
+ g, err := h.q.GetGameByID(ctx, int32(gameID))
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound)
@@ -718,7 +634,7 @@ func (h *Handler) postSubmissionRejudge(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- if err := h.rejudgeSubmission(ctx, submission, game.Language); err != nil {
+ if err := h.gameSvc.RejudgeSubmission(ctx, submission.SubmissionID, int(submission.GameID), int(submission.UserID), g.Language, submission.Code); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
@@ -731,27 +647,14 @@ func (h *Handler) postSubmissionsRejudgeLatest(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid game_id")
}
- ctx := c.Request().Context()
-
- game, err := h.q.GetGameByID(ctx, int32(gameID))
+ err = h.gameSvc.RejudgeLatestSubmissionsByGame(c.Request().Context(), gameID)
if err != nil {
- if errors.Is(err, pgx.ErrNoRows) {
+ if errors.Is(err, game.ErrNotFound) {
return echo.NewHTTPError(http.StatusNotFound)
}
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- submissions, err := h.q.GetLatestSubmissionsByGameID(ctx, int32(gameID))
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
-
- for _, s := range submissions {
- if err := h.rejudgeSubmission(ctx, s, game.Language); err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- }
-
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%sadmin/games/%d/submissions", h.conf.BasePath, gameID))
}
@@ -761,27 +664,14 @@ func (h *Handler) postSubmissionsRejudgeAll(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid game_id")
}
- ctx := c.Request().Context()
-
- game, err := h.q.GetGameByID(ctx, int32(gameID))
+ err = h.gameSvc.RejudgeAllSubmissionsByGame(c.Request().Context(), gameID)
if err != nil {
- if errors.Is(err, pgx.ErrNoRows) {
+ if errors.Is(err, game.ErrNotFound) {
return echo.NewHTTPError(http.StatusNotFound)
}
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- submissions, err := h.q.GetSubmissionsByGameID(ctx, int32(gameID))
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
-
- for _, s := range submissions {
- if err := h.rejudgeSubmission(ctx, s, game.Language); err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- }
-
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%sadmin/games/%d/submissions", h.conf.BasePath, gameID))
}
@@ -1107,23 +997,6 @@ func (h *Handler) getTournamentNew(c echo.Context) error {
})
}
-func nextPowerOf2(n int) int {
- p := 1
- for p < n {
- p *= 2
- }
- return p
-}
-
-func log2Int(n int) int {
- r := 0
- for n > 1 {
- n /= 2
- r++
- }
- return r
-}
-
func (h *Handler) postTournamentNew(c echo.Context) error {
displayName := c.FormValue("display_name")
numParticipants, err := strconv.Atoi(c.FormValue("num_participants"))
@@ -1131,34 +1004,7 @@ func (h *Handler) postTournamentNew(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid num_participants")
}
- bracketSize := nextPowerOf2(numParticipants)
- numRounds := log2Int(bracketSize)
-
- ctx := c.Request().Context()
- err = h.txm.RunInTx(ctx, func(qtx db.Querier) error {
- tournamentID, err := qtx.CreateTournament(ctx, db.CreateTournamentParams{
- DisplayName: displayName,
- BracketSize: int32(bracketSize),
- NumRounds: int32(numRounds),
- })
- if err != nil {
- return err
- }
- // Create match slots for all rounds
- for round := range numRounds {
- numPositions := bracketSize / (1 << (round + 1))
- for pos := range numPositions {
- if err := qtx.CreateTournamentMatch(ctx, db.CreateTournamentMatchParams{
- TournamentID: tournamentID,
- Round: int32(round),
- Position: int32(pos),
- }); err != nil {
- return err
- }
- }
- }
- return nil
- })
+ _, err = h.tournamentSvc.CreateTournament(c.Request().Context(), displayName, numParticipants)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
@@ -1173,30 +1019,19 @@ func (h *Handler) getTournamentEdit(c echo.Context) error {
}
ctx := c.Request().Context()
- tournament, err := h.q.GetTournamentByID(ctx, int32(tournamentID))
+ editData, err := h.tournamentSvc.GetTournamentEditData(ctx, tournamentID)
if err != nil {
- if errors.Is(err, pgx.ErrNoRows) {
+ if errors.Is(err, game.ErrNotFound) {
return echo.NewHTTPError(http.StatusNotFound)
}
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- entryRows, err := h.q.ListTournamentEntries(ctx, int32(tournamentID))
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- seedUserMap := make(map[int]int)
- for _, e := range entryRows {
- seedUserMap[int(e.Seed)] = int(e.UserID)
- }
+ t := editData.Tournament
- matchRows, err := h.q.ListTournamentMatches(ctx, int32(tournamentID))
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
matchGameMap := make(map[int]int)
var matches []echo.Map
- for _, m := range matchRows {
+ for _, m := range editData.Matches {
gameID := 0
if m.GameID != nil {
gameID = int(*m.GameID)
@@ -1235,7 +1070,7 @@ func (h *Handler) getTournamentEdit(c echo.Context) error {
})
}
- seeds := make([]int, tournament.BracketSize)
+ seeds := make([]int, t.BracketSize)
for i := range seeds {
seeds[i] = i + 1
}
@@ -1244,13 +1079,13 @@ func (h *Handler) getTournamentEdit(c echo.Context) error {
"BasePath": h.conf.BasePath,
"Title": "Tournament Edit",
"Tournament": echo.Map{
- "TournamentID": tournament.TournamentID,
- "DisplayName": tournament.DisplayName,
- "BracketSize": tournament.BracketSize,
- "NumRounds": tournament.NumRounds,
+ "TournamentID": t.TournamentID,
+ "DisplayName": t.DisplayName,
+ "BracketSize": t.BracketSize,
+ "NumRounds": t.NumRounds,
},
"Seeds": seeds,
- "SeedUserMap": seedUserMap,
+ "SeedUserMap": editData.SeedUserMap,
"Matches": matches,
"MatchGameMap": matchGameMap,
"Users": users,
@@ -1265,9 +1100,11 @@ func (h *Handler) postTournamentEdit(c echo.Context) error {
}
ctx := c.Request().Context()
- tournament, err := h.q.GetTournamentByID(ctx, int32(tournamentID))
+ // We need the tournament to know bracket size for seed parsing,
+ // and matches for match game parsing.
+ editData, err := h.tournamentSvc.GetTournamentEditData(ctx, tournamentID)
if err != nil {
- if errors.Is(err, pgx.ErrNoRows) {
+ if errors.Is(err, game.ErrNotFound) {
return echo.NewHTTPError(http.StatusNotFound)
}
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
@@ -1276,12 +1113,8 @@ func (h *Handler) postTournamentEdit(c echo.Context) error {
displayName := c.FormValue("display_name")
// Parse seed → user assignments
- type seedEntry struct {
- seed int
- userID int
- }
- var seedEntries []seedEntry
- for seed := 1; seed <= int(tournament.BracketSize); seed++ {
+ var seedEntries []tournament.SeedEntry
+ for seed := 1; seed <= int(editData.Tournament.BracketSize); seed++ {
raw := c.FormValue("seed_" + strconv.Itoa(seed))
if raw == "" || raw == "0" {
continue
@@ -1290,20 +1123,12 @@ func (h *Handler) postTournamentEdit(c echo.Context) error {
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid seed_"+strconv.Itoa(seed))
}
- seedEntries = append(seedEntries, seedEntry{seed: seed, userID: userID})
+ seedEntries = append(seedEntries, tournament.SeedEntry{Seed: seed, UserID: userID})
}
// Parse match → game assignments
- matchRows, err := h.q.ListTournamentMatches(ctx, int32(tournamentID))
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- type matchGame struct {
- matchID int
- gameID *int32
- }
- var matchGames []matchGame
- for _, m := range matchRows {
+ var matchGames []tournament.MatchGame
+ for _, m := range editData.Matches {
raw := c.FormValue("match_" + strconv.Itoa(int(m.TournamentMatchID)))
var gameID *int32
if raw != "" && raw != "0" {
@@ -1314,46 +1139,19 @@ func (h *Handler) postTournamentEdit(c echo.Context) error {
gid32 := int32(gid)
gameID = &gid32
}
- matchGames = append(matchGames, matchGame{matchID: int(m.TournamentMatchID), gameID: gameID})
+ matchGames = append(matchGames, tournament.MatchGame{MatchID: int(m.TournamentMatchID), GameID: gameID})
}
- err = h.txm.RunInTx(ctx, func(qtx db.Querier) error {
- if err := qtx.UpdateTournament(ctx, db.UpdateTournamentParams{
- TournamentID: int32(tournamentID),
- DisplayName: displayName,
- BracketSize: tournament.BracketSize,
- NumRounds: tournament.NumRounds,
- }); err != nil {
- return err
- }
-
- // Replace entries
- if err := qtx.DeleteTournamentEntries(ctx, int32(tournamentID)); err != nil {
- return err
- }
- for _, se := range seedEntries {
- if err := qtx.CreateTournamentEntry(ctx, db.CreateTournamentEntryParams{
- TournamentID: int32(tournamentID),
- UserID: int32(se.userID),
- Seed: int32(se.seed),
- }); err != nil {
- return err
- }
- }
-
- // Update match game assignments
- for _, mg := range matchGames {
- if err := qtx.UpdateTournamentMatchGame(ctx, db.UpdateTournamentMatchGameParams{
- TournamentMatchID: int32(mg.matchID),
- GameID: mg.gameID,
- }); err != nil {
- return err
- }
- }
-
- return nil
+ err = h.tournamentSvc.UpdateTournament(ctx, tournament.UpdateTournamentParams{
+ TournamentID: tournamentID,
+ DisplayName: displayName,
+ SeedEntries: seedEntries,
+ MatchGames: matchGames,
})
if err != nil {
+ if errors.Is(err, game.ErrNotFound) {
+ return echo.NewHTTPError(http.StatusNotFound)
+ }
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
diff --git a/backend/admin/handler_test.go b/backend/admin/handler_test.go
index 20f5775..ac9ccc6 100644
--- a/backend/admin/handler_test.go
+++ b/backend/admin/handler_test.go
@@ -16,7 +16,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 admin handler testing.
@@ -57,6 +59,12 @@ type mockQuerier struct {
listTournamentMatchesFunc func(ctx context.Context, tournamentID int32) ([]db.TournamentMatch, error)
createTournamentMatchFunc func(ctx context.Context, arg db.CreateTournamentMatchParams) error
updateTournamentMatchGameFunc func(ctx context.Context, arg db.UpdateTournamentMatchGameParams) error
+ updateGameFunc func(ctx context.Context, arg db.UpdateGameParams) error
+ removeAllMainPlayersFunc func(ctx context.Context, gameID int32) error
+ addMainPlayerFunc func(ctx context.Context, arg db.AddMainPlayerParams) error
+ aggregateTestcaseResultsFunc func(ctx context.Context, submissionID int32) (string, error)
+ listGameStateIDsFunc func(ctx context.Context) ([]db.ListGameStateIDsRow, error)
+ syncGameStateBestScoreSubmissionFunc func(ctx context.Context, arg db.SyncGameStateBestScoreSubmissionParams) error
}
func (m *mockQuerier) GetUserByID(ctx context.Context, userID int32) (db.User, error) {
@@ -308,11 +316,57 @@ func (m *mockQuerier) UpdateTournamentMatchGame(ctx context.Context, arg db.Upda
return nil
}
-// mockGameHub implements GameHub for testing.
+func (m *mockQuerier) UpdateGame(ctx context.Context, arg db.UpdateGameParams) error {
+ if m.updateGameFunc != nil {
+ return m.updateGameFunc(ctx, arg)
+ }
+ return nil
+}
+
+func (m *mockQuerier) RemoveAllMainPlayers(ctx context.Context, gameID int32) error {
+ if m.removeAllMainPlayersFunc != nil {
+ return m.removeAllMainPlayersFunc(ctx, gameID)
+ }
+ return nil
+}
+
+func (m *mockQuerier) AddMainPlayer(ctx context.Context, arg db.AddMainPlayerParams) error {
+ if m.addMainPlayerFunc != nil {
+ return m.addMainPlayerFunc(ctx, arg)
+ }
+ return nil
+}
+
+func (m *mockQuerier) AggregateTestcaseResults(ctx context.Context, submissionID int32) (string, error) {
+ if m.aggregateTestcaseResultsFunc != nil {
+ return m.aggregateTestcaseResultsFunc(ctx, submissionID)
+ }
+ return "pass", nil
+}
+
+func (m *mockQuerier) ListGameStateIDs(ctx context.Context) ([]db.ListGameStateIDsRow, error) {
+ if m.listGameStateIDsFunc != nil {
+ return m.listGameStateIDsFunc(ctx)
+ }
+ return nil, nil
+}
+
+func (m *mockQuerier) SyncGameStateBestScoreSubmission(ctx context.Context, arg db.SyncGameStateBestScoreSubmissionParams) error {
+ if m.syncGameStateBestScoreSubmissionFunc != nil {
+ return m.syncGameStateBestScoreSubmissionFunc(ctx, arg)
+ }
+ return nil
+}
+
+// mockGameHub implements game.GameHubInterface for testing.
type mockGameHub struct {
enqueueTestTasksFunc func(ctx context.Context, submissionID, gameID, userID int, language, code string) error
}
+func (m *mockGameHub) CalcCodeSize(_ string, _ string) int {
+ return 0
+}
+
func (m *mockGameHub) EnqueueTestTasks(ctx context.Context, submissionID, gameID, userID int, language, code string) error {
if m.enqueueTestTasksFunc != nil {
return m.enqueueTestTasksFunc(ctx, submissionID, gameID, userID, language, code)
@@ -321,7 +375,9 @@ func (m *mockGameHub) EnqueueTestTasks(ctx context.Context, submissionID, gameID
}
// mockTxManager implements db.TxManager for testing.
+// By default it passes the provided querier to the function.
type mockTxManager struct {
+ q db.Querier
runInTxFunc func(ctx context.Context, fn func(q db.Querier) error) error
}
@@ -329,6 +385,9 @@ func (m *mockTxManager) RunInTx(ctx context.Context, fn func(q db.Querier) error
if m.runInTxFunc != nil {
return m.runInTxFunc(ctx, fn)
}
+ if m.q != nil {
+ return fn(m.q)
+ }
return fn(&mockQuerier{})
}
@@ -345,11 +404,27 @@ func (r *mockRenderer) Render(_ io.Writer, name string, data any, _ echo.Context
}
func newTestHandler(q *mockQuerier) *Handler {
+ hub := &mockGameHub{}
+ txm := &mockTxManager{q: q}
+ gameSvc := game.NewService(q, txm, hub)
+ tournamentSvc := tournament.NewService(q, txm)
+ return &Handler{
+ gameSvc: gameSvc,
+ tournamentSvc: tournamentSvc,
+ q: q,
+ conf: &config.Config{BasePath: "/test/"},
+ }
+}
+
+func newTestHandlerWithHub(q *mockQuerier, hub *mockGameHub) *Handler {
+ txm := &mockTxManager{q: q}
+ gameSvc := game.NewService(q, txm, hub)
+ tournamentSvc := tournament.NewService(q, txm)
return &Handler{
- q: q,
- txm: &mockTxManager{},
- hub: &mockGameHub{},
- conf: &config.Config{BasePath: "/test/"},
+ gameSvc: gameSvc,
+ tournamentSvc: tournamentSvc,
+ q: q,
+ conf: &config.Config{BasePath: "/test/"},
}
}
@@ -1182,47 +1257,6 @@ func TestGetSubmissionDetail_NotFound(t *testing.T) {
// --- Tournament admin tests ---
-func TestNextPowerOf2(t *testing.T) {
- tests := []struct {
- input int
- expected int
- }{
- {2, 2},
- {3, 4},
- {4, 4},
- {5, 8},
- {6, 8},
- {7, 8},
- {8, 8},
- {9, 16},
- }
- for _, tt := range tests {
- got := nextPowerOf2(tt.input)
- if got != tt.expected {
- t.Errorf("nextPowerOf2(%d) = %d, want %d", tt.input, got, tt.expected)
- }
- }
-}
-
-func TestLog2Int(t *testing.T) {
- tests := []struct {
- input int
- expected int
- }{
- {1, 0},
- {2, 1},
- {4, 2},
- {8, 3},
- {16, 4},
- }
- for _, tt := range tests {
- got := log2Int(tt.input)
- if got != tt.expected {
- t.Errorf("log2Int(%d) = %d, want %d", tt.input, got, tt.expected)
- }
- }
-}
-
func TestGetTournaments_Empty(t *testing.T) {
h := newTestHandler(&mockQuerier{})
c, rec := newEchoContext(http.MethodGet, "/admin/tournaments", nil)
@@ -1266,25 +1300,18 @@ func TestGetTournamentNew(t *testing.T) {
func TestPostTournamentNew_Success(t *testing.T) {
var createdParams db.CreateTournamentParams
var matchCount int
- h := &Handler{
- q: &mockQuerier{},
- hub: &mockGameHub{},
- txm: &mockTxManager{
- runInTxFunc: func(_ context.Context, fn func(q db.Querier) error) error {
- return fn(&mockQuerier{
- createTournamentFunc: func(_ context.Context, arg db.CreateTournamentParams) (int32, error) {
- createdParams = arg
- return 1, nil
- },
- createTournamentMatchFunc: func(_ context.Context, _ db.CreateTournamentMatchParams) error {
- matchCount++
- return nil
- },
- })
- },
+ q := &mockQuerier{
+ createTournamentFunc: func(_ context.Context, arg db.CreateTournamentParams) (int32, error) {
+ createdParams = arg
+ return 1, nil
+ },
+ createTournamentMatchFunc: func(_ context.Context, _ db.CreateTournamentMatchParams) error {
+ matchCount++
+ return nil
},
- conf: &config.Config{BasePath: "/test/"},
}
+ h := newTestHandler(q)
+
form := url.Values{
"display_name": {"Test Tournament"},
"num_participants": {"3"},
@@ -1402,8 +1429,6 @@ func TestPostTournamentEdit_NotFound(t *testing.T) {
// --- Rejudge tests ---
func TestPostSubmissionRejudge_Success(t *testing.T) {
- var deletedSubmissionID int32
- var updatedStatus db.UpdateSubmissionStatusParams
var enqueuedSubmissionID, enqueuedGameID, enqueuedUserID int
var enqueuedLanguage, enqueuedCode string
@@ -1435,22 +1460,7 @@ func TestPostSubmissionRejudge_Success(t *testing.T) {
},
}
- txm := &mockTxManager{
- runInTxFunc: func(_ context.Context, fn func(q db.Querier) error) error {
- return fn(&mockQuerier{
- deleteTestcaseResultsBySubmissionIDFunc: func(_ context.Context, submissionID int32) error {
- deletedSubmissionID = submissionID
- return nil
- },
- updateSubmissionStatusFunc: func(_ context.Context, arg db.UpdateSubmissionStatusParams) error {
- updatedStatus = arg
- return nil
- },
- })
- },
- }
-
- h := &Handler{q: q, txm: txm, hub: hub, conf: &config.Config{BasePath: "/test/"}}
+ h := newTestHandlerWithHub(q, hub)
c, rec := newEchoContextWithForm("/admin/games/1/submissions/5/rejudge", map[string]string{
"gameID": "1",
@@ -1464,12 +1474,6 @@ func TestPostSubmissionRejudge_Success(t *testing.T) {
if rec.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", rec.Code, http.StatusSeeOther)
}
- if deletedSubmissionID != 5 {
- t.Errorf("deleted submission ID = %d, want 5", deletedSubmissionID)
- }
- if updatedStatus.SubmissionID != 5 || updatedStatus.Status != "running" {
- t.Errorf("updated status = %+v, want {SubmissionID: 5, Status: running}", updatedStatus)
- }
if enqueuedSubmissionID != 5 {
t.Errorf("enqueued submission ID = %d, want 5", enqueuedSubmissionID)
}
@@ -1530,13 +1534,7 @@ func TestPostSubmissionsRejudgeLatest_Success(t *testing.T) {
},
}
- txm := &mockTxManager{
- runInTxFunc: func(_ context.Context, fn func(q db.Querier) error) error {
- return fn(&mockQuerier{})
- },
- }
-
- h := &Handler{q: q, txm: txm, hub: hub, conf: &config.Config{BasePath: "/test/"}}
+ h := newTestHandlerWithHub(q, hub)
c, rec := newEchoContextWithForm("/admin/games/1/submissions/rejudge-latest", map[string]string{
"gameID": "1",
@@ -1580,13 +1578,7 @@ func TestPostSubmissionsRejudgeAll_Success(t *testing.T) {
},
}
- txm := &mockTxManager{
- runInTxFunc: func(_ context.Context, fn func(q db.Querier) error) error {
- return fn(&mockQuerier{})
- },
- }
-
- h := &Handler{q: q, txm: txm, hub: hub, conf: &config.Config{BasePath: "/test/"}}
+ h := newTestHandlerWithHub(q, hub)
c, rec := newEchoContextWithForm("/admin/games/1/submissions/rejudge-all", map[string]string{
"gameID": "1",