feat: merge auth&tester
This commit is contained in:
parent
0a2dea6c23
commit
441af4c6a2
72 changed files with 4910 additions and 2378 deletions
15
internal/problems/delivery.go
Normal file
15
internal/problems/delivery.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package problems
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ProblemsHandlers interface {
|
||||
ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error
|
||||
CreateProblem(c *fiber.Ctx) error
|
||||
DeleteProblem(c *fiber.Ctx, id int32) error
|
||||
GetProblem(c *fiber.Ctx, id int32) error
|
||||
UpdateProblem(c *fiber.Ctx, id int32) error
|
||||
UploadProblem(c *fiber.Ctx, id int32) error
|
||||
}
|
246
internal/problems/delivery/rest/handlers.go
Normal file
246
internal/problems/delivery/rest/handlers.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
problemsUC problems.UseCase
|
||||
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
const (
|
||||
sessionKey = "session"
|
||||
)
|
||||
|
||||
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
|
||||
const op = "sessionFromCtx"
|
||||
|
||||
session, ok := ctx.Value(sessionKey).(*models.Session)
|
||||
if !ok {
|
||||
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func NewHandlers(problemsUC problems.UseCase) *Handlers {
|
||||
return &Handlers{
|
||||
problemsUC: problemsUC,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
problemsList, err := h.problemsUC.ListProblems(c.Context(), models.ProblemsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListProblemsResponse{
|
||||
Problems: make([]testerv1.ProblemsListItem, len(problemsList.Problems)),
|
||||
Pagination: PaginationDTO(problemsList.Pagination),
|
||||
}
|
||||
|
||||
for i, problem := range problemsList.Problems {
|
||||
resp.Problems[i] = ProblemsListItemDTO(*problem)
|
||||
}
|
||||
return c.JSON(resp)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) CreateProblem(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
id, err := h.problemsUC.CreateProblem(c.Context(), "Название задачи")
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateProblemResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteProblem(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
err := h.problemsUC.DeleteProblem(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetProblem(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
problem, err := h.problemsUC.GetProblemById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(
|
||||
testerv1.GetProblemResponse{Problem: *ProblemDTO(problem)},
|
||||
)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UpdateProblem(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
var req testerv1.UpdateProblemRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.problemsUC.UpdateProblem(c.Context(), id, &models.ProblemUpdate{
|
||||
Title: req.Title,
|
||||
MemoryLimit: req.MemoryLimit,
|
||||
TimeLimit: req.TimeLimit,
|
||||
|
||||
Legend: req.Legend,
|
||||
InputFormat: req.InputFormat,
|
||||
OutputFormat: req.OutputFormat,
|
||||
Notes: req.Notes,
|
||||
Scoring: req.Scoring,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UploadProblem(c *fiber.Ctx, id int32) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
var req testerv1.UploadProblemRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := req.Archive.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = h.problemsUC.UploadProblem(c.Context(), id, data); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func PaginationDTO(p models.Pagination) testerv1.Pagination {
|
||||
return testerv1.Pagination{
|
||||
Page: p.Page,
|
||||
Total: p.Total,
|
||||
}
|
||||
}
|
||||
|
||||
func ProblemsListItemDTO(p models.ProblemsListItem) testerv1.ProblemsListItem {
|
||||
return testerv1.ProblemsListItem{
|
||||
Id: p.Id,
|
||||
Title: p.Title,
|
||||
MemoryLimit: p.MemoryLimit,
|
||||
TimeLimit: p.TimeLimit,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
SolvedCount: p.SolvedCount,
|
||||
}
|
||||
}
|
||||
|
||||
func ProblemDTO(p *models.Problem) *testerv1.Problem {
|
||||
return &testerv1.Problem{
|
||||
Id: p.Id,
|
||||
Title: p.Title,
|
||||
TimeLimit: p.TimeLimit,
|
||||
MemoryLimit: p.MemoryLimit,
|
||||
|
||||
Legend: p.Legend,
|
||||
InputFormat: p.InputFormat,
|
||||
OutputFormat: p.OutputFormat,
|
||||
Notes: p.Notes,
|
||||
Scoring: p.Scoring,
|
||||
|
||||
LegendHtml: p.LegendHtml,
|
||||
InputFormatHtml: p.InputFormatHtml,
|
||||
OutputFormatHtml: p.OutputFormatHtml,
|
||||
NotesHtml: p.NotesHtml,
|
||||
ScoringHtml: p.ScoringHtml,
|
||||
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
32
internal/problems/pg_repository.go
Normal file
32
internal/problems/pg_repository.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package problems
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
Rebind(query string) string
|
||||
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
|
||||
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
|
||||
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
|
||||
}
|
||||
|
||||
type Tx interface {
|
||||
Querier
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
BeginTx(ctx context.Context) (Tx, error)
|
||||
DB() Querier
|
||||
CreateProblem(ctx context.Context, q Querier, title string) (int32, error)
|
||||
GetProblemById(ctx context.Context, q Querier, id int32) (*models.Problem, error)
|
||||
DeleteProblem(ctx context.Context, q Querier, id int32) error
|
||||
ListProblems(ctx context.Context, q Querier, filter models.ProblemsFilter) (*models.ProblemsList, error)
|
||||
UpdateProblem(ctx context.Context, q Querier, id int32, heading *models.ProblemUpdate) error
|
||||
}
|
175
internal/problems/repository/pg_repository.go
Normal file
175
internal/problems/repository/pg_repository.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
_db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *sqlx.DB) *Repository {
|
||||
return &Repository{
|
||||
_db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) BeginTx(ctx context.Context) (problems.Tx, error) {
|
||||
tx, err := r._db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (r *Repository) DB() problems.Querier {
|
||||
return r._db
|
||||
}
|
||||
|
||||
const CreateProblemQuery = "INSERT INTO problems (title) VALUES ($1) RETURNING id"
|
||||
|
||||
func (r *Repository) CreateProblem(ctx context.Context, q problems.Querier, title string) (int32, error) {
|
||||
const op = "Repository.CreateProblem"
|
||||
|
||||
rows, err := q.QueryxContext(ctx, CreateProblemQuery, title)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const GetProblemByIdQuery = "SELECT * from problems WHERE id=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetProblemById(ctx context.Context, q problems.Querier, id int32) (*models.Problem, error) {
|
||||
const op = "Repository.ReadProblemById"
|
||||
|
||||
var problem models.Problem
|
||||
err := q.GetContext(ctx, &problem, GetProblemByIdQuery, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &problem, nil
|
||||
}
|
||||
|
||||
const DeleteProblemQuery = "DELETE FROM problems WHERE id=$1"
|
||||
|
||||
func (r *Repository) DeleteProblem(ctx context.Context, q problems.Querier, id int32) error {
|
||||
const op = "Repository.DeleteProblem"
|
||||
|
||||
_, err := q.ExecContext(ctx, DeleteProblemQuery, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ListProblemsQuery = `SELECT p.id,
|
||||
p.title,
|
||||
p.memory_limit,
|
||||
p.time_limit,
|
||||
p.created_at,
|
||||
p.updated_at,
|
||||
COALESCE(solved_count, 0) AS solved_count
|
||||
FROM problems p
|
||||
LEFT JOIN (SELECT t.problem_id,
|
||||
COUNT(DISTINCT s.participant_id) AS solved_count
|
||||
FROM solutions s
|
||||
JOIN tasks t ON s.task_id = t.id
|
||||
WHERE s.state = 5
|
||||
GROUP BY t.problem_id) sol ON p.id = sol.problem_id
|
||||
LIMIT $1 OFFSET $2`
|
||||
CountProblemsQuery = "SELECT COUNT(*) FROM problems"
|
||||
)
|
||||
|
||||
func (r *Repository) ListProblems(ctx context.Context, q problems.Querier, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
const op = "ContestRepository.ListProblems"
|
||||
|
||||
var list []*models.ProblemsListItem
|
||||
err := q.SelectContext(ctx, &list, ListProblemsQuery, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var count int32
|
||||
err = q.GetContext(ctx, &count, CountProblemsQuery)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ProblemsList{
|
||||
Problems: list,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
UpdateProblemQuery = `UPDATE problems
|
||||
SET title = COALESCE($2, title),
|
||||
time_limit = COALESCE($3, time_limit),
|
||||
memory_limit = COALESCE($4, memory_limit),
|
||||
|
||||
legend = COALESCE($5, legend),
|
||||
input_format = COALESCE($6, input_format),
|
||||
output_format = COALESCE($7, output_format),
|
||||
notes = COALESCE($8, notes),
|
||||
scoring = COALESCE($9, scoring),
|
||||
|
||||
legend_html = COALESCE($10, legend_html),
|
||||
input_format_html = COALESCE($11, input_format_html),
|
||||
output_format_html = COALESCE($12, output_format_html),
|
||||
notes_html = COALESCE($13, notes_html),
|
||||
scoring_html = COALESCE($14, scoring_html)
|
||||
|
||||
WHERE id=$1`
|
||||
)
|
||||
|
||||
func (r *Repository) UpdateProblem(ctx context.Context, q problems.Querier, id int32, problem *models.ProblemUpdate) error {
|
||||
const op = "Repository.UpdateProblem"
|
||||
|
||||
query := q.Rebind(UpdateProblemQuery)
|
||||
_, err := q.ExecContext(ctx, query,
|
||||
id,
|
||||
|
||||
problem.Title,
|
||||
problem.TimeLimit,
|
||||
problem.MemoryLimit,
|
||||
|
||||
problem.Legend,
|
||||
problem.InputFormat,
|
||||
problem.OutputFormat,
|
||||
problem.Notes,
|
||||
problem.Scoring,
|
||||
|
||||
problem.LegendHtml,
|
||||
problem.InputFormatHtml,
|
||||
problem.OutputFormatHtml,
|
||||
problem.NotesHtml,
|
||||
problem.ScoringHtml,
|
||||
)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
293
internal/problems/repository/pg_repository_test.go
Normal file
293
internal/problems/repository/pg_repository_test.go
Normal file
|
@ -0,0 +1,293 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems/repository"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing.
|
||||
func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) {
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
assert.NoError(t, err)
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
return sqlxDB, mock
|
||||
}
|
||||
|
||||
func TestRepository_CreateProblem(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
problem := models.Problem{
|
||||
Id: 1,
|
||||
Title: "Test Problem",
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.CreateProblemQuery).
|
||||
WithArgs(problem.Title).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(problem.Id))
|
||||
|
||||
id, err := repo.CreateProblem(ctx, db, problem.Title)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, problem.Id, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_GetProblemById(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
expected := &models.Problem{
|
||||
Id: 1,
|
||||
Title: "Test Problem",
|
||||
TimeLimit: 1000,
|
||||
MemoryLimit: 1024,
|
||||
Legend: "Test Legend",
|
||||
InputFormat: "Test Input Format",
|
||||
OutputFormat: "Test Output Format",
|
||||
Notes: "Test Notes",
|
||||
Scoring: "Test Scoring",
|
||||
LegendHtml: "Test Legend HTML",
|
||||
InputFormatHtml: "Test Input Format HTML",
|
||||
OutputFormatHtml: "Test Output Format HTML",
|
||||
NotesHtml: "Test Notes HTML",
|
||||
ScoringHtml: "Test Scoring HTML",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
columns := []string{
|
||||
"id",
|
||||
"title",
|
||||
"time_limit",
|
||||
"memory_limit",
|
||||
|
||||
"legend",
|
||||
"input_format",
|
||||
"output_format",
|
||||
"notes",
|
||||
"scoring",
|
||||
|
||||
"legend_html",
|
||||
"input_format_html",
|
||||
"output_format_html",
|
||||
"notes_html",
|
||||
"scoring_html",
|
||||
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(
|
||||
expected.Id,
|
||||
expected.Title,
|
||||
expected.TimeLimit,
|
||||
expected.MemoryLimit,
|
||||
|
||||
expected.Legend,
|
||||
expected.InputFormat,
|
||||
expected.OutputFormat,
|
||||
expected.Notes,
|
||||
expected.Scoring,
|
||||
|
||||
expected.LegendHtml,
|
||||
expected.InputFormatHtml,
|
||||
expected.OutputFormatHtml,
|
||||
expected.NotesHtml,
|
||||
expected.ScoringHtml,
|
||||
|
||||
expected.CreatedAt,
|
||||
expected.UpdatedAt)
|
||||
|
||||
mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(expected.Id).WillReturnRows(rows)
|
||||
|
||||
problem, err := repo.GetProblemById(ctx, db, expected.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualExportedValues(t, expected, problem)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
id := int32(1)
|
||||
|
||||
mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(id).WillReturnError(sql.ErrNoRows)
|
||||
|
||||
_, err := repo.GetProblemById(ctx, db, id)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteProblem(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
id := int32(1)
|
||||
|
||||
mock.ExpectExec(repository.DeleteProblemQuery).
|
||||
WithArgs(id).WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteProblem(ctx, db, id)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
id := int32(1)
|
||||
|
||||
mock.ExpectExec(repository.DeleteProblemQuery).WithArgs(id).WillReturnError(sql.ErrNoRows)
|
||||
|
||||
err := repo.DeleteProblem(ctx, db, id)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_ListProblems(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
expected := make([]*models.ProblemsListItem, 0)
|
||||
for i := 0; i < 10; i++ {
|
||||
problem := &models.ProblemsListItem{
|
||||
Id: int32(i + 1),
|
||||
Title: fmt.Sprintf("Test Problem %d", i+1),
|
||||
TimeLimit: 1000,
|
||||
MemoryLimit: 1024,
|
||||
SolvedCount: int32(123 * i),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
expected = append(expected, problem)
|
||||
}
|
||||
|
||||
filter := models.ProblemsFilter{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
var totalCount int32 = 10
|
||||
|
||||
columns := []string{
|
||||
"id",
|
||||
"title",
|
||||
"time_limit",
|
||||
"memory_limit",
|
||||
"solved_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns)
|
||||
for _, problem := range expected {
|
||||
rows = rows.AddRow(
|
||||
problem.Id,
|
||||
problem.Title,
|
||||
problem.TimeLimit,
|
||||
problem.MemoryLimit,
|
||||
problem.SolvedCount,
|
||||
problem.CreatedAt,
|
||||
problem.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.ListProblemsQuery).WillReturnRows(rows)
|
||||
mock.ExpectQuery(repository.CountProblemsQuery).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalCount))
|
||||
|
||||
problems, err := repo.ListProblems(ctx, db, filter)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, problems.Problems)
|
||||
assert.Equal(t, models.Pagination{
|
||||
Page: 1,
|
||||
Total: 1,
|
||||
}, problems.Pagination)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_UpdateProblem(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var id int32 = 1
|
||||
|
||||
update := &models.ProblemUpdate{
|
||||
Title: sp("Test Problem"),
|
||||
TimeLimit: ip(1000),
|
||||
MemoryLimit: ip(1024),
|
||||
Legend: sp("Test Legend"),
|
||||
InputFormat: sp("Test Input Format"),
|
||||
OutputFormat: sp("Test Output Format"),
|
||||
Notes: sp("Test Notes"),
|
||||
Scoring: sp("Test Scoring"),
|
||||
LegendHtml: sp("Test Legend HTML"),
|
||||
InputFormatHtml: sp("Test Input Format HTML"),
|
||||
OutputFormatHtml: sp("Test Output Format HTML"),
|
||||
NotesHtml: sp("Test Notes HTML"),
|
||||
ScoringHtml: sp("Test Scoring HTML"),
|
||||
}
|
||||
|
||||
mock.ExpectExec(repository.UpdateProblemQuery).WithArgs(
|
||||
id,
|
||||
|
||||
update.Title,
|
||||
update.TimeLimit,
|
||||
update.MemoryLimit,
|
||||
|
||||
update.Legend,
|
||||
update.InputFormat,
|
||||
update.OutputFormat,
|
||||
update.Notes,
|
||||
update.Scoring,
|
||||
|
||||
update.LegendHtml,
|
||||
update.InputFormatHtml,
|
||||
update.OutputFormatHtml,
|
||||
update.NotesHtml,
|
||||
update.ScoringHtml,
|
||||
).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
err := repo.UpdateProblem(ctx, db, id, update)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func sp(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func ip(s int32) *int32 {
|
||||
return &s
|
||||
}
|
15
internal/problems/usecase.go
Normal file
15
internal/problems/usecase.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package problems
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
CreateProblem(ctx context.Context, title string) (int32, error)
|
||||
GetProblemById(ctx context.Context, id int32) (*models.Problem, error)
|
||||
DeleteProblem(ctx context.Context, id int32) error
|
||||
ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error)
|
||||
UpdateProblem(ctx context.Context, id int32, problem *models.ProblemUpdate) error
|
||||
UploadProblem(ctx context.Context, id int32, archive []byte) error
|
||||
}
|
337
internal/problems/usecase/usecase.go
Normal file
337
internal/problems/usecase/usecase.go
Normal file
|
@ -0,0 +1,337 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/problems"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
type UseCase struct {
|
||||
problemRepo problems.Repository
|
||||
pandocClient pkg.PandocClient
|
||||
}
|
||||
|
||||
func NewUseCase(
|
||||
problemRepo problems.Repository,
|
||||
pandocClient pkg.PandocClient,
|
||||
) *UseCase {
|
||||
return &UseCase{
|
||||
problemRepo: problemRepo,
|
||||
pandocClient: pandocClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UseCase) CreateProblem(ctx context.Context, title string) (int32, error) {
|
||||
return u.problemRepo.CreateProblem(ctx, u.problemRepo.DB(), title)
|
||||
}
|
||||
|
||||
func (u *UseCase) GetProblemById(ctx context.Context, id int32) (*models.Problem, error) {
|
||||
return u.problemRepo.GetProblemById(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *UseCase) DeleteProblem(ctx context.Context, id int32) error {
|
||||
return u.problemRepo.DeleteProblem(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *UseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
return u.problemRepo.ListProblems(ctx, u.problemRepo.DB(), filter)
|
||||
}
|
||||
|
||||
func (u *UseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate *models.ProblemUpdate) error {
|
||||
if isEmpty(*problemUpdate) {
|
||||
return pkg.Wrap(pkg.ErrBadInput, nil, "UpdateProblem", "empty problem update")
|
||||
}
|
||||
|
||||
tx, err := u.problemRepo.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
problem, err := u.problemRepo.GetProblemById(ctx, tx, id)
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
||||
statement := models.ProblemStatement{
|
||||
Legend: problem.Legend,
|
||||
InputFormat: problem.InputFormat,
|
||||
OutputFormat: problem.OutputFormat,
|
||||
Notes: problem.Notes,
|
||||
Scoring: problem.Scoring,
|
||||
}
|
||||
|
||||
if problemUpdate.Legend != nil {
|
||||
statement.Legend = *problemUpdate.Legend
|
||||
}
|
||||
if problemUpdate.InputFormat != nil {
|
||||
statement.InputFormat = *problemUpdate.InputFormat
|
||||
}
|
||||
if problemUpdate.OutputFormat != nil {
|
||||
statement.OutputFormat = *problemUpdate.OutputFormat
|
||||
}
|
||||
if problemUpdate.Notes != nil {
|
||||
statement.Notes = *problemUpdate.Notes
|
||||
}
|
||||
if problemUpdate.Scoring != nil {
|
||||
statement.Scoring = *problemUpdate.Scoring
|
||||
}
|
||||
|
||||
builtStatement, err := build(ctx, u.pandocClient, trimSpaces(statement))
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
||||
if builtStatement.LegendHtml != problem.LegendHtml {
|
||||
problemUpdate.LegendHtml = &builtStatement.LegendHtml
|
||||
}
|
||||
if builtStatement.InputFormatHtml != problem.InputFormatHtml {
|
||||
problemUpdate.InputFormatHtml = &builtStatement.InputFormatHtml
|
||||
}
|
||||
if builtStatement.OutputFormatHtml != problem.OutputFormatHtml {
|
||||
problemUpdate.OutputFormatHtml = &builtStatement.OutputFormatHtml
|
||||
}
|
||||
if builtStatement.NotesHtml != problem.NotesHtml {
|
||||
problemUpdate.NotesHtml = &builtStatement.NotesHtml
|
||||
}
|
||||
if builtStatement.ScoringHtml != problem.ScoringHtml {
|
||||
problemUpdate.ScoringHtml = &builtStatement.ScoringHtml
|
||||
}
|
||||
|
||||
err = u.problemRepo.UpdateProblem(ctx, tx, id, problemUpdate)
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ProblemProperties struct {
|
||||
Title string `json:"name"`
|
||||
TimeLimit int32 `json:"timeLimit"`
|
||||
MemoryLimit int32 `json:"memoryLimit"`
|
||||
}
|
||||
|
||||
func (u *UseCase) UploadProblem(ctx context.Context, id int32, data []byte) error {
|
||||
|
||||
locale := "russian"
|
||||
defaultLocale := "english"
|
||||
var localeProblem, defaultProblem string
|
||||
var localeProperties, defaultProperties ProblemProperties
|
||||
|
||||
r := bytes.NewReader(data)
|
||||
rc, err := zip.NewReader(r, int64(r.Len()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
testsZipBuf := new(bytes.Buffer)
|
||||
w := zip.NewWriter(testsZipBuf)
|
||||
|
||||
for _, f := range rc.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
if f.Name == fmt.Sprintf("statements/%s/problem.tex", locale) {
|
||||
localeProblem, err = readProblem(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.Name == fmt.Sprintf("statements/%s/problem.tex", defaultLocale) {
|
||||
defaultProblem, err = readProblem(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.Name == fmt.Sprintf("statements/%s/problem-properties.json", locale) {
|
||||
localeProperties, err = readProperties(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.Name == fmt.Sprintf("statements/%s/problem-properties.json", defaultLocale) {
|
||||
defaultProperties, err = readProperties(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(f.Name, "tests/") {
|
||||
if err := w.Copy(f); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
// testsZipBuf contains test files; this is for s3
|
||||
|
||||
localeProperties.MemoryLimit /= 1024 * 1024
|
||||
defaultProperties.MemoryLimit /= 1024 * 1024
|
||||
|
||||
problemUpdate := &models.ProblemUpdate{}
|
||||
if localeProblem != "" {
|
||||
problemUpdate.Legend = &localeProblem
|
||||
problemUpdate.Title = &localeProperties.Title
|
||||
problemUpdate.TimeLimit = &localeProperties.TimeLimit
|
||||
problemUpdate.MemoryLimit = &localeProperties.MemoryLimit
|
||||
} else {
|
||||
problemUpdate.Legend = &defaultProblem
|
||||
problemUpdate.Title = &defaultProperties.Title
|
||||
problemUpdate.TimeLimit = &defaultProperties.TimeLimit
|
||||
problemUpdate.MemoryLimit = &defaultProperties.MemoryLimit
|
||||
}
|
||||
if err := u.UpdateProblem(ctx, id, problemUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readProblem(f *zip.File) (string, error) {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rc.Close()
|
||||
problemData, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(problemData), nil
|
||||
}
|
||||
|
||||
func readProperties(f *zip.File) (ProblemProperties, error) {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return ProblemProperties{}, err
|
||||
}
|
||||
defer rc.Close()
|
||||
var properties ProblemProperties
|
||||
if err := json.NewDecoder(rc).Decode(&properties); err != nil {
|
||||
return ProblemProperties{}, err
|
||||
}
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func isEmpty(p models.ProblemUpdate) bool {
|
||||
return p.Title == nil &&
|
||||
p.Legend == nil &&
|
||||
p.InputFormat == nil &&
|
||||
p.OutputFormat == nil &&
|
||||
p.Notes == nil &&
|
||||
p.Scoring == nil &&
|
||||
p.MemoryLimit == nil &&
|
||||
p.TimeLimit == nil
|
||||
}
|
||||
|
||||
func wrap(s string) string {
|
||||
return fmt.Sprintf("\\begin{document}\n%s\n\\end{document}\n", s)
|
||||
}
|
||||
|
||||
func trimSpaces(statement models.ProblemStatement) models.ProblemStatement {
|
||||
return models.ProblemStatement{
|
||||
Legend: strings.TrimSpace(statement.Legend),
|
||||
InputFormat: strings.TrimSpace(statement.InputFormat),
|
||||
OutputFormat: strings.TrimSpace(statement.OutputFormat),
|
||||
Notes: strings.TrimSpace(statement.Notes),
|
||||
Scoring: strings.TrimSpace(statement.Scoring),
|
||||
}
|
||||
}
|
||||
|
||||
func sanitize(statement models.Html5ProblemStatement) models.Html5ProblemStatement {
|
||||
p := bluemonday.UGCPolicy()
|
||||
|
||||
p.AllowAttrs("class").Globally()
|
||||
p.AllowAttrs("style").Globally()
|
||||
p.AllowStyles("text-align").MatchingEnum("center", "left", "right").Globally()
|
||||
p.AllowStyles("display").MatchingEnum("block", "inline", "inline-block").Globally()
|
||||
|
||||
p.AllowStandardURLs()
|
||||
p.AllowAttrs("cite").OnElements("blockquote", "q")
|
||||
p.AllowAttrs("href").OnElements("a", "area")
|
||||
p.AllowAttrs("src").OnElements("img")
|
||||
|
||||
if statement.LegendHtml != "" {
|
||||
statement.LegendHtml = p.Sanitize(statement.LegendHtml)
|
||||
}
|
||||
if statement.InputFormatHtml != "" {
|
||||
statement.InputFormatHtml = p.Sanitize(statement.InputFormatHtml)
|
||||
}
|
||||
if statement.OutputFormatHtml != "" {
|
||||
statement.OutputFormatHtml = p.Sanitize(statement.OutputFormatHtml)
|
||||
}
|
||||
if statement.NotesHtml != "" {
|
||||
statement.NotesHtml = p.Sanitize(statement.NotesHtml)
|
||||
}
|
||||
if statement.ScoringHtml != "" {
|
||||
statement.ScoringHtml = p.Sanitize(statement.ScoringHtml)
|
||||
}
|
||||
|
||||
return statement
|
||||
}
|
||||
|
||||
func build(ctx context.Context, pandocClient pkg.PandocClient, p models.ProblemStatement) (models.Html5ProblemStatement, error) {
|
||||
p = trimSpaces(p)
|
||||
|
||||
latex := models.ProblemStatement{}
|
||||
|
||||
if p.Legend != "" {
|
||||
latex.Legend = wrap(p.Legend)
|
||||
}
|
||||
if p.InputFormat != "" {
|
||||
latex.InputFormat = wrap(p.InputFormat)
|
||||
}
|
||||
if p.OutputFormat != "" {
|
||||
latex.OutputFormat = wrap(p.OutputFormat)
|
||||
}
|
||||
if p.Notes != "" {
|
||||
latex.Notes = wrap(p.Notes)
|
||||
}
|
||||
if p.Scoring != "" {
|
||||
latex.Scoring = wrap(p.Scoring)
|
||||
}
|
||||
|
||||
req := []string{
|
||||
latex.Legend,
|
||||
latex.InputFormat,
|
||||
latex.OutputFormat,
|
||||
latex.Notes,
|
||||
latex.Scoring,
|
||||
}
|
||||
|
||||
res, err := pandocClient.BatchConvertLatexToHtml5(ctx, req)
|
||||
if err != nil {
|
||||
return models.Html5ProblemStatement{}, err
|
||||
}
|
||||
|
||||
if len(res) != len(req) {
|
||||
return models.Html5ProblemStatement{}, fmt.Errorf("wrong number of fieilds returned: %d", len(res))
|
||||
}
|
||||
|
||||
sanitizedStatement := sanitize(models.Html5ProblemStatement{
|
||||
LegendHtml: res[0],
|
||||
InputFormatHtml: res[1],
|
||||
OutputFormatHtml: res[2],
|
||||
NotesHtml: res[3],
|
||||
ScoringHtml: res[4],
|
||||
})
|
||||
|
||||
return sanitizedStatement, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue