Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func main() {
if err := database.EnsureMatchPartitions(context.Background()); err != nil {
log.Error("Failed to ensure match partitions", zap.Error(err))
}
database.StartPartitionMaintenance()

// Подключаемся к Redis
redisCache, err := cache.New(&cfg.Redis, log, m)
Expand Down
4 changes: 2 additions & 2 deletions internal/api/handlers/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,10 @@ func (h *GameHandler) AddGameToTournament(w http.ResponseWriter, r *http.Request
writeError(w, errors.ErrUnauthorized)
return
}
userRole, _ := r.Context().Value(middleware.RoleKey).(string)
userRole, _ := r.Context().Value(middleware.RoleKey).(domain.Role)

// Проверяем права: админ или создатель турнира
isAdmin := userRole == "admin"
isAdmin := userRole == domain.RoleAdmin
isCreator := false

if !isAdmin && h.tournamentRepo != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/game_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ func TestGameHandler_AddGameToTournament_AdminSuccess(t *testing.T) {
rctx.URLParams.Add("id", tournamentID.String())
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
ctx = context.WithValue(ctx, middleware.UserIDKey, userID)
ctx = context.WithValue(ctx, middleware.RoleKey, "admin")
ctx = context.WithValue(ctx, middleware.RoleKey, domain.RoleAdmin)
req = req.WithContext(ctx)
rr := httptest.NewRecorder()

Expand Down
1 change: 1 addition & 0 deletions internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ func (s *Server) setupRoutes() {
// Program routes (все требуют аутентификации)
r.Route("/programs", func(r chi.Router) {
r.Use(middleware.Auth(s.authService, s.log))
r.Use(middleware.MaxBodySize(10 << 20)) // 10MB for file uploads

r.Post("/", s.programHandler.Create)
r.Get("/", s.programHandler.List)
Expand Down
6 changes: 3 additions & 3 deletions internal/domain/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ func (t *Tournament) Validate() error {
}
}

// Валидация max_participants
if t.MaxParticipants != nil && *t.MaxParticipants <= 0 {
errs.Add("max_participants", "max_participants must be positive")
// Валидация max_participants (минимум 2, т.к. турнир требует ≥2 участников)
if t.MaxParticipants != nil && *t.MaxParticipants < 2 {
errs.Add("max_participants", "max_participants must be at least 2")
}

if errs.HasErrors() {
Expand Down
24 changes: 24 additions & 0 deletions internal/infrastructure/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,30 @@ func (db *DB) EnsureMatchPartitions(ctx context.Context) error {
return nil
}

// StartPartitionMaintenance launches a background goroutine that periodically
// ensures match partitions exist for the current and next month. This prevents
// partition-not-found errors if the application runs for extended periods
// without restart.
func (db *DB) StartPartitionMaintenance() {
go func() {
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()

for {
select {
case <-ticker.C:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
if err := db.EnsureMatchPartitions(ctx); err != nil {
db.log.Error("Periodic partition maintenance failed", zap.Error(err))
}
cancel()
case <-db.done:
return
}
}
}()
}

// Close закрывает соединение с базой данных
func (db *DB) Close() error {
close(db.done)
Expand Down
13 changes: 13 additions & 0 deletions internal/infrastructure/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,19 @@ func (e *Executor) parseResult(exitCode int64, stdout, stderr string) (*domain.M
return nil, fmt.Errorf("invalid score2: %s", scores[1])
}

// Validate score bounds to prevent extreme values from malicious bot output.
// Scale the ceiling with configured iterations so legitimate high-iteration
// runs are not rejected. 1000 points-per-iteration is a generous upper
// bound for any supported game type; the floor of 100 000 covers the
// default 100-iteration setting comfortably.
maxScore := e.config.DefaultIterations * 1000
if maxScore < 100_000 {
maxScore = 100_000
}
if score1 < 0 || score1 > maxScore || score2 < 0 || score2 > maxScore {
return nil, fmt.Errorf("scores out of bounds [0, %d]: %d, %d", maxScore, score1, score2)
}

result.Score1 = score1
result.Score2 = score2

Expand Down
48 changes: 45 additions & 3 deletions internal/infrastructure/executor/executor_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,56 @@ func TestBoolPtr(t *testing.T) {
func TestParseResult_LargeScores(t *testing.T) {
e := newTestExecutor(t)

result, err := e.parseResult(0, "999999 888888", "")
// Scores within the allowed bound [0, 100000]
result, err := e.parseResult(0, "99999 88888", "")

require.NoError(t, err)
assert.Equal(t, 999999, result.Score1)
assert.Equal(t, 888888, result.Score2)
assert.Equal(t, 99999, result.Score1)
assert.Equal(t, 88888, result.Score2)
assert.Equal(t, 1, result.Winner)
}

func TestParseResult_ScoresOutOfBounds(t *testing.T) {
e := newTestExecutor(t)

// Default config (0 iterations) → floor of 100_000
_, err := e.parseResult(0, "999999 888888", "")

require.Error(t, err)
assert.Contains(t, err.Error(), "scores out of bounds")
}

func TestParseResult_HighIterationsAcceptsLargeScores(t *testing.T) {
e := newTestExecutor(t)
e.config.DefaultIterations = 500 // maxScore = 500*1000 = 500_000

result, err := e.parseResult(0, "450000 300000", "")

require.NoError(t, err)
assert.Equal(t, 450000, result.Score1)
assert.Equal(t, 300000, result.Score2)
assert.Equal(t, 1, result.Winner)
}

func TestParseResult_HighIterationsStillRejectsExtremeScores(t *testing.T) {
e := newTestExecutor(t)
e.config.DefaultIterations = 500 // maxScore = 500_000

_, err := e.parseResult(0, "999999 888888", "")

require.Error(t, err)
assert.Contains(t, err.Error(), "scores out of bounds")
}

func TestParseResult_NegativeScore(t *testing.T) {
e := newTestExecutor(t)

_, err := e.parseResult(0, "-1 50", "")

require.Error(t, err)
assert.Contains(t, err.Error(), "scores out of bounds")
}

func TestParseResult_ZeroScores(t *testing.T) {
e := newTestExecutor(t)

Expand Down
Loading