From 4fb0b80f24dee546179b2f861a01103bd27adcfe Mon Sep 17 00:00:00 2001 From: Vyacheslav1557 Date: Sat, 12 Apr 2025 00:12:28 +0500 Subject: [PATCH] test(tester): fix tests --- go.mod | 5 +- go.sum | 7 + .../repository/pg_contests_repository.go | 163 +- .../repository/pg_contests_repository_test.go | 1364 ++++++++++++++++- .../repository/pg_problems_repository_test.go | 140 +- proto | 2 +- 6 files changed, 1500 insertions(+), 181 deletions(-) diff --git a/go.mod b/go.mod index 42f6dcd..1ffca01 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,13 @@ go 1.23.6 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/Masterminds/squirrel v1.5.4 github.com/gofiber/fiber/v2 v2.52.6 github.com/golang-jwt/jwt/v4 v4.5.1 github.com/google/uuid v1.6.0 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 + github.com/microcosm-cc/bluemonday v1.0.27 github.com/oapi-codegen/runtime v1.1.1 github.com/open-policy-agent/opa v1.2.0 github.com/rabbitmq/amqp091-go v1.10.0 @@ -35,10 +37,11 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.21.0 // indirect diff --git a/go.sum b/go.sum index 16d2140..b86972e 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= @@ -97,6 +99,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -146,6 +152,7 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= diff --git a/internal/tester/repository/pg_contests_repository.go b/internal/tester/repository/pg_contests_repository.go index ec73a08..6a72f5b 100644 --- a/internal/tester/repository/pg_contests_repository.go +++ b/internal/tester/repository/pg_contests_repository.go @@ -2,10 +2,9 @@ package repository import ( "context" - "fmt" "git.sch9.ru/new_gate/ms-tester/internal/models" + sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" - "strings" ) type ContestRepository struct { @@ -172,7 +171,7 @@ const ( func (r *ContestRepository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) { const op = "ContestRepository.ReadTasks" - var contests []*models.ContestsListItem + contests := make([]*models.ContestsListItem, 0) query := r.db.Rebind(readContestsListQuery) err := r.db.SelectContext(ctx, &contests, query, filter.PageSize, filter.Offset()) if err != nil { @@ -207,7 +206,7 @@ func (r *ContestRepository) ListParticipants(ctx context.Context, filter models. filter.PageSize = 1 } - var participants []*models.ParticipantsListItem + participants := make([]*models.ParticipantsListItem, 0) query := r.db.Rebind(readParticipantsListQuery) err := r.db.SelectContext(ctx, &participants, query, filter.ContestId, filter.PageSize, filter.Offset()) if err != nil { @@ -313,93 +312,94 @@ func (r *ContestRepository) CreateSolution(ctx context.Context, creation *models return id, nil } +// buildListSolutionsQueries builds two SQL queries: one for selecting solutions +// and another for counting them. The first query selects all columns that are +// needed for the solutions list, including the task and contest titles, and +// the participant name. The second query counts the number of solutions that +// match the filter. +// +// The caller is responsible for executing the queries and processing the +// results. +func buildListSolutionsQueries(filter models.SolutionsFilter) (sq.SelectBuilder, sq.SelectBuilder) { + columns := []string{ + "s.id", + "s.participant_id", + "p2.name AS participant_name", + "s.state", + "s.score", + "s.penalty", + "s.time_stat", + "s.memory_stat", + "s.language", + "s.task_id", + "t.position AS task_position", + "p.title AS task_title", + "t.contest_id", + "c.title", + "s.updated_at", + "s.created_at", + } + + qb := sq.Select(columns...). + From("solutions s"). + LeftJoin("tasks t ON s.task_id = t.id"). + LeftJoin("problems p ON t.problem_id = p.id"). + LeftJoin("contests c ON t.contest_id = c.id"). + LeftJoin("participants p2 ON s.participant_id = p2.id") + + if filter.ContestId != nil { + qb = qb.Where("s.contest_id = ?", *filter.ContestId) + } + if filter.ParticipantId != nil { + qb = qb.Where("s.participant_id = ?", *filter.ParticipantId) + } + if filter.TaskId != nil { + qb = qb.Where("s.task_id = ?", *filter.TaskId) + } + if filter.Language != nil { + qb = qb.Where("s.language = ?", *filter.Language) + } + if filter.State != nil { + qb = qb.Where("s.state = ?", *filter.State) + } + + countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub") + + if filter.Order != nil && *filter.Order < 0 { + qb = qb.OrderBy("s.id DESC") + } + + qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset())) + + return qb, countQb +} + func (r *ContestRepository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) { const op = "ContestRepository.ListSolutions" - baseQuery := ` -SELECT s.id, + baseQb, countQb := buildListSolutionsQueries(filter) - s.participant_id, - p2.name as participant_name, - - s.state, - s.score, - s.penalty, - s.time_stat, - s.memory_stat, - s.language, - - s.task_id, - t.position as task_position, - p.title as task_title, - - t.contest_id, - c.title, - - s.updated_at, - s.created_at -FROM solutions s - LEFT JOIN tasks t ON s.task_id = t.id - LEFT JOIN problems p ON t.problem_id = p.id - LEFT JOIN contests c ON t.contest_id = c.id - LEFT JOIN participants p2 on s.participant_id = p2.id -WHERE 1=1 - ` - - var conditions []string - var args []interface{} - - if filter.ContestId != nil { - conditions = append(conditions, "s.contest_id = ?") - args = append(args, *filter.ContestId) + query, args, err := countQb.ToSql() + if err != nil { + return nil, handlePgErr(err, op) } - if filter.ParticipantId != nil { - conditions = append(conditions, "s.participant_id = ?") - args = append(args, *filter.ParticipantId) - } - if filter.TaskId != nil { - conditions = append(conditions, "s.task_id = ?") - args = append(args, *filter.TaskId) - } - if filter.Language != nil { - conditions = append(conditions, "s.language = ?") - args = append(args, *filter.Language) - } - if filter.State != nil { - conditions = append(conditions, "s.state = ?") - args = append(args, *filter.State) - } - - if len(conditions) > 0 { - baseQuery += " AND " + strings.Join(conditions, " AND ") - } - - if filter.Order != nil { - orderDirection := "ASC" - if *filter.Order < 0 { - orderDirection = "DESC" - } - baseQuery += fmt.Sprintf(" ORDER BY s.id %s", orderDirection) - } - - countQuery := "SELECT COUNT(*) FROM (" + baseQuery + ") as count_table" var totalCount int32 - err := r.db.QueryRowxContext(ctx, r.db.Rebind(countQuery), args...).Scan(&totalCount) + err = r.db.QueryRowxContext(ctx, r.db.Rebind(query), args...).Scan(&totalCount) if err != nil { return nil, handlePgErr(err, op) } - offset := (filter.Page - 1) * filter.PageSize - baseQuery += " LIMIT ? OFFSET ?" - args = append(args, filter.PageSize, offset) - - rows, err := r.db.QueryxContext(ctx, r.db.Rebind(baseQuery), args...) + query, args, err = baseQb.ToSql() + if err != nil { + return nil, handlePgErr(err, op) + } + rows, err := r.db.QueryxContext(ctx, r.db.Rebind(query), args...) if err != nil { return nil, handlePgErr(err, op) } defer rows.Close() - var solutions []*models.SolutionsListItem + solutions := make([]*models.SolutionsListItem, 0) for rows.Next() { var solution models.SolutionsListItem err = rows.StructScan(&solution) @@ -551,7 +551,7 @@ WITH Attempts AS ( MIN(CASE WHEN s.state = 5 THEN s.penalty END) as success_penalty FROM solutions s JOIN tasks t ON t.id = s.task_id - WHERE t.contest_id = :contest_id + WHERE t.contest_id = ? GROUP BY s.participant_id, s.task_id ) SELECT @@ -559,11 +559,11 @@ SELECT p.name, COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total, COALESCE(SUM(CASE WHEN a.success_penalty IS NOT NULL - THEN a.failed_attempts * :penalty + a.success_penalty + THEN a.failed_attempts * ? + a.success_penalty ELSE 0 END), 0) as penalty_in_total FROM participants p LEFT JOIN Attempts a ON a.participant_id = p.id -WHERE p.contest_id = :contest_id +WHERE p.contest_id = ? GROUP BY p.id, p.name ` ) @@ -596,10 +596,7 @@ func (r *ContestRepository) ReadMonitor(ctx context.Context, contestId int32) (* penalty := int32(20) // FIXME namedQuery := r.db.Rebind(participantsQuery) - rows3, err := r.db.NamedQueryContext(ctx, namedQuery, map[string]interface{}{ - "contest_id": contestId, - "penalty": penalty, - }) + rows3, err := r.db.QueryxContext(ctx, namedQuery, contestId, penalty, contestId) if err != nil { return nil, handlePgErr(err, op) } diff --git a/internal/tester/repository/pg_contests_repository_test.go b/internal/tester/repository/pg_contests_repository_test.go index be2cccd..0b46c2e 100644 --- a/internal/tester/repository/pg_contests_repository_test.go +++ b/internal/tester/repository/pg_contests_repository_test.go @@ -2,153 +2,1369 @@ package repository import ( "context" + "database/sql" + "database/sql/driver" + "errors" + "git.sch9.ru/new_gate/ms-tester/internal/models" + "git.sch9.ru/new_gate/ms-tester/pkg" "github.com/DATA-DOG/go-sqlmock" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" - "go.uber.org/zap" "testing" + "time" ) -func TestContestRepository_CreateContest(t *testing.T) { - t.Parallel() +// contestTestFixture encapsulates common test setup +type contestTestFixture struct { + db *sql.DB + sqlxDB *sqlx.DB + mock sqlmock.Sqlmock + contestRepo *ContestRepository +} +// newContestTestFixture creates and initializes a new test fixture +func newContestTestFixture(t *testing.T) *contestTestFixture { db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) require.NoError(t, err) - defer db.Close() sqlxDB := sqlx.NewDb(db, "sqlmock") - defer sqlxDB.Close() + contestRepo := NewContestRepository(sqlxDB) - contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + return &contestTestFixture{ + db: db, + sqlxDB: sqlxDB, + mock: mock, + contestRepo: contestRepo, + } +} + +// cleanup closes database connections +func (tf *contestTestFixture) cleanup() { + tf.db.Close() + tf.sqlxDB.Close() +} + +func TestContestRepository_CreateContest(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() t.Run("valid contest creation", func(t *testing.T) { title := "Contest title" - rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - mock.ExpectQuery(sqlxDB.Rebind(createContestQuery)).WithArgs(title).WillReturnRows(rows) + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(createContestQuery)). + WithArgs(title). + WillReturnRows(rows) - id, err := contestRepo.CreateContest(context.Background(), title) + id, err := tf.contestRepo.CreateContest(context.Background(), title) require.NoError(t, err) require.Equal(t, int32(1), id) }) } +func TestContestRepository_ReadContestById(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("valid contest read", func(t *testing.T) { + contestID := int32(1) + now := time.Now().UTC() + expectedContest := &models.Contest{ + Id: contestID, + Title: "Contest title", + CreatedAt: now, + UpdatedAt: now, + } + + rows := sqlmock.NewRows([]string{"id", "title", "created_at", "updated_at"}). + AddRow(expectedContest.Id, expectedContest.Title, expectedContest.CreatedAt, expectedContest.UpdatedAt) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readContestByIdQuery)). + WithArgs(contestID). + WillReturnRows(rows) + + contest, err := tf.contestRepo.ReadContestById(context.Background(), contestID) + require.NoError(t, err) + require.Equal(t, expectedContest, contest) + }) + + t.Run("contest not found", func(t *testing.T) { + contestID := int32(999) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readContestByIdQuery)). + WithArgs(contestID). + WillReturnError(sql.ErrNoRows) + + contest, err := tf.contestRepo.ReadContestById(context.Background(), contestID) + require.Error(t, err) + require.ErrorIs(t, err, pkg.ErrNotFound) + require.Nil(t, contest) + }) +} + func TestContestRepository_DeleteContest(t *testing.T) { - t.Parallel() - - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - require.NoError(t, err) - defer db.Close() - - sqlxDB := sqlx.NewDb(db, "sqlmock") - defer sqlxDB.Close() - - contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + tf := newContestTestFixture(t) + defer tf.cleanup() t.Run("valid contest deletion", func(t *testing.T) { id := int32(1) rows := sqlmock.NewResult(1, 1) - mock.ExpectExec(sqlxDB.Rebind(deleteContestQuery)).WithArgs(id).WillReturnResult(rows) + tf.mock.ExpectExec(tf.sqlxDB.Rebind(deleteContestQuery)). + WithArgs(id). + WillReturnResult(rows) - err = contestRepo.DeleteContest(context.Background(), id) + err := tf.contestRepo.DeleteContest(context.Background(), id) require.NoError(t, err) }) } func TestContestRepository_AddTask(t *testing.T) { - t.Parallel() - - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - require.NoError(t, err) - defer db.Close() - - sqlxDB := sqlx.NewDb(db, "sqlmock") - defer sqlxDB.Close() - - contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) - - t.Run("valid task additional", func(t *testing.T) { - taskId := int32(1) - contestId := int32(1) + tf := newContestTestFixture(t) + defer tf.cleanup() + t.Run("valid task addition", func(t *testing.T) { + taskID := int32(1) + contestID := int32(1) rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - mock.ExpectQuery(sqlxDB.Rebind(addTaskQuery)).WithArgs(taskId, contestId, contestId).WillReturnRows(rows) - - id, err := contestRepo.AddTask(context.Background(), contestId, taskId) + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(addTaskQuery)). + WithArgs(taskID, contestID, contestID). + WillReturnRows(rows) + id, err := tf.contestRepo.AddTask(context.Background(), contestID, taskID) require.NoError(t, err) require.Equal(t, int32(1), id) - }) } func TestContestRepository_DeleteTask(t *testing.T) { - t.Parallel() - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - require.NoError(t, err) - defer db.Close() + tf := newContestTestFixture(t) + defer tf.cleanup() - sqlxDB := sqlx.NewDb(db, "sqlmock") - defer sqlxDB.Close() - contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) t.Run("valid task deletion", func(t *testing.T) { id := int32(1) rows := sqlmock.NewResult(1, 1) - mock.ExpectExec(sqlxDB.Rebind(deleteTaskQuery)).WithArgs(id).WillReturnResult(rows) + tf.mock.ExpectExec(tf.sqlxDB.Rebind(deleteTaskQuery)). + WithArgs(id). + WillReturnResult(rows) - err = contestRepo.DeleteTask(context.Background(), id) + err := tf.contestRepo.DeleteTask(context.Background(), id) require.NoError(t, err) }) } func TestContestRepository_AddParticipant(t *testing.T) { - t.Parallel() - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - require.NoError(t, err) - defer db.Close() - - sqlxDB := sqlx.NewDb(db, "sqlmock") - defer sqlxDB.Close() - contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + tf := newContestTestFixture(t) + defer tf.cleanup() t.Run("valid participant addition", func(t *testing.T) { - contestId := int32(1) - userId := int32(1) - name := "" - + contestID := int32(1) + userID := int32(1) rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - mock.ExpectQuery(sqlxDB.Rebind(addParticipantQuery)).WithArgs(contestId, userId, name).WillReturnRows(rows) - - id, err := contestRepo.AddParticipant(context.Background(), contestId, userId) + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(addParticipantQuery)). + WithArgs(contestID, userID, ""). + WillReturnRows(rows) + id, err := tf.contestRepo.AddParticipant(context.Background(), contestID, userID) require.NoError(t, err) require.Equal(t, int32(1), id) }) } func TestContestRepository_DeleteParticipant(t *testing.T) { - t.Parallel() - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - require.NoError(t, err) - defer db.Close() - - sqlxDB := sqlx.NewDb(db, "sqlmock") - defer sqlxDB.Close() - - contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + tf := newContestTestFixture(t) + defer tf.cleanup() t.Run("valid participant deletion", func(t *testing.T) { id := int32(1) rows := sqlmock.NewResult(1, 1) - mock.ExpectExec(sqlxDB.Rebind(deleteParticipantQuery)).WithArgs(id).WillReturnResult(rows) + tf.mock.ExpectExec(tf.sqlxDB.Rebind(deleteParticipantQuery)). + WithArgs(id). + WillReturnResult(rows) - err = contestRepo.DeleteParticipant(context.Background(), id) + err := tf.contestRepo.DeleteParticipant(context.Background(), id) require.NoError(t, err) }) } + +func TestContestRepository_ReadTasks(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("successful tasks retrieval", func(t *testing.T) { + contestID := int32(1) + now := time.Now().UTC() + expectedTasks := []*models.TasksListItem{ + { + Id: 1, + ProblemId: 10, + ContestId: contestID, + Position: 1, + Title: "Task 1", + MemoryLimit: 256, + TimeLimit: 1000, + CreatedAt: now, + UpdatedAt: now, + }, + { + Id: 2, + ProblemId: 11, + ContestId: contestID, + Position: 2, + Title: "Task 2", + MemoryLimit: 512, + TimeLimit: 2000, + CreatedAt: now, + UpdatedAt: now, + }, + } + + rows := sqlmock.NewRows([]string{ + "id", "problem_id", "contest_id", "position", "title", + "memory_limit", "time_limit", "created_at", "updated_at", + }). + AddRow( + expectedTasks[0].Id, expectedTasks[0].ProblemId, expectedTasks[0].ContestId, + expectedTasks[0].Position, expectedTasks[0].Title, expectedTasks[0].MemoryLimit, + expectedTasks[0].TimeLimit, expectedTasks[0].CreatedAt, expectedTasks[0].UpdatedAt, + ). + AddRow( + expectedTasks[1].Id, expectedTasks[1].ProblemId, expectedTasks[1].ContestId, + expectedTasks[1].Position, expectedTasks[1].Title, expectedTasks[1].MemoryLimit, + expectedTasks[1].TimeLimit, expectedTasks[1].CreatedAt, expectedTasks[1].UpdatedAt, + ) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readTasksQuery)). + WithArgs(contestID). + WillReturnRows(rows) + + tasks, err := tf.contestRepo.ReadTasks(context.Background(), contestID) + require.NoError(t, err) + require.Equal(t, expectedTasks, tasks) + }) + + t.Run("no tasks found", func(t *testing.T) { + contestID := int32(999) + + rows := sqlmock.NewRows([]string{ + "id", "problem_id", "contest_id", "position", "title", + "memory_limit", "time_limit", "created_at", "updated_at", + }) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readTasksQuery)). + WithArgs(contestID). + WillReturnRows(rows) + + tasks, err := tf.contestRepo.ReadTasks(context.Background(), contestID) + require.NoError(t, err) + require.Empty(t, tasks) + }) + + t.Run("database error", func(t *testing.T) { + contestID := int32(1) + expectedErr := sql.ErrConnDone + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readTasksQuery)). + WithArgs(contestID). + WillReturnError(expectedErr) + + tasks, err := tf.contestRepo.ReadTasks(context.Background(), contestID) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.ReadTasks") + require.Nil(t, tasks) + }) +} + +func TestContestRepository_ListContests(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("successful contests list retrieval", func(t *testing.T) { + filter := models.ContestsFilter{ + Page: 1, + PageSize: 2, + } + now := time.Now().UTC() + expectedContests := []*models.ContestsListItem{ + { + Id: 1, + Title: "Contest 1", + CreatedAt: now, + UpdatedAt: now, + }, + { + Id: 2, + Title: "Contest 2", + CreatedAt: now, + UpdatedAt: now, + }, + } + totalCount := int32(5) + + // Mock contests query + contestsRows := sqlmock.NewRows([]string{"id", "title", "created_at", "updated_at"}). + AddRow( + expectedContests[0].Id, expectedContests[0].Title, + expectedContests[0].CreatedAt, expectedContests[0].UpdatedAt, + ). + AddRow( + expectedContests[1].Id, expectedContests[1].Title, + expectedContests[1].CreatedAt, expectedContests[1].UpdatedAt, + ) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readContestsListQuery)). + WithArgs(filter.PageSize, filter.Offset()). + WillReturnRows(contestsRows) + + // Mock count query + countRows := sqlmock.NewRows([]string{"count"}).AddRow(totalCount) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countContestsQuery)). + WillReturnRows(countRows) + + result, err := tf.contestRepo.ListContests(context.Background(), filter) + require.NoError(t, err) + expectedResult := &models.ContestsList{ + Contests: expectedContests, + Pagination: models.Pagination{ + Total: models.Total(totalCount, filter.PageSize), + Page: filter.Page, + }, + } + require.Equal(t, expectedResult, result) + }) + + t.Run("no contests found", func(t *testing.T) { + filter := models.ContestsFilter{ + Page: 1, + PageSize: 2, + } + + // Mock empty contests query + contestsRows := sqlmock.NewRows([]string{"id", "title", "created_at", "updated_at"}) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readContestsListQuery)). + WithArgs(filter.PageSize, filter.Offset()). + WillReturnRows(contestsRows) + + // Mock count query + countRows := sqlmock.NewRows([]string{"count"}).AddRow(int32(0)) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countContestsQuery)). + WillReturnRows(countRows) + + result, err := tf.contestRepo.ListContests(context.Background(), filter) + require.NoError(t, err) + expectedResult := &models.ContestsList{ + Contests: []*models.ContestsListItem{}, + Pagination: models.Pagination{ + Total: models.Total(0, filter.PageSize), + Page: filter.Page, + }, + } + require.Equal(t, expectedResult, result) + }) + + t.Run("error in contests query", func(t *testing.T) { + filter := models.ContestsFilter{ + Page: 1, + PageSize: 2, + } + expectedErr := sql.ErrConnDone + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readContestsListQuery)). + WithArgs(filter.PageSize, filter.Offset()). + WillReturnError(expectedErr) + + result, err := tf.contestRepo.ListContests(context.Background(), filter) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.ReadTasks") + require.Nil(t, result) + }) + + t.Run("error in count query", func(t *testing.T) { + filter := models.ContestsFilter{ + Page: 1, + PageSize: 2, + } + expectedErr := sql.ErrConnDone + + // Mock successful contests query + contestsRows := sqlmock.NewRows([]string{"id", "title", "created_at", "updated_at"}) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readContestsListQuery)). + WithArgs(filter.PageSize, filter.Offset()). + WillReturnRows(contestsRows) + + // Mock failing count query + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countContestsQuery)). + WillReturnError(expectedErr) + + result, err := tf.contestRepo.ListContests(context.Background(), filter) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.ReadTasks") + require.Nil(t, result) + }) +} + +func TestContestRepository_ListContestParticipants(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("successful participants list retrieval", func(t *testing.T) { + filter := models.ParticipantsFilter{ + ContestId: 1, + Page: 1, + PageSize: 2, + } + now := time.Now().UTC() + expectedParticipants := []*models.ParticipantsListItem{ + { + Id: 1, + UserId: 101, + Name: "Participant 1", + CreatedAt: now, + UpdatedAt: now, + }, + { + Id: 2, + UserId: 102, + Name: "Participant 2", + CreatedAt: now, + UpdatedAt: now, + }, + } + totalCount := int32(5) + + // Mock participants query + participantsRows := sqlmock.NewRows([]string{"id", "user_id", "name", "created_at", "updated_at"}). + AddRow( + expectedParticipants[0].Id, expectedParticipants[0].UserId, expectedParticipants[0].Name, + expectedParticipants[0].CreatedAt, expectedParticipants[0].UpdatedAt, + ). + AddRow( + expectedParticipants[1].Id, expectedParticipants[1].UserId, expectedParticipants[1].Name, + expectedParticipants[1].CreatedAt, expectedParticipants[1].UpdatedAt, + ) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readParticipantsListQuery)). + WithArgs(filter.ContestId, filter.PageSize, filter.Offset()). + WillReturnRows(participantsRows) + + // Mock count query + countRows := sqlmock.NewRows([]string{"count"}).AddRow(totalCount) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countParticipantsQuery)). + WithArgs(filter.ContestId). + WillReturnRows(countRows) + + result, err := tf.contestRepo.ListParticipants(context.Background(), filter) + require.NoError(t, err) + expectedResult := &models.ParticipantsList{ + Participants: expectedParticipants, + Pagination: models.Pagination{ + Total: models.Total(totalCount, filter.PageSize), + Page: filter.Page, + }, + } + require.Equal(t, expectedResult, result) + }) + + t.Run("no participants found", func(t *testing.T) { + filter := models.ParticipantsFilter{ + ContestId: 999, + Page: 1, + PageSize: 2, + } + + // Mock empty participants query + participantsRows := sqlmock.NewRows([]string{"id", "user_id", "name", "created_at", "updated_at"}) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readParticipantsListQuery)). + WithArgs(filter.ContestId, filter.PageSize, filter.Offset()). + WillReturnRows(participantsRows) + + // Mock count query + countRows := sqlmock.NewRows([]string{"count"}).AddRow(int32(0)) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countParticipantsQuery)). + WithArgs(filter.ContestId). + WillReturnRows(countRows) + + result, err := tf.contestRepo.ListParticipants(context.Background(), filter) + require.NoError(t, err) + expectedResult := &models.ParticipantsList{ + Participants: []*models.ParticipantsListItem{}, + Pagination: models.Pagination{ + Total: models.Total(0, filter.PageSize), + Page: filter.Page, + }, + } + require.Equal(t, expectedResult, result) + }) + + t.Run("page size limited to 1 when exceeding 20", func(t *testing.T) { + filter := models.ParticipantsFilter{ + ContestId: 1, + Page: 1, + PageSize: 25, // Exceeds limit + } + now := time.Now().UTC() + expectedParticipants := []*models.ParticipantsListItem{ + { + Id: 1, + UserId: 101, + Name: "Participant 1", + CreatedAt: now, + UpdatedAt: now, + }, + } + totalCount := int32(5) + + // Mock participants query with limited page size (1) + participantsRows := sqlmock.NewRows([]string{"id", "user_id", "name", "created_at", "updated_at"}). + AddRow( + expectedParticipants[0].Id, expectedParticipants[0].UserId, expectedParticipants[0].Name, + expectedParticipants[0].CreatedAt, expectedParticipants[0].UpdatedAt, + ) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readParticipantsListQuery)). + WithArgs(filter.ContestId, int32(1), filter.Offset()). // PageSize limited to 1 + WillReturnRows(participantsRows) + + // Mock count query + countRows := sqlmock.NewRows([]string{"count"}).AddRow(totalCount) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countParticipantsQuery)). + WithArgs(filter.ContestId). + WillReturnRows(countRows) + + result, err := tf.contestRepo.ListParticipants(context.Background(), filter) + require.NoError(t, err) + expectedResult := &models.ParticipantsList{ + Participants: expectedParticipants, + Pagination: models.Pagination{ + Total: models.Total(totalCount, 1), // PageSize limited to 1 + Page: filter.Page, + }, + } + require.Equal(t, expectedResult, result) + }) + + t.Run("error in participants query", func(t *testing.T) { + filter := models.ParticipantsFilter{ + ContestId: 1, + Page: 1, + PageSize: 2, + } + expectedErr := sql.ErrConnDone + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readParticipantsListQuery)). + WithArgs(filter.ContestId, filter.PageSize, filter.Offset()). + WillReturnError(expectedErr) + + result, err := tf.contestRepo.ListParticipants(context.Background(), filter) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.ReadParticipants") + require.Nil(t, result) + }) + + t.Run("error in count query", func(t *testing.T) { + filter := models.ParticipantsFilter{ + ContestId: 1, + Page: 1, + PageSize: 2, + } + expectedErr := sql.ErrConnDone + + // Mock successful participants query + participantsRows := sqlmock.NewRows([]string{"id", "user_id", "name", "created_at", "updated_at"}) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readParticipantsListQuery)). + WithArgs(filter.ContestId, filter.PageSize, filter.Offset()). + WillReturnRows(participantsRows) + + // Mock failing count query + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countParticipantsQuery)). + WithArgs(filter.ContestId). + WillReturnError(expectedErr) + + result, err := tf.contestRepo.ListParticipants(context.Background(), filter) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.ReadParticipants") + require.Nil(t, result) + }) +} + +func TestContestRepository_UpdateContest(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("successful contest update", func(t *testing.T) { + contestID := int32(1) + update := models.ContestUpdate{ + Title: sp("Updated Contest"), + } + result := sqlmock.NewResult(1, 1) + + tf.mock.ExpectExec(tf.sqlxDB.Rebind(updateContestQuery)). + WithArgs(update.Title, contestID). + WillReturnResult(result) + + err := tf.contestRepo.UpdateContest(context.Background(), contestID, update) + require.NoError(t, err) + }) + + t.Run("database error", func(t *testing.T) { + contestID := int32(1) + update := models.ContestUpdate{ + Title: sp("Updated Contest"), + } + expectedErr := sql.ErrConnDone + + tf.mock.ExpectExec(tf.sqlxDB.Rebind(updateContestQuery)). + WithArgs(update.Title, contestID). + WillReturnError(expectedErr) + + err := tf.contestRepo.UpdateContest(context.Background(), contestID, update) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.UpdateContest") + }) +} + +func TestContestRepository_UpdateParticipant(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("successful participant update", func(t *testing.T) { + participantID := int32(1) + update := models.ParticipantUpdate{ + Name: sp("Updated Name"), + } + result := sqlmock.NewResult(1, 1) + + tf.mock.ExpectExec(tf.sqlxDB.Rebind(updateParticipantQuery)). + WithArgs(update.Name, participantID). + WillReturnResult(result) + + err := tf.contestRepo.UpdateParticipant(context.Background(), participantID, update) + require.NoError(t, err) + }) + + t.Run("database error", func(t *testing.T) { + participantID := int32(1) + update := models.ParticipantUpdate{ + Name: sp("Updated Name"), + } + expectedErr := sql.ErrConnDone + + tf.mock.ExpectExec(tf.sqlxDB.Rebind(updateParticipantQuery)). + WithArgs(update.Name, participantID). + WillReturnError(expectedErr) + + err := tf.contestRepo.UpdateParticipant(context.Background(), participantID, update) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.UpdateParticipant") + }) +} + +func TestContestRepository_ReadSolution(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("successful solution read", func(t *testing.T) { + solutionID := int32(1) + now := time.Now().UTC() + expectedSolution := &models.Solution{ + Id: solutionID, + TaskId: 10, + ParticipantId: 20, + Language: 228, // FIXME: use constant + Penalty: 0, + Solution: "func main() {}", + CreatedAt: now, + UpdatedAt: now, + } + + rows := sqlmock.NewRows([]string{ + "id", "task_id", "participant_id", "language", "penalty", "solution", "created_at", "updated_at", + }). + AddRow( + expectedSolution.Id, expectedSolution.TaskId, expectedSolution.ParticipantId, + expectedSolution.Language, expectedSolution.Penalty, expectedSolution.Solution, + expectedSolution.CreatedAt, expectedSolution.UpdatedAt, + ) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readSolutionQuery)). + WithArgs(solutionID). + WillReturnRows(rows) + + solution, err := tf.contestRepo.ReadSolution(context.Background(), solutionID) + require.NoError(t, err) + require.Equal(t, expectedSolution, solution) + }) + + t.Run("solution not found", func(t *testing.T) { + solutionID := int32(999) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readSolutionQuery)). + WithArgs(solutionID). + WillReturnError(sql.ErrNoRows) + + solution, err := tf.contestRepo.ReadSolution(context.Background(), solutionID) + require.Error(t, err) + require.ErrorIs(t, err, pkg.ErrNotFound) + require.Nil(t, solution) + }) + + t.Run("database error", func(t *testing.T) { + solutionID := int32(1) + expectedErr := sql.ErrConnDone + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readSolutionQuery)). + WithArgs(solutionID). + WillReturnError(expectedErr) + + solution, err := tf.contestRepo.ReadSolution(context.Background(), solutionID) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.ReadSolution") + require.Nil(t, solution) + }) +} + +func TestContestRepository_CreateSolution(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("successful solution creation", func(t *testing.T) { + creation := &models.SolutionCreation{ + TaskId: 10, + ParticipantId: 20, + Language: 228, // FIXME: use constant + Penalty: 0, + Solution: "func main() {}", + } + expectedID := int32(1) + + rows := sqlmock.NewRows([]string{"id"}).AddRow(expectedID) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(createSolutionQuery)). + WithArgs(creation.TaskId, creation.ParticipantId, creation.Language, creation.Penalty, creation.Solution). + WillReturnRows(rows) + + id, err := tf.contestRepo.CreateSolution(context.Background(), creation) + require.NoError(t, err) + require.Equal(t, expectedID, id) + }) + + t.Run("database error on query", func(t *testing.T) { + creation := &models.SolutionCreation{ + TaskId: 10, + ParticipantId: 20, + Language: 228, // FIXME: use constant + Penalty: 0, + Solution: "func main() {}", + } + expectedErr := sql.ErrConnDone + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(createSolutionQuery)). + WithArgs(creation.TaskId, creation.ParticipantId, creation.Language, creation.Penalty, creation.Solution). + WillReturnError(expectedErr) + + id, err := tf.contestRepo.CreateSolution(context.Background(), creation) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.CreateSolution") + require.Equal(t, int32(0), id) + }) + + t.Run("database error on scan", func(t *testing.T) { + creation := &models.SolutionCreation{ + TaskId: 10, + ParticipantId: 20, + Language: 228, // FIXME: use constant + Penalty: 0, + Solution: "func main() {}", + } + rows := sqlmock.NewRows([]string{"id"}).AddRow(nil) // Invalid row to cause scan error + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(createSolutionQuery)). + WithArgs(creation.TaskId, creation.ParticipantId, creation.Language, creation.Penalty, creation.Solution). + WillReturnRows(rows) + + id, err := tf.contestRepo.CreateSolution(context.Background(), creation) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.CreateSolution") + require.Equal(t, int32(0), id) + }) +} + +func TestContestRepository_ListSolutions(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + // Define columns once for all test cases + solutionColumns := []string{ + "id", "participant_id", "participant_name", "state", "score", "penalty", + "time_stat", "memory_stat", "language", "task_id", "task_position", + "task_title", "contest_id", "contest_title", "updated_at", "created_at", + } + + t.Run("successful solutions list retrieval with filters", func(t *testing.T) { + filter := models.SolutionsFilter{ + ContestId: i32p(int32(1)), + ParticipantId: i32p(int32(101)), + TaskId: i32p(int32(10)), + Language: i32p(int32(228)), + State: i32p(int32(1)), // Assuming state is an int32 + Page: 1, + PageSize: 2, + Order: i32p(int32(-1)), // DESC order + } + now := time.Now().UTC() + expectedSolutions := []*models.SolutionsListItem{ + { + Id: 1, + ParticipantId: 101, + ParticipantName: "Participant 1", + State: 1, + Score: 100, + Penalty: 0, + TimeStat: 500, + MemoryStat: 1024, + Language: 228, + TaskId: 10, + TaskPosition: 1, + TaskTitle: "Task 1", + ContestId: 1, + ContestTitle: "Contest 1", + UpdatedAt: now, + CreatedAt: now, + }, + { + Id: 2, + ParticipantId: 101, + ParticipantName: "Participant 1", + State: 1, + Score: 90, + Penalty: 0, + TimeStat: 600, + MemoryStat: 2048, + Language: 228, + TaskId: 10, + TaskPosition: 1, + TaskTitle: "Task 1", + ContestId: 1, + ContestTitle: "Contest 1", + UpdatedAt: now, + CreatedAt: now, + }, + } + totalCount := int32(5) + + // Build queries using the function + baseQb, countQb := buildListSolutionsQueries(filter) + countQuery, countArgs, err := countQb.ToSql() + require.NoError(t, err) + baseQuery, baseArgs, err := baseQb.ToSql() + require.NoError(t, err) + + // Mock count query + countRows := sqlmock.NewRows([]string{"count"}).AddRow(totalCount) + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countQuery)). + WithArgs(i2v(countArgs)...). + WillReturnRows(countRows) + + // Mock solutions query + solutionsRows := sqlmock.NewRows(solutionColumns) + for _, sol := range expectedSolutions { + solutionsRows.AddRow( + sol.Id, sol.ParticipantId, sol.ParticipantName, sol.State, sol.Score, sol.Penalty, + sol.TimeStat, sol.MemoryStat, sol.Language, sol.TaskId, sol.TaskPosition, + sol.TaskTitle, sol.ContestId, sol.ContestTitle, sol.UpdatedAt, sol.CreatedAt, + ) + } + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(baseQuery)). + WithArgs(i2v(baseArgs)...). + WillReturnRows(solutionsRows) + + result, err := tf.contestRepo.ListSolutions(context.Background(), filter) + require.NoError(t, err) + expectedResult := &models.SolutionsList{ + Solutions: expectedSolutions, + Pagination: models.Pagination{ + Total: models.Total(totalCount, filter.PageSize), + Page: filter.Page, + }, + } + require.Equal(t, expectedResult, result) + }) + + t.Run("no solutions found", func(t *testing.T) { + filter := models.SolutionsFilter{ + ContestId: i32p(int32(999)), + Page: 1, + PageSize: 2, + } + + // Build queries using the function + baseQb, countQb := buildListSolutionsQueries(filter) + countQuery, countArgs, err := countQb.ToSql() + require.NoError(t, err) + baseQuery, baseArgs, err := baseQb.ToSql() + require.NoError(t, err) + + // Mock count query + countRows := sqlmock.NewRows([]string{"count"}).AddRow(int32(0)) + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countQuery)). + WithArgs(i2v(countArgs)...). + WillReturnRows(countRows) + + // Mock solutions query + solutionsRows := sqlmock.NewRows(solutionColumns) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(baseQuery)). + WithArgs(i2v(baseArgs)...). + WillReturnRows(solutionsRows) + + result, err := tf.contestRepo.ListSolutions(context.Background(), filter) + require.NoError(t, err) + expectedResult := &models.SolutionsList{ + Solutions: []*models.SolutionsListItem{}, + Pagination: models.Pagination{ + Total: models.Total(0, filter.PageSize), + Page: filter.Page, + }, + } + require.Equal(t, expectedResult, result) + }) + + t.Run("error in count query", func(t *testing.T) { + filter := models.SolutionsFilter{ + ContestId: i32p(int32(1)), + Page: 1, + PageSize: 2, + } + expectedErr := sql.ErrConnDone + + // Build queries using the function + _, countQb := buildListSolutionsQueries(filter) + countQuery, countArgs, err := countQb.ToSql() + require.NoError(t, err) + + // Mock count query with error + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countQuery)). + WithArgs(i2v(countArgs)...). + WillReturnError(expectedErr) + + result, err := tf.contestRepo.ListSolutions(context.Background(), filter) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.ListSolutions") + require.Nil(t, result) + }) + + t.Run("error in solutions query", func(t *testing.T) { + filter := models.SolutionsFilter{ + ContestId: i32p(int32(1)), + Page: 1, + PageSize: 2, + } + totalCount := int32(5) + expectedErr := sql.ErrConnDone + + // Build queries using the function + baseQb, countQb := buildListSolutionsQueries(filter) + countQuery, countArgs, err := countQb.ToSql() + require.NoError(t, err) + baseQuery, baseArgs, err := baseQb.ToSql() + require.NoError(t, err) + + // Mock count query + countRows := sqlmock.NewRows([]string{"count"}).AddRow(totalCount) + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countQuery)). + WithArgs(i2v(countArgs)...). + WillReturnRows(countRows) + + // Mock solutions query with error + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(baseQuery)). + WithArgs(i2v(baseArgs)...). + WillReturnError(expectedErr) + + result, err := tf.contestRepo.ListSolutions(context.Background(), filter) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.ListSolutions") + require.Nil(t, result) + }) + + t.Run("error in struct scan", func(t *testing.T) { + filter := models.SolutionsFilter{ + ContestId: i32p(int32(1)), + Page: 1, + PageSize: 2, + } + totalCount := int32(1) + + // Build queries using the function + baseQb, countQb := buildListSolutionsQueries(filter) + countQuery, countArgs, err := countQb.ToSql() + require.NoError(t, err) + baseQuery, baseArgs, err := baseQb.ToSql() + require.NoError(t, err) + + // Mock count query + countRows := sqlmock.NewRows([]string{"count"}).AddRow(totalCount) + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(countQuery)). + WithArgs(i2v(countArgs)...). + WillReturnRows(countRows) + + // Mock solutions query with invalid data to cause scan error + solutionsRows := sqlmock.NewRows(solutionColumns).AddRow( + nil, 101, "Participant 1", 1, 100, 0, // Invalid id (nil) + 500, 1024, 228, 10, 1, "Task 1", 1, "Contest 1", + time.Now().UTC(), time.Now().UTC(), + ) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(baseQuery)). + WithArgs(i2v(baseArgs)...). + WillReturnRows(solutionsRows) + + result, err := tf.contestRepo.ListSolutions(context.Background(), filter) + require.Error(t, err) + require.Contains(t, err.Error(), "ContestRepository.ListSolutions") + require.Nil(t, result) + }) +} + +func TestContestRepository_ReadTask(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("valid task read", func(t *testing.T) { + taskID := int32(1) + now := time.Now().UTC() + expectedTask := &models.Task{ + Id: taskID, + Position: 1, + Title: "Task title", + TimeLimit: 1000, + MemoryLimit: 256, + ProblemId: 1, + ContestId: 1, + LegendHtml: "

Legend

", + InputFormatHtml: "

Input

", + OutputFormatHtml: "

Output

", + NotesHtml: "

Notes

", + ScoringHtml: "

Scoring

", + CreatedAt: now, + UpdatedAt: now, + } + + rows := sqlmock.NewRows([]string{ + "id", + "position", + "title", + "time_limit", + "memory_limit", + "problem_id", + "contest_id", + "legend_html", + "input_format_html", + "output_format_html", + "notes_html", + "scoring_html", + "created_at", + "updated_at", + }).AddRow( + expectedTask.Id, + expectedTask.Position, + expectedTask.Title, + expectedTask.TimeLimit, + expectedTask.MemoryLimit, + expectedTask.ProblemId, + expectedTask.ContestId, + expectedTask.LegendHtml, + expectedTask.InputFormatHtml, + expectedTask.OutputFormatHtml, + expectedTask.NotesHtml, + expectedTask.ScoringHtml, + expectedTask.CreatedAt, + expectedTask.UpdatedAt, + ) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readTaskQuery)). + WithArgs(taskID). + WillReturnRows(rows) + + task, err := tf.contestRepo.ReadTask(context.Background(), taskID) + require.NoError(t, err) + require.Equal(t, expectedTask, task) + }) + + t.Run("task not found", func(t *testing.T) { + taskID := int32(999) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readTaskQuery)). + WithArgs(taskID). + WillReturnError(sql.ErrNoRows) + + task, err := tf.contestRepo.ReadTask(context.Background(), taskID) + require.Error(t, err) + require.ErrorIs(t, err, pkg.ErrNotFound) + require.Nil(t, task) + }) +} + +func TestContestRepository_ReadMonitor(t *testing.T) { + tf := newContestTestFixture(t) + defer tf.cleanup() + + t.Run("valid monitor read", func(t *testing.T) { + contestID := int32(1) + now := time.Now().UTC() + penalty := int32(20) + + // Expected monitor data + expectedMonitor := &models.Monitor{ + Summary: []*models.ProblemStatSummary{ + { + Id: 1, + Position: 1, + Total: 10, + Success: 5, + }, + { + Id: 2, + Position: 2, + Total: 8, + Success: 3, + }, + }, + Participants: []*models.ParticipantsStat{ + { + Id: 1, + Name: "Participant 1", + SolvedInTotal: 2, + PenaltyInTotal: 40, + Solutions: []*models.SolutionsListItem{ + { + Id: 101, + ParticipantId: 1, + ParticipantName: "Participant 1", + State: 5, + Score: 100, + Penalty: 0, + TimeStat: 1000, + MemoryStat: 256, + Language: 228, // FIXME: use constants + TaskId: 1, + TaskPosition: 1, + TaskTitle: "Task 1", + ContestId: 1, + ContestTitle: "Contest 1", + CreatedAt: now, + UpdatedAt: now, + }, + }, + }, + }, + } + + // Mock statistics query + statsRows := sqlmock.NewRows([]string{"task_id", "position", "total", "success"}). + AddRow(1, 1, 10, 5). + AddRow(2, 2, 8, 3) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readStatisticsQuery)). + WithArgs(contestID). + WillReturnRows(statsRows) + + // Mock solutions query + solutionsRows := sqlmock.NewRows([]string{ + "id", + "participant_id", + "participant_name", + "state", + "score", + "penalty", + "time_stat", + "memory_stat", + "language", + "task_id", + "task_position", + "task_title", + "contest_id", + "contest_title", + "updated_at", + "created_at", + }).AddRow( + 101, + 1, + "Participant 1", + 5, + 100, + 0, + 1000, + 256, + 228, + 1, + 1, + "Task 1", + 1, + "Contest 1", + now, + now, + ) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(solutionsQuery)). + WithArgs(contestID). + WillReturnRows(solutionsRows) + + // Mock participants query + participantsRows := sqlmock.NewRows([]string{ + "id", + "name", + "solved_in_total", + "penalty_in_total", + }).AddRow(1, "Participant 1", 2, 40) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(participantsQuery)). + WithArgs(contestID, penalty, contestID). + WillReturnRows(participantsRows) + + // Execute the function + monitor, err := tf.contestRepo.ReadMonitor(context.Background(), contestID) + require.NoError(t, err) + require.Equal(t, expectedMonitor, monitor) + }) + + t.Run("contest not found", func(t *testing.T) { + contestID := int32(999) + + // Mock statistics query to return no rows + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readStatisticsQuery)). + WithArgs(contestID). + WillReturnRows(sqlmock.NewRows([]string{"task_id", "position", "total", "success"})) + + // Mock solutions query + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(solutionsQuery)). + WithArgs(contestID). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "participant_id", "participant_name", "state", "score", + "penalty", "time_stat", "memory_stat", "language", "task_id", + "task_position", "task_title", "contest_id", "contest_title", + "updated_at", "created_at", + })) + + // Mock participants query + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(participantsQuery)). + WithArgs(contestID, int32(20), contestID). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "solved_in_total", "penalty_in_total", + })) + + // Execute the function + monitor, err := tf.contestRepo.ReadMonitor(context.Background(), contestID) + require.NoError(t, err) + require.Empty(t, monitor.Summary) + require.Empty(t, monitor.Participants) + }) + + t.Run("statistics query error", func(t *testing.T) { + contestID := int32(1) + dbError := errors.New("database connection failed") + + // Mock statistics query to return an error + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readStatisticsQuery)). + WithArgs(contestID). + WillReturnError(dbError) + + // Execute the function + monitor, err := tf.contestRepo.ReadMonitor(context.Background(), contestID) + require.Error(t, err) + require.Contains(t, err.Error(), dbError.Error()) + require.Nil(t, monitor) + }) + + t.Run("solutions query error", func(t *testing.T) { + contestID := int32(1) + dbError := errors.New("solutions query failed") + + // Mock statistics query + statsRows := sqlmock.NewRows([]string{"task_id", "position", "total", "success"}). + AddRow(1, 1, 10, 5) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readStatisticsQuery)). + WithArgs(contestID). + WillReturnRows(statsRows) + + // Mock solutions query to return an error + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(solutionsQuery)). + WithArgs(contestID). + WillReturnError(dbError) + + // Execute the function + monitor, err := tf.contestRepo.ReadMonitor(context.Background(), contestID) + require.Error(t, err) + require.Contains(t, err.Error(), dbError.Error()) + require.Nil(t, monitor) + }) + + t.Run("participants query error", func(t *testing.T) { + contestID := int32(1) + dbError := errors.New("participants query failed") + + // Mock statistics query + statsRows := sqlmock.NewRows([]string{"task_id", "position", "total", "success"}). + AddRow(1, 1, 10, 5) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readStatisticsQuery)). + WithArgs(contestID). + WillReturnRows(statsRows) + + // Mock solutions query + solutionsRows := sqlmock.NewRows([]string{ + "id", "participant_id", "participant_name", "state", "score", + "penalty", "time_stat", "memory_stat", "language", "task_id", + "task_position", "task_title", "contest_id", "contest_title", + "updated_at", "created_at", + }) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(solutionsQuery)). + WithArgs(contestID). + WillReturnRows(solutionsRows) + + // Mock participants query to return an error + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(participantsQuery)). + WithArgs(contestID, int32(20), contestID). + WillReturnError(dbError) + + // Execute the function + monitor, err := tf.contestRepo.ReadMonitor(context.Background(), contestID) + require.Error(t, err) + require.Contains(t, err.Error(), dbError.Error()) + require.Nil(t, monitor) + }) +} + +// Helper function to create int32 pointers +func i32p(i int32) *int32 { + return &i +} + +// Helper function to create string pointers +func sp(s string) *string { + return &s +} + +// Helper function to convert slice of interface{} to slice of driver.Value +func i2v(s []interface{}) []driver.Value { + r := make([]driver.Value, len(s)) + for i, v := range s { + r[i] = v + } + return r +} diff --git a/internal/tester/repository/pg_problems_repository_test.go b/internal/tester/repository/pg_problems_repository_test.go index 76d67cf..7fb68ea 100644 --- a/internal/tester/repository/pg_problems_repository_test.go +++ b/internal/tester/repository/pg_problems_repository_test.go @@ -2,57 +2,153 @@ package repository import ( "context" + "database/sql" + "git.sch9.ru/new_gate/ms-tester/internal/models" "github.com/DATA-DOG/go-sqlmock" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" - "go.uber.org/zap" "testing" ) -func TestProblemRepository_CreateProblem(t *testing.T) { - t.Parallel() +type problemTestFixture struct { + db *sql.DB + sqlxDB *sqlx.DB + mock sqlmock.Sqlmock + problemRepo *ProblemRepository +} +func newProblemTestFixture(t *testing.T) *problemTestFixture { db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) require.NoError(t, err) - defer db.Close() sqlxDB := sqlx.NewDb(db, "sqlmock") - defer sqlxDB.Close() + repo := NewProblemRepository(sqlxDB) - problemRepo := NewProblemRepository(sqlxDB, zap.NewNop()) + return &problemTestFixture{ + db: db, + sqlxDB: sqlxDB, + mock: mock, + problemRepo: repo, + } +} + +// cleanup closes database connections +func (tf *problemTestFixture) cleanup() { + tf.db.Close() + tf.sqlxDB.Close() +} + +func TestProblemRepository_CreateProblem(t *testing.T) { + tf := newProblemTestFixture(t) + defer tf.cleanup() t.Run("valid problem creation", func(t *testing.T) { - title := "Problem title" - + title := "Test Problem" rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - mock.ExpectQuery(sqlxDB.Rebind(createProblemQuery)).WithArgs(title).WillReturnRows(rows) + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(createProblemQuery)). + WithArgs(title). + WillReturnRows(rows) - id, err := problemRepo.CreateProblem(context.Background(), title) + id, err := tf.problemRepo.CreateProblem(context.Background(), tf.problemRepo.DB(), title) require.NoError(t, err) require.Equal(t, int32(1), id) }) } +func TestProblemRepository_ReadProblemById(t *testing.T) { + tf := newProblemTestFixture(t) + defer tf.cleanup() + + t.Run("valid problem read", func(t *testing.T) { + id := int32(1) + rows := sqlmock.NewRows([]string{"id", "title"}). + AddRow(1, "Test Problem") + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readProblemQuery)). + WithArgs(id). + WillReturnRows(rows) + + problem, err := tf.problemRepo.ReadProblemById(context.Background(), tf.problemRepo.DB(), id) + require.NoError(t, err) + require.NotNil(t, problem) + require.Equal(t, int32(1), problem.Id) + require.Equal(t, "Test Problem", problem.Title) + }) +} + func TestProblemRepository_DeleteProblem(t *testing.T) { - t.Parallel() - - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - require.NoError(t, err) - defer db.Close() - - sqlxDB := sqlx.NewDb(db, "sqlmock") - defer sqlxDB.Close() - - problemRepo := NewProblemRepository(sqlxDB, zap.NewNop()) + tf := newProblemTestFixture(t) + defer tf.cleanup() t.Run("valid problem deletion", func(t *testing.T) { id := int32(1) rows := sqlmock.NewResult(1, 1) - mock.ExpectExec(sqlxDB.Rebind(deleteProblemQuery)).WithArgs(id).WillReturnResult(rows) + tf.mock.ExpectExec(tf.sqlxDB.Rebind(deleteProblemQuery)). + WithArgs(id). + WillReturnResult(rows) - err = problemRepo.DeleteProblem(context.Background(), id) + err := tf.problemRepo.DeleteProblem(context.Background(), tf.problemRepo.DB(), id) + require.NoError(t, err) + }) +} + +func TestProblemRepository_ListProblems(t *testing.T) { + tf := newProblemTestFixture(t) + defer tf.cleanup() + + t.Run("valid problems list", func(t *testing.T) { + filter := models.ProblemsFilter{ + Page: 1, + PageSize: 10, + } + listRows := sqlmock.NewRows([]string{"id", "title", "solved_count"}). + AddRow(1, "Problem 1", 5). + AddRow(2, "Problem 2", 3) + countRows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(ListProblemsQuery)). + WithArgs(filter.PageSize, filter.Offset()). + WillReturnRows(listRows) + + tf.mock.ExpectQuery(tf.sqlxDB.Rebind(CountProblemsQuery)). + WillReturnRows(countRows) + + result, err := tf.problemRepo.ListProblems(context.Background(), tf.problemRepo.DB(), filter) + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result.Problems, 2) + require.Equal(t, int32(1), result.Pagination.Page) + require.Equal(t, int32(1), result.Pagination.Total) + }) +} + +func TestProblemRepository_UpdateProblem(t *testing.T) { + tf := newProblemTestFixture(t) + defer tf.cleanup() + + t.Run("valid problem update", func(t *testing.T) { + id := int32(1) + problemUpdate := models.ProblemUpdate{ + Title: sp("Updated Title"), + TimeLimit: i32p(1000), + MemoryLimit: i32p(256), + } + rows := sqlmock.NewResult(1, 1) + + tf.mock.ExpectExec(tf.sqlxDB.Rebind(UpdateProblemQuery)). + WithArgs( + problemUpdate.Title, + problemUpdate.TimeLimit, + problemUpdate.MemoryLimit, + nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, + id, + ). + WillReturnResult(rows) + + err := tf.problemRepo.UpdateProblem(context.Background(), tf.problemRepo.DB(), id, problemUpdate) require.NoError(t, err) }) } diff --git a/proto b/proto index 1fbee7b..f00483d 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 1fbee7ba29c358c76d1c835ac6999ce9e1b59ee9 +Subproject commit f00483d24a53a243734c793fc24e02d52d39fdab -- 2.45.3