feat: merge auth&tester
This commit is contained in:
parent
0a2dea6c23
commit
441af4c6a2
72 changed files with 4910 additions and 2378 deletions
13
internal/auth/delivery.go
Normal file
13
internal/auth/delivery.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type AuthHandlers interface {
|
||||
ListSessions(c *fiber.Ctx) error
|
||||
Terminate(c *fiber.Ctx) error
|
||||
Login(c *fiber.Ctx) error
|
||||
Logout(c *fiber.Ctx) error
|
||||
Refresh(c *fiber.Ctx) error
|
||||
}
|
160
internal/auth/delivery/rest/handlers.go
Normal file
160
internal/auth/delivery/rest/handlers.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/auth"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
authUC auth.UseCase
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
func NewHandlers(authUC auth.UseCase, jwtSecret string) *Handlers {
|
||||
return &Handlers{
|
||||
authUC: authUC,
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
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 (h *Handlers) ListSessions(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (h *Handlers) Terminate(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
err = h.authUC.Terminate(ctx, session.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handlers) Login(c *fiber.Ctx) error {
|
||||
authHeader := c.Get("Authorization", "")
|
||||
if authHeader == "" {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
username, pwd, err := parseBasicAuth(authHeader)
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
credentials := &models.Credentials{
|
||||
Username: strings.ToLower(username),
|
||||
Password: pwd,
|
||||
}
|
||||
device := &models.Device{
|
||||
Ip: c.IP(),
|
||||
UseAgent: c.Get("User-Agent", ""),
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := h.authUC.Login(ctx, credentials, device)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, models.JWT{
|
||||
SessionId: session.Id,
|
||||
UserId: session.UserId,
|
||||
Role: session.Role,
|
||||
IssuedAt: time.Now().Unix(),
|
||||
})
|
||||
|
||||
token, err := claims.SignedString([]byte(h.jwtSecret))
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
|
||||
c.Set("Authorization", "Bearer "+token)
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handlers) Logout(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
err = h.authUC.Logout(c.Context(), session.Id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handlers) Refresh(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
err = h.authUC.Refresh(c.Context(), session.Id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func parseBasicAuth(header string) (string, string, error) {
|
||||
const (
|
||||
op = "parseBasicAuth"
|
||||
msg = "invalid auth header"
|
||||
)
|
||||
|
||||
authParts := strings.Split(header, " ")
|
||||
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "basic" {
|
||||
return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg)
|
||||
}
|
||||
|
||||
decodedAuth, err := base64.StdEncoding.DecodeString(authParts[1])
|
||||
if err != nil {
|
||||
return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg)
|
||||
}
|
||||
|
||||
authParts = strings.Split(string(decodedAuth), ":")
|
||||
if len(authParts) != 2 {
|
||||
return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg)
|
||||
}
|
||||
|
||||
return authParts[0], authParts[1], nil
|
||||
}
|
14
internal/auth/usecase.go
Normal file
14
internal/auth/usecase.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
Login(ctx context.Context, credentials *models.Credentials, device *models.Device) (*models.Session, error)
|
||||
Refresh(ctx context.Context, sessionId string) error
|
||||
Logout(ctx context.Context, sessionId string) error
|
||||
Terminate(ctx context.Context, userId int32) error
|
||||
ListSessions(ctx context.Context, userId int32) ([]*models.Session, error)
|
||||
}
|
70
internal/auth/usecase/usecase.go
Normal file
70
internal/auth/usecase/usecase.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UseCase struct {
|
||||
usersUC users.UseCase
|
||||
sessionsUC sessions.UseCase
|
||||
}
|
||||
|
||||
func NewUseCase(usersUC users.UseCase, sessionsUC sessions.UseCase) *UseCase {
|
||||
return &UseCase{
|
||||
usersUC: usersUC,
|
||||
sessionsUC: sessionsUC,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *UseCase) Login(ctx context.Context, credentials *models.Credentials, device *models.Device) (*models.Session, error) {
|
||||
const op = "UseCase.Login"
|
||||
|
||||
user, err := uc.usersUC.ReadUserByUsername(ctx, credentials.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !user.IsSamePwd(credentials.Password) {
|
||||
return nil, pkg.Wrap(pkg.ErrNotFound, nil, op, "password mismatch")
|
||||
}
|
||||
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: user.Id,
|
||||
Role: user.Role,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(40 * time.Minute),
|
||||
UserAgent: device.UseAgent,
|
||||
Ip: device.Ip,
|
||||
}
|
||||
|
||||
err = uc.sessionsUC.CreateSession(ctx, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (uc *UseCase) Logout(ctx context.Context, sessionId string) error {
|
||||
return uc.sessionsUC.DeleteSession(ctx, sessionId)
|
||||
}
|
||||
|
||||
func (uc *UseCase) Refresh(ctx context.Context, sessionId string) error {
|
||||
return uc.sessionsUC.UpdateSession(ctx, sessionId)
|
||||
}
|
||||
|
||||
func (uc *UseCase) Terminate(ctx context.Context, userId int32) error {
|
||||
return uc.sessionsUC.DeleteAllSessions(ctx, userId)
|
||||
}
|
||||
|
||||
func (uc *UseCase) ListSessions(ctx context.Context, userId int32) ([]*models.Session, error) {
|
||||
// TODO: implement me
|
||||
panic("implement me")
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
package tester
|
||||
package contests
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Handlers interface {
|
||||
type ContestsHandlers interface {
|
||||
ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error
|
||||
CreateContest(c *fiber.Ctx) error
|
||||
DeleteContest(c *fiber.Ctx, id int32) error
|
||||
|
@ -14,18 +14,12 @@ type Handlers interface {
|
|||
DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error
|
||||
ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error
|
||||
UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error
|
||||
AddParticipant(c *fiber.Ctx, params testerv1.AddParticipantParams) error
|
||||
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
|
||||
CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) error
|
||||
ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error
|
||||
CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error
|
||||
GetSolution(c *fiber.Ctx, id int32) error
|
||||
DeleteTask(c *fiber.Ctx, id int32) error
|
||||
AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error
|
||||
CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error
|
||||
GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error
|
||||
GetTask(c *fiber.Ctx, id int32) error
|
||||
}
|
202
internal/contests/delivery/rest/contests_handlers.go
Normal file
202
internal/contests/delivery/rest/contests_handlers.go
Normal file
|
@ -0,0 +1,202 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests"
|
||||
"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
|
||||
contestsUC contests.UseCase
|
||||
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
func NewHandlers(problemsUC problems.UseCase, contestsUC contests.UseCase, jwtSecret string) *Handlers {
|
||||
return &Handlers{
|
||||
problemsUC: problemsUC,
|
||||
contestsUC: contestsUC,
|
||||
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
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 (h *Handlers) CreateContest(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.contestsUC.CreateContest(ctx, "Название контеста")
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(&testerv1.CreateContestResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetContest(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:
|
||||
contest, err := h.contestsUC.GetContest(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
solutions := make([]*models.SolutionsListItem, 0)
|
||||
participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId)
|
||||
if err == nil { // Admin or Teacher may not participate in contest
|
||||
solutions, err = h.contestsUC.GetBestSolutions(ctx, id, participantId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(GetContestResponseDTO(contest, tasks, solutions))
|
||||
case models.RoleStudent:
|
||||
contest, err := h.contestsUC.GetContest(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId)
|
||||
solutions, err := h.contestsUC.GetBestSolutions(c.Context(), id, participantId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(GetContestResponseDTO(contest, tasks, solutions))
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UpdateContest(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.UpdateContestRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.contestsUC.UpdateContest(ctx, id, models.ContestUpdate{
|
||||
Title: req.Title,
|
||||
})
|
||||
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) DeleteContest(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.contestsUC.DeleteContest(ctx, 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) ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
filter := models.ContestsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
contestsList, err := h.contestsUC.ListContests(ctx, filter)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(ListContestsResponseDTO(contestsList))
|
||||
case models.RoleStudent:
|
||||
filter.UserId = &session.UserId
|
||||
contestsList, err := h.contestsUC.ListContests(ctx, filter)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(ListContestsResponseDTO(contestsList))
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
202
internal/contests/delivery/rest/dto.go
Normal file
202
internal/contests/delivery/rest/dto.go
Normal file
|
@ -0,0 +1,202 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func GetContestResponseDTO(contest *models.Contest,
|
||||
tasks []*models.TasksListItem,
|
||||
solutions []*models.SolutionsListItem) *testerv1.GetContestResponse {
|
||||
|
||||
m := make(map[int32]*models.SolutionsListItem)
|
||||
|
||||
for i := 0; i < len(solutions); i++ {
|
||||
m[solutions[i].TaskPosition] = solutions[i]
|
||||
}
|
||||
|
||||
resp := testerv1.GetContestResponse{
|
||||
Contest: ContestDTO(*contest),
|
||||
Tasks: make([]struct {
|
||||
Solution testerv1.SolutionsListItem `json:"solution"`
|
||||
Task testerv1.TasksListItem `json:"task"`
|
||||
}, len(tasks)),
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
solution := testerv1.SolutionsListItem{}
|
||||
if sol, ok := m[task.Position]; ok {
|
||||
solution = SolutionsListItemDTO(*sol)
|
||||
}
|
||||
resp.Tasks[i] = struct {
|
||||
Solution testerv1.SolutionsListItem `json:"solution"`
|
||||
Task testerv1.TasksListItem `json:"task"`
|
||||
}{
|
||||
Solution: solution,
|
||||
Task: TasksListItemDTO(*task),
|
||||
}
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func ListContestsResponseDTO(contestsList *models.ContestsList) *testerv1.ListContestsResponse {
|
||||
resp := testerv1.ListContestsResponse{
|
||||
Contests: make([]testerv1.ContestsListItem, len(contestsList.Contests)),
|
||||
Pagination: PaginationDTO(contestsList.Pagination),
|
||||
}
|
||||
|
||||
for i, contest := range contestsList.Contests {
|
||||
resp.Contests[i] = ContestsListItemDTO(*contest)
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func ListSolutionsResponseDTO(solutionsList *models.SolutionsList) *testerv1.ListSolutionsResponse {
|
||||
resp := testerv1.ListSolutionsResponse{
|
||||
Solutions: make([]testerv1.SolutionsListItem, len(solutionsList.Solutions)),
|
||||
Pagination: PaginationDTO(solutionsList.Pagination),
|
||||
}
|
||||
|
||||
for i, solution := range solutionsList.Solutions {
|
||||
resp.Solutions[i] = SolutionsListItemDTO(*solution)
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func GetTaskResponseDTO(contest *models.Contest, tasks []*models.TasksListItem, task *models.Task) *testerv1.GetTaskResponse {
|
||||
resp := testerv1.GetTaskResponse{
|
||||
Contest: ContestDTO(*contest),
|
||||
Tasks: make([]testerv1.TasksListItem, len(tasks)),
|
||||
Task: *TaskDTO(task),
|
||||
}
|
||||
|
||||
for i, t := range tasks {
|
||||
resp.Tasks[i] = TasksListItemDTO(*t)
|
||||
}
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func PaginationDTO(p models.Pagination) testerv1.Pagination {
|
||||
return testerv1.Pagination{
|
||||
Page: p.Page,
|
||||
Total: p.Total,
|
||||
}
|
||||
}
|
||||
|
||||
func ContestDTO(c models.Contest) testerv1.Contest {
|
||||
return testerv1.Contest{
|
||||
Id: c.Id,
|
||||
Title: c.Title,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ContestsListItemDTO(c models.ContestsListItem) testerv1.ContestsListItem {
|
||||
return testerv1.ContestsListItem{
|
||||
Id: c.Id,
|
||||
Title: c.Title,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func TasksListItemDTO(t models.TasksListItem) testerv1.TasksListItem {
|
||||
return testerv1.TasksListItem{
|
||||
Id: t.Id,
|
||||
Position: t.Position,
|
||||
Title: t.Title,
|
||||
MemoryLimit: t.MemoryLimit,
|
||||
ProblemId: t.ProblemId,
|
||||
TimeLimit: t.TimeLimit,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func TaskDTO(t *models.Task) *testerv1.Task {
|
||||
return &testerv1.Task{
|
||||
Id: t.Id,
|
||||
Title: t.Title,
|
||||
MemoryLimit: t.MemoryLimit,
|
||||
TimeLimit: t.TimeLimit,
|
||||
|
||||
InputFormatHtml: t.InputFormatHtml,
|
||||
LegendHtml: t.LegendHtml,
|
||||
NotesHtml: t.NotesHtml,
|
||||
OutputFormatHtml: t.OutputFormatHtml,
|
||||
Position: t.Position,
|
||||
ScoringHtml: t.ScoringHtml,
|
||||
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ParticipantsListItemDTO(p models.ParticipantsListItem) testerv1.ParticipantsListItem {
|
||||
return testerv1.ParticipantsListItem{
|
||||
Id: p.Id,
|
||||
UserId: p.UserId,
|
||||
Name: p.Name,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func SolutionsListItemDTO(s models.SolutionsListItem) testerv1.SolutionsListItem {
|
||||
return testerv1.SolutionsListItem{
|
||||
Id: s.Id,
|
||||
|
||||
ParticipantId: s.ParticipantId,
|
||||
ParticipantName: s.ParticipantName,
|
||||
|
||||
State: s.State,
|
||||
Score: s.Score,
|
||||
Penalty: s.Penalty,
|
||||
TimeStat: s.TimeStat,
|
||||
MemoryStat: s.MemoryStat,
|
||||
Language: s.Language,
|
||||
|
||||
TaskId: s.TaskId,
|
||||
TaskPosition: s.TaskPosition,
|
||||
TaskTitle: s.TaskTitle,
|
||||
|
||||
ContestId: s.ContestId,
|
||||
ContestTitle: s.ContestTitle,
|
||||
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func SolutionDTO(s models.Solution) testerv1.Solution {
|
||||
return testerv1.Solution{
|
||||
Id: s.Id,
|
||||
|
||||
ParticipantId: s.ParticipantId,
|
||||
ParticipantName: s.ParticipantName,
|
||||
|
||||
Solution: s.Solution,
|
||||
|
||||
State: s.State,
|
||||
Score: s.Score,
|
||||
Penalty: s.Penalty,
|
||||
TimeStat: s.TimeStat,
|
||||
MemoryStat: s.MemoryStat,
|
||||
Language: s.Language,
|
||||
|
||||
TaskId: s.TaskId,
|
||||
TaskPosition: s.TaskPosition,
|
||||
TaskTitle: s.TaskTitle,
|
||||
|
||||
ContestId: s.ContestId,
|
||||
ContestTitle: s.ContestTitle,
|
||||
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
71
internal/contests/delivery/rest/monitor_handlers.go
Normal file
71
internal/contests/delivery/rest/monitor_handlers.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
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/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *Handlers) GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) 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, models.RoleStudent:
|
||||
contest, err := h.contestsUC.GetContest(ctx, params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
monitor, err := h.contestsUC.GetMonitor(ctx, params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(ctx, params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.GetMonitorResponse{
|
||||
Contest: ContestDTO(*contest),
|
||||
Tasks: make([]testerv1.TasksListItem, len(tasks)),
|
||||
Participants: make([]testerv1.ParticipantsStat, len(monitor.Participants)),
|
||||
SummaryPerProblem: make([]testerv1.ProblemStatSummary, len(monitor.Summary)),
|
||||
}
|
||||
|
||||
for i, participant := range monitor.Participants {
|
||||
resp.Participants[i] = testerv1.ParticipantsStat{
|
||||
Id: participant.Id,
|
||||
Name: participant.Name,
|
||||
PenaltyInTotal: participant.PenaltyInTotal,
|
||||
Solutions: make([]testerv1.SolutionsListItem, len(participant.Solutions)),
|
||||
SolvedInTotal: participant.SolvedInTotal,
|
||||
}
|
||||
|
||||
for j, solution := range participant.Solutions {
|
||||
resp.Participants[i].Solutions[j] = SolutionsListItemDTO(*solution)
|
||||
}
|
||||
}
|
||||
|
||||
for i, problem := range monitor.Summary {
|
||||
resp.SummaryPerProblem[i] = testerv1.ProblemStatSummary{
|
||||
Id: problem.Id,
|
||||
Success: problem.Success,
|
||||
Total: problem.Total,
|
||||
}
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
resp.Tasks[i] = TasksListItemDTO(*task)
|
||||
}
|
||||
return c.JSON(resp)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
116
internal/contests/delivery/rest/participants_handlers.go
Normal file
116
internal/contests/delivery/rest/participants_handlers.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
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/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *Handlers) CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) 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.contestsUC.CreateParticipant(ctx, params.ContestId, params.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateParticipantResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) 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.UpdateParticipantRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.contestsUC.UpdateParticipant(ctx, params.ParticipantId, models.ParticipantUpdate{
|
||||
Name: req.Name,
|
||||
})
|
||||
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) DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) 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.contestsUC.DeleteParticipant(c.Context(), params.ParticipantId)
|
||||
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) ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) 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:
|
||||
participantsList, err := h.contestsUC.ListParticipants(c.Context(), models.ParticipantsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
ContestId: params.ContestId,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListParticipantsResponse{
|
||||
Participants: make([]testerv1.ParticipantsListItem, len(participantsList.Participants)),
|
||||
Pagination: PaginationDTO(participantsList.Pagination),
|
||||
}
|
||||
|
||||
for i, participant := range participantsList.Participants {
|
||||
resp.Participants[i] = ParticipantsListItemDTO(*participant)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
148
internal/contests/delivery/rest/solutions_handlers.go
Normal file
148
internal/contests/delivery/rest/solutions_handlers.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
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/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSolutionSize int64 = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
func (h *Handlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) 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, models.RoleStudent:
|
||||
s, err := c.FormFile("solution")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Size == 0 || s.Size > maxSolutionSize {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
f, err := s.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := h.contestsUC.CreateSolution(ctx, &models.SolutionCreation{
|
||||
UserId: session.UserId,
|
||||
TaskId: params.TaskId,
|
||||
Language: params.Language,
|
||||
Solution: string(b),
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateSolutionResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetSolution(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:
|
||||
solution, err := h.contestsUC.GetSolution(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)})
|
||||
case models.RoleStudent:
|
||||
_, err := h.contestsUC.GetParticipantId3(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
solution, err := h.contestsUC.GetSolution(ctx, id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error {
|
||||
ctx := c.Context()
|
||||
|
||||
session, err := sessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
filter := models.SolutionsFilter{
|
||||
ContestId: params.ContestId,
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
ParticipantId: params.ParticipantId,
|
||||
TaskId: params.TaskId,
|
||||
Language: params.Language,
|
||||
Order: params.Order,
|
||||
State: params.State,
|
||||
}
|
||||
|
||||
switch session.Role {
|
||||
case models.RoleAdmin, models.RoleTeacher:
|
||||
solutionsList, err := h.contestsUC.ListSolutions(ctx, filter)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(ListSolutionsResponseDTO(solutionsList))
|
||||
case models.RoleStudent:
|
||||
if params.ContestId == nil {
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
|
||||
participantId, err := h.contestsUC.GetParticipantId(ctx, *params.ContestId, session.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
|
||||
// Student cannot view other users' solutions
|
||||
if params.ParticipantId != nil && *params.ParticipantId != participantId {
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
|
||||
filter.ParticipantId = &participantId
|
||||
solutionsList, err := h.contestsUC.ListSolutions(ctx, filter)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(ListSolutionsResponseDTO(solutionsList))
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
105
internal/contests/delivery/rest/tasks_handlers.go
Normal file
105
internal/contests/delivery/rest/tasks_handlers.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
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/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (h *Handlers) CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) 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.contestsUC.CreateTask(ctx, params.ContestId, params.ProblemId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateTaskResponse{
|
||||
Id: id,
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetTask(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:
|
||||
contest, err := h.contestsUC.GetContest(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
t, err := h.contestsUC.GetTask(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(GetTaskResponseDTO(contest, tasks, t))
|
||||
case models.RoleStudent:
|
||||
_, err = h.contestsUC.GetParticipantId2(ctx, id, session.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
|
||||
contest, err := h.contestsUC.GetContest(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.GetTasks(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
t, err := h.contestsUC.GetTask(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(GetTaskResponseDTO(contest, tasks, t))
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) DeleteTask(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.contestsUC.DeleteTask(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))
|
||||
}
|
||||
}
|
34
internal/contests/pg_repository.go
Normal file
34
internal/contests/pg_repository.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package contests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
CreateContest(ctx context.Context, title string) (int32, error)
|
||||
GetContest(ctx context.Context, id int32) (*models.Contest, error)
|
||||
DeleteContest(ctx context.Context, id int32) error
|
||||
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
|
||||
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
|
||||
|
||||
CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
|
||||
GetTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
DeleteTask(ctx context.Context, taskId int32) error
|
||||
GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
|
||||
|
||||
GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error)
|
||||
GetParticipantId3(ctx context.Context, solutionId int32) (int32, error)
|
||||
CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
DeleteParticipant(ctx context.Context, participantId int32) error
|
||||
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
|
||||
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
|
||||
|
||||
GetSolution(ctx context.Context, id int32) (*models.Solution, error)
|
||||
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
|
||||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error)
|
||||
|
||||
GetMonitor(ctx context.Context, id int32, penalty int32) (*models.Monitor, error)
|
||||
}
|
145
internal/contests/repository/contests_pg_repository.go
Normal file
145
internal/contests/repository/contests_pg_repository.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *sqlx.DB) *Repository {
|
||||
return &Repository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
const CreateContestQuery = "INSERT INTO contests (title) VALUES ($1) RETURNING id"
|
||||
|
||||
func (r *Repository) CreateContest(ctx context.Context, title string) (int32, error) {
|
||||
const op = "Repository.CreateContest"
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, CreateContestQuery, 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 GetContestQuery = "SELECT * from contests WHERE id=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetContest(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
const op = "Repository.GetContest"
|
||||
|
||||
var contest models.Contest
|
||||
err := r.db.GetContext(ctx, &contest, GetContestQuery, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return &contest, nil
|
||||
}
|
||||
|
||||
const (
|
||||
UpdateContestQuery = "UPDATE contests SET title = COALESCE($1, title) WHERE id = $2"
|
||||
)
|
||||
|
||||
func (r *Repository) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
|
||||
const op = "Repository.UpdateContest"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, UpdateContestQuery, contestUpdate.Title, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const DeleteContestQuery = "DELETE FROM contests WHERE id=$1"
|
||||
|
||||
func (r *Repository) DeleteContest(ctx context.Context, id int32) error {
|
||||
const op = "Repository.DeleteContest"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, DeleteContestQuery, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildListContestsQueries(filter models.ContestsFilter) (sq.SelectBuilder, sq.SelectBuilder) {
|
||||
columns := []string{
|
||||
"c.id",
|
||||
"c.title",
|
||||
"c.created_at",
|
||||
"c.updated_at",
|
||||
}
|
||||
|
||||
qb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...).From("contests c")
|
||||
|
||||
if filter.UserId != nil {
|
||||
qb = qb.Join("participants p ON c.id = p.contest_id")
|
||||
qb = qb.Where(sq.Eq{"p.user_id": *filter.UserId})
|
||||
}
|
||||
|
||||
countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub")
|
||||
|
||||
if filter.Order != nil && *filter.Order < 0 {
|
||||
qb = qb.OrderBy("c.created_at DESC")
|
||||
} else {
|
||||
qb = qb.OrderBy("c.created_at ASC")
|
||||
}
|
||||
|
||||
qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset()))
|
||||
|
||||
return qb, countQb
|
||||
}
|
||||
|
||||
func (r *Repository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
|
||||
const op = "Repository.ListContests"
|
||||
|
||||
baseQb, countQb := buildListContestsQueries(filter)
|
||||
|
||||
query, args, err := baseQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var contests []*models.ContestsListItem
|
||||
err = r.db.SelectContext(ctx, &contests, query, args...)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
query, args, err = countQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var count int32
|
||||
err = r.db.GetContext(ctx, &count, query, args...)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ContestsList{
|
||||
Contests: contests,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
116
internal/contests/repository/contests_pg_repository_test.go
Normal file
116
internal/contests/repository/contests_pg_repository_test.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"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_CreateContest(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
contest := models.Contest{
|
||||
Id: 1,
|
||||
Title: "Test Contest",
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.CreateContestQuery).
|
||||
WithArgs(contest.Title).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(contest.Id))
|
||||
|
||||
id, err := repo.CreateContest(ctx, contest.Title)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, contest.Id, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_GetContest(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
contest := models.Contest{
|
||||
Id: 1,
|
||||
Title: "Test Contest",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.GetContestQuery).
|
||||
WithArgs(contest.Id).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "title", "created_at", "updated_at"}).
|
||||
AddRow(contest.Id, contest.Title, contest.CreatedAt, contest.UpdatedAt))
|
||||
|
||||
result, err := repo.GetContest(ctx, contest.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualExportedValues(t, &contest, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_UpdateContest(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 contestId int32 = 1
|
||||
update := models.ContestUpdate{
|
||||
Title: sp("Updated Contest"),
|
||||
}
|
||||
|
||||
mock.ExpectExec(repository.UpdateContestQuery).
|
||||
WithArgs(update.Title, contestId).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.UpdateContest(ctx, contestId, update)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteContest(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectExec(repository.DeleteContestQuery).
|
||||
WithArgs(1).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteContest(ctx, 1)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func sp(s string) *string {
|
||||
return &s
|
||||
}
|
161
internal/contests/repository/monitor_pg_repository.go
Normal file
161
internal/contests/repository/monitor_pg_repository.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
const (
|
||||
// state=5 - AC
|
||||
ReadStatisticsQuery = `
|
||||
SELECT t.id as task_id,
|
||||
t.position,
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN s.state = 5 THEN 1 END) as success
|
||||
FROM tasks t LEFT JOIN solutions s ON t.id = s.task_id
|
||||
WHERE t.contest_id = $1
|
||||
GROUP BY t.id, t.position
|
||||
ORDER BY t.position;
|
||||
`
|
||||
|
||||
SolutionsQuery = `
|
||||
WITH RankedSolutions AS (
|
||||
SELECT
|
||||
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 as contest_title,
|
||||
|
||||
s.updated_at,
|
||||
s.created_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY s.task_id, s.participant_id
|
||||
ORDER BY s.score DESC, s.created_at
|
||||
) as rn
|
||||
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 t.contest_id = $1
|
||||
)
|
||||
SELECT
|
||||
rs.id,
|
||||
|
||||
rs.participant_id,
|
||||
rs.participant_name,
|
||||
|
||||
rs.state,
|
||||
rs.score,
|
||||
rs.penalty,
|
||||
rs.time_stat,
|
||||
rs.memory_stat,
|
||||
rs.language,
|
||||
|
||||
rs.task_id,
|
||||
rs.task_position,
|
||||
rs.task_title,
|
||||
|
||||
rs.contest_id,
|
||||
rs.contest_title,
|
||||
|
||||
rs.updated_at,
|
||||
rs.created_at
|
||||
FROM RankedSolutions rs
|
||||
WHERE rs.rn = 1`
|
||||
|
||||
ParticipantsQuery = `
|
||||
WITH Attempts AS (
|
||||
SELECT
|
||||
s.participant_id,
|
||||
s.task_id,
|
||||
COUNT(*) FILTER (WHERE s.state != 5 AND s.created_at < (
|
||||
SELECT MIN(s2.created_at)
|
||||
FROM solutions s2
|
||||
WHERE s2.participant_id = s.participant_id
|
||||
AND s2.task_id = s.task_id
|
||||
AND s2.state = 5
|
||||
)) as failed_attempts,
|
||||
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 = $1
|
||||
GROUP BY s.participant_id, s.task_id
|
||||
)
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total,
|
||||
COALESCE(SUM(a.failed_attempts), 0) * $2 + COALESCE(SUM(a.success_penalty), 0) as penalty_in_total
|
||||
FROM participants p LEFT JOIN Attempts a ON a.participant_id = p.id
|
||||
WHERE p.contest_id = $1
|
||||
GROUP BY p.id, p.name
|
||||
`
|
||||
)
|
||||
|
||||
func (r *Repository) GetMonitor(ctx context.Context, contestId int32, penalty int32) (*models.Monitor, error) {
|
||||
const op = "Repository.GetMonitor"
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, ReadStatisticsQuery, contestId)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var monitor models.Monitor
|
||||
for rows.Next() {
|
||||
var stat models.ProblemStatSummary
|
||||
err = rows.StructScan(&stat)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
monitor.Summary = append(monitor.Summary, &stat)
|
||||
}
|
||||
|
||||
var solutions []*models.SolutionsListItem
|
||||
err = r.db.SelectContext(ctx, &solutions, SolutionsQuery, contestId)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
rows3, err := r.db.QueryxContext(ctx, ParticipantsQuery, contestId, penalty)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
defer rows3.Close()
|
||||
|
||||
solutionsMap := make(map[int32][]*models.SolutionsListItem)
|
||||
for _, solution := range solutions {
|
||||
solutionsMap[solution.ParticipantId] = append(solutionsMap[solution.ParticipantId], solution)
|
||||
}
|
||||
|
||||
for rows3.Next() {
|
||||
var stat models.ParticipantsStat
|
||||
err = rows3.StructScan(&stat)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
if sols, ok := solutionsMap[stat.Id]; ok {
|
||||
stat.Solutions = sols
|
||||
}
|
||||
|
||||
monitor.Participants = append(monitor.Participants, &stat)
|
||||
}
|
||||
|
||||
return &monitor, nil
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package repository
|
126
internal/contests/repository/participants_pg_repository.go
Normal file
126
internal/contests/repository/participants_pg_repository.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
const GetParticipantIdQuery = "SELECT id FROM participants WHERE user_id=$1 AND contest_id=$2 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) {
|
||||
const op = "Repository.GetParticipantId"
|
||||
|
||||
var participantId int32
|
||||
err := r.db.GetContext(ctx, &participantId, GetParticipantIdQuery, userId, contestId)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return participantId, nil
|
||||
}
|
||||
|
||||
const GetParticipantId2Query = "SELECT p.id FROM participants p JOIN tasks t ON p.contest_id=t.contest_id WHERE user_id=$1 AND t.id=$2 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error) {
|
||||
const op = "Repository.GetParticipantId2"
|
||||
|
||||
var participantId int32
|
||||
err := r.db.GetContext(ctx, &participantId, GetParticipantId2Query, userId, taskId)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return participantId, nil
|
||||
}
|
||||
|
||||
const GetParticipantId3Query = "SELECT participant_id FROM solutions WHERE id=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) {
|
||||
const op = "Repository.GetParticipantId3"
|
||||
|
||||
var participantId int32
|
||||
err := r.db.GetContext(ctx, &participantId, GetParticipantId3Query, solutionId)
|
||||
if err != nil {
|
||||
return 0, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return participantId, nil
|
||||
}
|
||||
|
||||
const CreateParticipantQuery = "INSERT INTO participants (user_id, contest_id, name) VALUES ($1, $2, $3) RETURNING id"
|
||||
|
||||
func (r *Repository) CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) {
|
||||
const op = "Repository.CreateParticipant"
|
||||
|
||||
name := ""
|
||||
rows, err := r.db.QueryxContext(ctx, CreateParticipantQuery, userId, contestId, name)
|
||||
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, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const DeleteParticipantQuery = "DELETE FROM participants WHERE id=$1"
|
||||
|
||||
const (
|
||||
UpdateParticipantQuery = "UPDATE participants SET name = COALESCE($1, name) WHERE id = $2"
|
||||
)
|
||||
|
||||
func (r *Repository) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
|
||||
const op = "Repository.UpdateParticipant"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, UpdateParticipantQuery, participantUpdate.Name, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteParticipant(ctx context.Context, participantId int32) error {
|
||||
const op = "Repository.DeleteParticipant"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, DeleteParticipantQuery, participantId)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ReadParticipantsListQuery = `SELECT id, user_id, name, created_at, updated_at FROM participants WHERE contest_id = $1 LIMIT $2 OFFSET $3`
|
||||
CountParticipantsQuery = "SELECT COUNT(*) FROM participants WHERE contest_id = $1"
|
||||
)
|
||||
|
||||
func (r *Repository) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
|
||||
const op = "Repository.ReadParticipants"
|
||||
|
||||
var participants []*models.ParticipantsListItem
|
||||
err := r.db.SelectContext(ctx, &participants,
|
||||
ReadParticipantsListQuery, filter.ContestId, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var count int32
|
||||
err = r.db.GetContext(ctx, &count, CountParticipantsQuery, filter.ContestId)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ParticipantsList{
|
||||
Participants: participants,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRepository_CreateParticipant(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var (
|
||||
expectedId int32 = 1
|
||||
userId int32 = 2
|
||||
contestId int32 = 3
|
||||
)
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectQuery(repository.CreateParticipantQuery).
|
||||
WithArgs(userId, contestId).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
|
||||
|
||||
id, err := repo.CreateParticipant(ctx, contestId, userId)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedId, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteParticipant(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 participantId int32 = 1
|
||||
|
||||
mock.ExpectExec(repository.DeleteParticipantQuery).
|
||||
WithArgs(participantId).WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteParticipant(ctx, participantId)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
222
internal/contests/repository/solutions_pg_repository.go
Normal file
222
internal/contests/repository/solutions_pg_repository.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
const (
|
||||
GetSolutionQuery = "SELECT * FROM solutions WHERE id = $1"
|
||||
)
|
||||
|
||||
func (r *Repository) GetSolution(ctx context.Context, id int32) (*models.Solution, error) {
|
||||
const op = "Repository.GetSolution"
|
||||
|
||||
var solution models.Solution
|
||||
err := r.db.GetContext(ctx, &solution, GetSolutionQuery, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &solution, nil
|
||||
}
|
||||
|
||||
const (
|
||||
CreateSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id`
|
||||
)
|
||||
|
||||
func (r *Repository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
|
||||
const op = "Repository.CreateSolution"
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx,
|
||||
CreateSolutionQuery,
|
||||
creation.TaskId,
|
||||
creation.ParticipantId,
|
||||
creation.Language,
|
||||
creation.Penalty,
|
||||
creation.Solution,
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
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.StatementBuilder.PlaceholderFormat(sq.Dollar).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(sq.Eq{"s.contest_id": *filter.ContestId})
|
||||
}
|
||||
if filter.ParticipantId != nil {
|
||||
qb = qb.Where(sq.Eq{"s.participant_id": *filter.ParticipantId})
|
||||
}
|
||||
if filter.TaskId != nil {
|
||||
qb = qb.Where(sq.Eq{"s.task_id": *filter.TaskId})
|
||||
}
|
||||
if filter.Language != nil {
|
||||
qb = qb.Where(sq.Eq{"s.language": *filter.Language})
|
||||
}
|
||||
if filter.State != nil {
|
||||
qb = qb.Where(sq.Eq{"s.state": *filter.State})
|
||||
}
|
||||
|
||||
countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub")
|
||||
|
||||
if filter.Order != nil && *filter.Order < 0 {
|
||||
qb = qb.OrderBy("s.id DESC")
|
||||
} else {
|
||||
qb = qb.OrderBy("s.id ASC")
|
||||
}
|
||||
|
||||
qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset()))
|
||||
|
||||
return qb, countQb
|
||||
}
|
||||
|
||||
func (r *Repository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
|
||||
const op = "ContestRepository.ListSolutions"
|
||||
|
||||
baseQb, countQb := buildListSolutionsQueries(filter)
|
||||
|
||||
query, args, err := countQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var totalCount int32
|
||||
err = r.db.GetContext(ctx, &totalCount, query, args...)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
query, args, err = baseQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
rows, err := r.db.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
solutions := make([]*models.SolutionsListItem, 0)
|
||||
for rows.Next() {
|
||||
var solution models.SolutionsListItem
|
||||
err = rows.StructScan(&solution)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
solutions = append(solutions, &solution)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.SolutionsList{
|
||||
Solutions: solutions,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(totalCount, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// state=5 - AC
|
||||
GetBestSolutions = `
|
||||
WITH contest_tasks AS (
|
||||
SELECT t.id AS task_id,
|
||||
t.position AS task_position,
|
||||
t.contest_id,
|
||||
t.problem_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
p.title AS task_title,
|
||||
c.title AS contest_title
|
||||
FROM tasks t
|
||||
LEFT JOIN problems p ON p.id = t.problem_id
|
||||
LEFT JOIN contests c ON c.id = t.contest_id
|
||||
WHERE t.contest_id = ?
|
||||
),
|
||||
best_solutions AS (
|
||||
SELECT DISTINCT ON (s.task_id)
|
||||
*
|
||||
FROM solutions s
|
||||
WHERE s.participant_id = ?
|
||||
ORDER BY s.task_id, s.score DESC, s.created_at DESC
|
||||
)
|
||||
SELECT
|
||||
s.id,
|
||||
s.participant_id,
|
||||
p.name AS participant_name,
|
||||
s.state,
|
||||
s.score,
|
||||
s.penalty,
|
||||
s.time_stat,
|
||||
s.memory_stat,
|
||||
s.language,
|
||||
ct.task_id,
|
||||
ct.task_position,
|
||||
ct.task_title,
|
||||
ct.contest_id,
|
||||
ct.contest_title,
|
||||
s.updated_at,
|
||||
s.created_at
|
||||
FROM contest_tasks ct
|
||||
LEFT JOIN best_solutions s ON s.task_id = ct.task_id
|
||||
LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL
|
||||
ORDER BY ct.task_position
|
||||
`
|
||||
)
|
||||
|
||||
func (r *Repository) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) {
|
||||
const op = "Repository.GetBestSolutions"
|
||||
var solutions []*models.SolutionsListItem
|
||||
query := r.db.Rebind(GetBestSolutions)
|
||||
err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId)
|
||||
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return solutions, nil
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package repository
|
101
internal/contests/repository/tasks_pg_repository.go
Normal file
101
internal/contests/repository/tasks_pg_repository.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
const CreateTaskQuery = `INSERT INTO tasks (problem_id, contest_id, position)
|
||||
VALUES ($1, $2, COALESCE((SELECT MAX(position) FROM tasks WHERE contest_id = $2), 0) + 1)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
func (r *Repository) CreateTask(ctx context.Context, contestId int32, problemId int32) (int32, error) {
|
||||
const op = "Repository.AddTask"
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, CreateTaskQuery, problemId, contestId)
|
||||
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 DeleteTaskQuery = "DELETE FROM tasks WHERE id=$1"
|
||||
|
||||
func (r *Repository) DeleteTask(ctx context.Context, taskId int32) error {
|
||||
const op = "Repository.DeleteTask"
|
||||
|
||||
_, err := r.db.ExecContext(ctx, DeleteTaskQuery, taskId)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const GetTasksQuery = `SELECT tasks.id,
|
||||
problem_id,
|
||||
contest_id,
|
||||
position,
|
||||
title,
|
||||
memory_limit,
|
||||
time_limit,
|
||||
tasks.created_at,
|
||||
tasks.updated_at
|
||||
FROM tasks
|
||||
INNER JOIN problems ON tasks.problem_id = problems.id
|
||||
WHERE contest_id = $1 ORDER BY position`
|
||||
|
||||
func (r *Repository) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
|
||||
const op = "Repository.ReadTasks"
|
||||
|
||||
var tasks []*models.TasksListItem
|
||||
err := r.db.SelectContext(ctx, &tasks, GetTasksQuery, contestId)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
const (
|
||||
GetTaskQuery = `
|
||||
SELECT
|
||||
t.id,
|
||||
t.position,
|
||||
p.title,
|
||||
p.time_limit,
|
||||
p.memory_limit,
|
||||
t.problem_id,
|
||||
t.contest_id,
|
||||
p.legend_html,
|
||||
p.input_format_html,
|
||||
p.output_format_html,
|
||||
p.notes_html,
|
||||
p.scoring_html,
|
||||
t.created_at,
|
||||
t.updated_at
|
||||
FROM tasks t
|
||||
LEFT JOIN problems p ON t.problem_id = p.id
|
||||
WHERE t.id = ?
|
||||
`
|
||||
)
|
||||
|
||||
func (r *Repository) GetTask(ctx context.Context, id int32) (*models.Task, error) {
|
||||
const op = "Repository.ReadTask"
|
||||
|
||||
query := r.db.Rebind(GetTaskQuery)
|
||||
var task models.Task
|
||||
err := r.db.GetContext(ctx, &task, query, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &task, nil
|
||||
}
|
51
internal/contests/repository/tasks_pg_repository_test.go
Normal file
51
internal/contests/repository/tasks_pg_repository_test.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRepository_CreateTask(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var (
|
||||
expectedId int32 = 1
|
||||
problemId int32 = 2
|
||||
contestId int32 = 3
|
||||
)
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectQuery(repository.CreateTaskQuery).
|
||||
WithArgs(problemId, contestId).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
|
||||
|
||||
id, err := repo.CreateTask(ctx, contestId, problemId)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedId, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteTask(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectExec(repository.DeleteTaskQuery).
|
||||
WithArgs(1).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteTask(ctx, 1)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
34
internal/contests/usecase.go
Normal file
34
internal/contests/usecase.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package contests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
CreateContest(ctx context.Context, title string) (int32, error)
|
||||
GetContest(ctx context.Context, id int32) (*models.Contest, error)
|
||||
DeleteContest(ctx context.Context, id int32) error
|
||||
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
|
||||
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
|
||||
|
||||
CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
|
||||
DeleteTask(ctx context.Context, taskId int32) error
|
||||
GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
|
||||
GetTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
|
||||
CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error)
|
||||
GetParticipantId3(ctx context.Context, solutionId int32) (int32, error)
|
||||
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
|
||||
DeleteParticipant(ctx context.Context, participantId int32) error
|
||||
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
|
||||
|
||||
GetSolution(ctx context.Context, id int32) (*models.Solution, error)
|
||||
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
|
||||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error)
|
||||
|
||||
GetMonitor(ctx context.Context, id int32) (*models.Monitor, error)
|
||||
}
|
39
internal/contests/usecase/contests_usecase.go
Normal file
39
internal/contests/usecase/contests_usecase.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/contests"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type ContestUseCase struct {
|
||||
contestRepo contests.Repository
|
||||
}
|
||||
|
||||
func NewContestUseCase(
|
||||
contestRepo contests.Repository,
|
||||
) *ContestUseCase {
|
||||
return &ContestUseCase{
|
||||
contestRepo: contestRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateContest(ctx context.Context, title string) (int32, error) {
|
||||
return uc.contestRepo.CreateContest(ctx, title)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetContest(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
return uc.contestRepo.GetContest(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
|
||||
return uc.contestRepo.UpdateContest(ctx, id, contestUpdate)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteContest(ctx context.Context, id int32) error {
|
||||
return uc.contestRepo.DeleteContest(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
|
||||
return uc.contestRepo.ListContests(ctx, filter)
|
||||
}
|
10
internal/contests/usecase/monitor_usecase.go
Normal file
10
internal/contests/usecase/monitor_usecase.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func (uc *ContestUseCase) GetMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
|
||||
return uc.contestRepo.GetMonitor(ctx, contestId, 20)
|
||||
}
|
34
internal/contests/usecase/participants_usecase.go
Normal file
34
internal/contests/usecase/participants_usecase.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func (uc *ContestUseCase) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) {
|
||||
return uc.contestRepo.GetParticipantId(ctx, contestId, userId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error) {
|
||||
return uc.contestRepo.GetParticipantId2(ctx, taskId, userId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) {
|
||||
return uc.contestRepo.GetParticipantId3(ctx, solutionId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateParticipant(ctx context.Context, contestId int32, userId int32) (id int32, err error) {
|
||||
return uc.contestRepo.CreateParticipant(ctx, contestId, userId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteParticipant(ctx context.Context, participantId int32) error {
|
||||
return uc.contestRepo.DeleteParticipant(ctx, participantId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
|
||||
return uc.contestRepo.ListParticipants(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
|
||||
return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate)
|
||||
}
|
29
internal/contests/usecase/solutions_usecase.go
Normal file
29
internal/contests/usecase/solutions_usecase.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func (uc *ContestUseCase) GetSolution(ctx context.Context, id int32) (*models.Solution, error) {
|
||||
return uc.contestRepo.GetSolution(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
|
||||
participantId, err := uc.contestRepo.GetParticipantId2(ctx, creation.TaskId, creation.UserId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
creation.ParticipantId = participantId
|
||||
|
||||
return uc.contestRepo.CreateSolution(ctx, creation)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
|
||||
return uc.contestRepo.ListSolutions(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) {
|
||||
return uc.contestRepo.GetBestSolutions(ctx, contestId, participantId)
|
||||
}
|
22
internal/contests/usecase/tasks_usecase.go
Normal file
22
internal/contests/usecase/tasks_usecase.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
func (uc *ContestUseCase) CreateTask(ctx context.Context, contestId int32, taskId int32) (id int32, err error) {
|
||||
return uc.contestRepo.CreateTask(ctx, contestId, taskId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetTask(ctx context.Context, id int32) (*models.Task, error) {
|
||||
return uc.contestRepo.GetTask(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
|
||||
return uc.contestRepo.GetTasks(ctx, contestId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteTask(ctx context.Context, taskId int32) error {
|
||||
return uc.contestRepo.DeleteTask(ctx, taskId)
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
package rest
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"strings"
|
||||
|
@ -12,19 +15,15 @@ const (
|
|||
TokenKey = "token"
|
||||
)
|
||||
|
||||
func AuthMiddleware(jwtSecret string) fiber.Handler {
|
||||
func AuthMiddleware(jwtSecret string, sessionsUC sessions.UseCase) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
const op = "AuthMiddleware"
|
||||
|
||||
authHeader := c.Get("Authorization", "")
|
||||
if authHeader == "" {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
authParts := strings.Split(authHeader, " ")
|
||||
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "bearer" {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
|
@ -36,34 +35,30 @@ func AuthMiddleware(jwtSecret string) fiber.Handler {
|
|||
return []byte(jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
token, ok := parsedToken.Claims.(*models.JWT)
|
||||
if !ok {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
err = token.Valid()
|
||||
if err != nil {
|
||||
c.Locals(TokenKey, nil)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
//ctx := c.Context()
|
||||
ctx := c.Context()
|
||||
|
||||
// check if session exists
|
||||
//_, err = userUC.ReadSession(ctx, token.SessionId)
|
||||
//if err != nil {
|
||||
// if errors.Is(err, pkg.ErrNotFound) {
|
||||
// c.Locals(TokenKey, nil)
|
||||
// return c.Next()
|
||||
// }
|
||||
//
|
||||
// return c.SendStatus(pkg.ToREST(err))
|
||||
//}
|
||||
_, err = sessionsUC.ReadSession(ctx, token.SessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, pkg.ErrNotFound) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
c.Locals(TokenKey, token)
|
||||
return c.Next()
|
|
@ -24,6 +24,8 @@ type ContestsList struct {
|
|||
type ContestsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
UserId *int32
|
||||
Order *int32
|
||||
}
|
||||
|
||||
func (f ContestsFilter) Offset() int32 {
|
||||
|
@ -53,3 +55,73 @@ type ProblemStatSummary struct {
|
|||
Success int32 `db:"success"`
|
||||
Total int32 `db:"total"`
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Id int32 `db:"id"`
|
||||
Position int32 `db:"position"`
|
||||
Title string `db:"title"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
|
||||
ProblemId int32 `db:"problem_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
|
||||
LegendHtml string `db:"legend_html"`
|
||||
InputFormatHtml string `db:"input_format_html"`
|
||||
OutputFormatHtml string `db:"output_format_html"`
|
||||
NotesHtml string `db:"notes_html"`
|
||||
ScoringHtml string `db:"scoring_html"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type TasksListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
ProblemId int32 `db:"problem_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Position int32 `db:"position"`
|
||||
Title string `db:"title"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type Participant struct {
|
||||
Id int32 `db:"id"`
|
||||
UserId int32 `db:"user_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Name string `db:"name"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ParticipantsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
UserId int32 `db:"user_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Name string `db:"name"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ParticipantsList struct {
|
||||
Participants []*ParticipantsListItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type ParticipantsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
|
||||
ContestId int32
|
||||
}
|
||||
|
||||
func (f ParticipantsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type ParticipantUpdate struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Participant struct {
|
||||
Id int32 `db:"id"`
|
||||
UserId int32 `db:"user_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Name string `db:"name"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ParticipantsListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
UserId int32 `db:"user_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Name string `db:"name"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type ParticipantsList struct {
|
||||
Participants []*ParticipantsListItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type ParticipantsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
|
||||
ContestId int32
|
||||
}
|
||||
|
||||
func (f ParticipantsFilter) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type ParticipantUpdate struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
|
@ -13,7 +13,6 @@ type Problem struct {
|
|||
OutputFormat string `db:"output_format"`
|
||||
Notes string `db:"notes"`
|
||||
Scoring string `db:"scoring"`
|
||||
LatexSummary string `db:"latex_summary"`
|
||||
|
||||
LegendHtml string `db:"legend_html"`
|
||||
InputFormatHtml string `db:"input_format_html"`
|
||||
|
|
|
@ -1,20 +1,74 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-policy-agent/opa/v1/rego"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Id string `json:"id"`
|
||||
UserId int32 `json:"user_id"`
|
||||
Role Role `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Ip string `json:"ip"`
|
||||
}
|
||||
|
||||
func (s *Session) Valid() error {
|
||||
if uuid.Validate(s.Id) != nil {
|
||||
return errors.New("invalid session id")
|
||||
}
|
||||
if s.UserId == 0 {
|
||||
return errors.New("empty user id")
|
||||
}
|
||||
if s.CreatedAt.IsZero() {
|
||||
return errors.New("empty created at")
|
||||
}
|
||||
if s.ExpiresAt.IsZero() {
|
||||
return errors.New("empty expires at")
|
||||
}
|
||||
if s.UserAgent == "" {
|
||||
return errors.New("empty user agent")
|
||||
}
|
||||
if s.Ip == "" {
|
||||
return errors.New("empty ip")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) JSON() ([]byte, error) {
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
func (s *Session) UserIdHash() string {
|
||||
return sha256string(strconv.FormatInt(int64(s.UserId), 10))
|
||||
}
|
||||
|
||||
func (s *Session) SessionIdHash() string {
|
||||
return sha256string(s.Id)
|
||||
}
|
||||
|
||||
func (s *Session) Key() string {
|
||||
return fmt.Sprintf("userid:%s:sessionid:%s", s.UserIdHash(), s.SessionIdHash())
|
||||
}
|
||||
func sha256string(s string) string {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(s))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
type JWT struct {
|
||||
SessionId string `json:"session_id"`
|
||||
UserId int32 `json:"user_id"`
|
||||
Role Role `json:"role"`
|
||||
ExpiresAt int64 `json:"exp"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
NotBefore int64 `json:"nbf"`
|
||||
Permissions []grant `json:"permissions"`
|
||||
SessionId string `json:"session_id"`
|
||||
UserId int32 `json:"user_id"`
|
||||
Role Role `json:"role"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
}
|
||||
|
||||
func (j JWT) Valid() error {
|
||||
|
@ -24,139 +78,18 @@ func (j JWT) Valid() error {
|
|||
if j.UserId == 0 {
|
||||
return errors.New("empty user id")
|
||||
}
|
||||
if j.ExpiresAt == 0 {
|
||||
return errors.New("empty expires at")
|
||||
}
|
||||
if j.IssuedAt == 0 {
|
||||
return errors.New("empty issued at")
|
||||
}
|
||||
if j.NotBefore == 0 {
|
||||
return errors.New("empty not before")
|
||||
}
|
||||
if len(j.Permissions) == 0 {
|
||||
return errors.New("empty permissions")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Role int32
|
||||
|
||||
const (
|
||||
RoleGuest Role = -1
|
||||
RoleStudent Role = 0
|
||||
RoleTeacher Role = 1
|
||||
RoleAdmin Role = 2
|
||||
)
|
||||
|
||||
func (r Role) String() string {
|
||||
switch r {
|
||||
case RoleGuest:
|
||||
return "guest"
|
||||
case RoleStudent:
|
||||
return "student"
|
||||
case RoleTeacher:
|
||||
return "teacher"
|
||||
case RoleAdmin:
|
||||
return "admin"
|
||||
}
|
||||
|
||||
panic("invalid role")
|
||||
type Credentials struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type Action string
|
||||
|
||||
const (
|
||||
Create Action = "create"
|
||||
Read Action = "read"
|
||||
Update Action = "update"
|
||||
Delete Action = "delete"
|
||||
)
|
||||
|
||||
type Resource string
|
||||
|
||||
const (
|
||||
ResourceAnotherUser Resource = "another-user"
|
||||
ResourceMeUser Resource = "me-user"
|
||||
ResourceListUser Resource = "list-user"
|
||||
|
||||
ResourceOwnSession Resource = "own-session"
|
||||
)
|
||||
|
||||
type grant struct {
|
||||
Action Action `json:"action"`
|
||||
Resource Resource `json:"resource"`
|
||||
}
|
||||
|
||||
var Grants = map[string][]grant{
|
||||
RoleGuest.String(): {},
|
||||
RoleStudent.String(): {
|
||||
{Read, ResourceAnotherUser},
|
||||
{Read, ResourceMeUser},
|
||||
{Update, ResourceOwnSession},
|
||||
{Delete, ResourceOwnSession},
|
||||
},
|
||||
RoleTeacher.String(): {
|
||||
{Create, ResourceAnotherUser},
|
||||
{Read, ResourceAnotherUser},
|
||||
{Read, ResourceMeUser},
|
||||
{Read, ResourceListUser},
|
||||
{Update, ResourceOwnSession},
|
||||
{Delete, ResourceOwnSession},
|
||||
},
|
||||
RoleAdmin.String(): {
|
||||
{Create, ResourceAnotherUser},
|
||||
{Read, ResourceAnotherUser},
|
||||
{Read, ResourceMeUser},
|
||||
{Read, ResourceListUser},
|
||||
{Update, ResourceAnotherUser},
|
||||
{Update, ResourceOwnSession},
|
||||
{Delete, ResourceAnotherUser},
|
||||
{Delete, ResourceOwnSession},
|
||||
},
|
||||
}
|
||||
|
||||
const module = `package app.rbac
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if {
|
||||
some grant in input.role_grants[input.role]
|
||||
|
||||
input.action == grant.action
|
||||
input.resource == grant.resource
|
||||
}
|
||||
`
|
||||
|
||||
var query rego.PreparedEvalQuery
|
||||
|
||||
func (r Role) HasPermission(action Action, resource Resource) bool {
|
||||
ctx := context.TODO()
|
||||
|
||||
input := map[string]interface{}{
|
||||
"action": action,
|
||||
"resource": resource,
|
||||
"role": r.String(),
|
||||
"role_grants": Grants,
|
||||
}
|
||||
|
||||
results, err := query.Eval(ctx, rego.EvalInput(input))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return results.Allowed()
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
ctx := context.TODO()
|
||||
|
||||
query, err = rego.New(
|
||||
rego.Query("data.app.rbac.allow"),
|
||||
rego.Module("ms-auth.rego", module),
|
||||
).PrepareForEval(ctx)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
type Device struct {
|
||||
Ip string
|
||||
UseAgent string
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ type Solution struct {
|
|||
type SolutionCreation struct {
|
||||
Solution string
|
||||
TaskId int32
|
||||
UserId int32
|
||||
ParticipantId int32
|
||||
Language int32
|
||||
Penalty int32
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Task struct {
|
||||
Id int32 `db:"id"`
|
||||
Position int32 `db:"position"`
|
||||
Title string `db:"title"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
|
||||
ProblemId int32 `db:"problem_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
|
||||
LegendHtml string `db:"legend_html"`
|
||||
InputFormatHtml string `db:"input_format_html"`
|
||||
OutputFormatHtml string `db:"output_format_html"`
|
||||
NotesHtml string `db:"notes_html"`
|
||||
ScoringHtml string `db:"scoring_html"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type TasksListItem struct {
|
||||
Id int32 `db:"id"`
|
||||
ProblemId int32 `db:"problem_id"`
|
||||
ContestId int32 `db:"contest_id"`
|
||||
Position int32 `db:"position"`
|
||||
Title string `db:"title"`
|
||||
MemoryLimit int32 `db:"memory_limit"`
|
||||
TimeLimit int32 `db:"time_limit"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
71
internal/models/user.go
Normal file
71
internal/models/user.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Role int32
|
||||
|
||||
type User struct {
|
||||
Id int32 `db:"id"`
|
||||
Username string `db:"username"`
|
||||
HashedPassword string `db:"hashed_pwd"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Role Role `db:"role"`
|
||||
}
|
||||
|
||||
type UserCreation struct {
|
||||
Username string
|
||||
Password string
|
||||
Role Role
|
||||
}
|
||||
|
||||
func (u *UserCreation) HashPassword() error {
|
||||
hpwd, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hpwd)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) IsSamePwd(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type UsersListFilters struct {
|
||||
PageSize int32
|
||||
Page int32
|
||||
}
|
||||
|
||||
func (f UsersListFilters) Offset() int32 {
|
||||
return (f.Page - 1) * f.PageSize
|
||||
}
|
||||
|
||||
type UsersList struct {
|
||||
Users []*User
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type UserUpdate struct {
|
||||
Username *string
|
||||
Role *Role
|
||||
}
|
||||
|
||||
const (
|
||||
RoleGuest Role = -1
|
||||
RoleStudent Role = 0
|
||||
RoleTeacher Role = 1
|
||||
RoleAdmin Role = 2
|
||||
)
|
||||
|
||||
type Grant struct {
|
||||
Action string
|
||||
Resource string
|
||||
}
|
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
|
||||
}
|
|
@ -7,48 +7,48 @@ import (
|
|||
"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/internal/tester"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
type ProblemUseCase struct {
|
||||
problemRepo tester.ProblemPostgresRepository
|
||||
type UseCase struct {
|
||||
problemRepo problems.Repository
|
||||
pandocClient pkg.PandocClient
|
||||
}
|
||||
|
||||
func NewProblemUseCase(
|
||||
problemRepo tester.ProblemPostgresRepository,
|
||||
func NewUseCase(
|
||||
problemRepo problems.Repository,
|
||||
pandocClient pkg.PandocClient,
|
||||
) *ProblemUseCase {
|
||||
return &ProblemUseCase{
|
||||
) *UseCase {
|
||||
return &UseCase{
|
||||
problemRepo: problemRepo,
|
||||
pandocClient: pandocClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) CreateProblem(ctx context.Context, title string) (int32, error) {
|
||||
func (u *UseCase) CreateProblem(ctx context.Context, title string) (int32, error) {
|
||||
return u.problemRepo.CreateProblem(ctx, u.problemRepo.DB(), title)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) {
|
||||
return u.problemRepo.ReadProblemById(ctx, u.problemRepo.DB(), id)
|
||||
func (u *UseCase) GetProblemById(ctx context.Context, id int32) (*models.Problem, error) {
|
||||
return u.problemRepo.GetProblemById(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) DeleteProblem(ctx context.Context, id int32) error {
|
||||
func (u *UseCase) DeleteProblem(ctx context.Context, id int32) error {
|
||||
return u.problemRepo.DeleteProblem(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
func (u *UseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
return u.problemRepo.ListProblems(ctx, u.problemRepo.DB(), filter)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate models.ProblemUpdate) error {
|
||||
if isEmpty(problemUpdate) {
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpd
|
|||
return err
|
||||
}
|
||||
|
||||
problem, err := u.problemRepo.ReadProblemById(ctx, tx, id)
|
||||
problem, err := u.problemRepo.GetProblemById(ctx, tx, id)
|
||||
if err != nil {
|
||||
return errors.Join(err, tx.Rollback())
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ type ProblemProperties struct {
|
|||
MemoryLimit int32 `json:"memoryLimit"`
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) UploadProblem(ctx context.Context, id int32, data []byte) error {
|
||||
func (u *UseCase) UploadProblem(ctx context.Context, id int32, data []byte) error {
|
||||
|
||||
locale := "russian"
|
||||
defaultLocale := "english"
|
||||
|
@ -185,7 +185,7 @@ func (u *ProblemUseCase) UploadProblem(ctx context.Context, id int32, data []byt
|
|||
localeProperties.MemoryLimit /= 1024 * 1024
|
||||
defaultProperties.MemoryLimit /= 1024 * 1024
|
||||
|
||||
var problemUpdate models.ProblemUpdate
|
||||
problemUpdate := &models.ProblemUpdate{}
|
||||
if localeProblem != "" {
|
||||
problemUpdate.Legend = &localeProblem
|
||||
problemUpdate.Title = &localeProperties.Title
|
183
internal/sessions/repository/valkey_repository.go
Normal file
183
internal/sessions/repository/valkey_repository.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/valkey-io/valkey-go"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ValkeyRepository struct {
|
||||
db valkey.Client
|
||||
}
|
||||
|
||||
func NewValkeyRepository(db valkey.Client) *ValkeyRepository {
|
||||
return &ValkeyRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
const SessionLifetime = time.Minute * 40
|
||||
|
||||
func (r *ValkeyRepository) CreateSession(ctx context.Context, session *models.Session) error {
|
||||
const op = "ValkeyRepository.CreateSession"
|
||||
|
||||
data, err := session.JSON()
|
||||
if err != nil {
|
||||
return pkg.Wrap(pkg.ErrInternal, err, op, "cannot marshal session")
|
||||
}
|
||||
|
||||
resp := r.db.Do(ctx, r.db.
|
||||
B().Set().
|
||||
Key(session.Key()).
|
||||
Value(string(data)).
|
||||
Exat(session.ExpiresAt).
|
||||
Build(),
|
||||
)
|
||||
|
||||
err = resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrInternal, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
readSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
|
||||
if #result[2] == 0 then
|
||||
return nil
|
||||
else
|
||||
return redis.call('GET', result[2][1])
|
||||
end`
|
||||
)
|
||||
|
||||
func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
|
||||
const op = "ValkeyRepository.ReadSession"
|
||||
|
||||
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(readSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
|
||||
)
|
||||
|
||||
if err := resp.Error(); err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return nil, pkg.Wrap(pkg.ErrNotFound, err, op, "reading session")
|
||||
}
|
||||
return nil, pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
session := &models.Session{}
|
||||
|
||||
err := resp.DecodeJSON(session)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session storage corrupted")
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
const (
|
||||
updateSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
|
||||
return #result[2] > 0 and redis.call('EXPIRE', result[2][1], ARGV[2]) == 1`
|
||||
)
|
||||
|
||||
var (
|
||||
sessionLifetimeString = strconv.Itoa(int(SessionLifetime.Seconds()))
|
||||
)
|
||||
|
||||
func (r *ValkeyRepository) UpdateSession(ctx context.Context, sessionId string) error {
|
||||
const op = "ValkeyRepository.UpdateSession"
|
||||
|
||||
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(updateSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash), sessionLifetimeString},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const deleteSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
|
||||
return #result[2] > 0 and redis.call('DEL', result[2][1]) == 1`
|
||||
|
||||
func (r *ValkeyRepository) DeleteSession(ctx context.Context, sessionId string) error {
|
||||
const op = "ValkeyRepository.DeleteSession"
|
||||
|
||||
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(deleteSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
deleteUserSessionsScript = `local cursor = 0
|
||||
local dels = 0
|
||||
repeat
|
||||
local result = redis.call('SCAN', cursor, 'MATCH', ARGV[1])
|
||||
for _,key in ipairs(result[2]) do
|
||||
redis.call('DEL', key)
|
||||
dels = dels + 1
|
||||
end
|
||||
cursor = tonumber(result[1])
|
||||
until cursor == 0
|
||||
return dels`
|
||||
)
|
||||
|
||||
func (r *ValkeyRepository) DeleteAllSessions(ctx context.Context, userId int32) error {
|
||||
const op = "ValkeyRepository.DeleteAllSessions"
|
||||
|
||||
userIdHash := (&models.Session{UserId: userId}).UserIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(deleteUserSessionsScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:%s:sessionid:*", userIdHash)},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
312
internal/sessions/repository/valkey_repository_test.go
Normal file
312
internal/sessions/repository/valkey_repository_test.go
Normal file
|
@ -0,0 +1,312 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions/repository"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valkey-io/valkey-go"
|
||||
"github.com/valkey-io/valkey-go/mock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValkeyRepository_CreateSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: 1,
|
||||
Role: models.RoleAdmin,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(repository.SessionLifetime),
|
||||
UserAgent: "Mozilla/5.0",
|
||||
Ip: "127.0.0.1",
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "SET" {
|
||||
return false
|
||||
}
|
||||
if cmd[1] != session.Key() {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != "EXAT" {
|
||||
return false
|
||||
}
|
||||
if cmd[4] != strconv.FormatInt(session.ExpiresAt.Unix(), 10) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.CreateSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_ReadSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: 1,
|
||||
Role: models.RoleAdmin,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(repository.SessionLifetime),
|
||||
UserAgent: "Mozilla/5.0",
|
||||
Ip: "127.0.0.1",
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
fmt.Println(cmd)
|
||||
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
d, err := session.JSON()
|
||||
require.NoError(t, err)
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyString(string(d))))
|
||||
res, err := sessionRepo.ReadSession(ctx, session.Id)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(res.CreatedAt.Unix(), res.ExpiresAt.UnixNano())
|
||||
fmt.Println(session.CreatedAt.Unix(), session.ExpiresAt.UnixNano())
|
||||
require.EqualExportedValues(t, session, res)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
res, err := sessionRepo.ReadSession(ctx, session.Id)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
require.Empty(t, res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_UpdateSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.UpdateSession(ctx, session.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
err := sessionRepo.UpdateSession(ctx, session.Id)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_DeleteSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.DeleteSession(ctx, session.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
err := sessionRepo.DeleteSession(ctx, session.Id)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_DeleteAllSessions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
UserId: 1,
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
fmt.Println(cmd)
|
||||
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.DeleteAllSessions(ctx, session.UserId)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
UserId: 1,
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
err := sessionRepo.DeleteAllSessions(ctx, session.UserId)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
})
|
||||
}
|
14
internal/sessions/usecase.go
Normal file
14
internal/sessions/usecase.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
CreateSession(ctx context.Context, creation *models.Session) error
|
||||
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
|
||||
UpdateSession(ctx context.Context, sessionId string) error
|
||||
DeleteSession(ctx context.Context, sessionId string) error
|
||||
DeleteAllSessions(ctx context.Context, userId int32) error
|
||||
}
|
78
internal/sessions/usecase/usecase.go
Normal file
78
internal/sessions/usecase/usecase.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/config"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
type SessionsUC struct {
|
||||
sessionsRepo sessions.ValkeyRepository
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
func NewUseCase(
|
||||
sessionRepo sessions.ValkeyRepository,
|
||||
cfg config.Config,
|
||||
) *SessionsUC {
|
||||
return &SessionsUC{
|
||||
sessionsRepo: sessionRepo,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSession is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *SessionsUC) CreateSession(ctx context.Context, creation *models.Session) error {
|
||||
const op = "UseCase.CreateSession"
|
||||
|
||||
err := u.sessionsRepo.CreateSession(ctx, creation)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot create session")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadSession is for internal use only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *SessionsUC) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
|
||||
const op = "UseCase.ReadSession"
|
||||
|
||||
session, err := u.sessionsRepo.ReadSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "cannot read session")
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (u *SessionsUC) UpdateSession(ctx context.Context, sessionId string) error {
|
||||
const op = "UseCase.UpdateSession"
|
||||
|
||||
err := u.sessionsRepo.UpdateSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot update session")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SessionsUC) DeleteSession(ctx context.Context, sessionId string) error {
|
||||
const op = "UseCase.DeleteSession"
|
||||
|
||||
err := u.sessionsRepo.DeleteSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot delete session")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SessionsUC) DeleteAllSessions(ctx context.Context, userId int32) error {
|
||||
const op = "UseCase.DeleteAllSessions"
|
||||
|
||||
err := u.sessionsRepo.DeleteAllSessions(ctx, userId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot delete all sessions")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
14
internal/sessions/valkey_repository.go
Normal file
14
internal/sessions/valkey_repository.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type ValkeyRepository interface {
|
||||
CreateSession(ctx context.Context, creation *models.Session) error
|
||||
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
|
||||
UpdateSession(ctx context.Context, sessionId string) error
|
||||
DeleteSession(ctx context.Context, sessionId string) error
|
||||
DeleteAllSessions(ctx context.Context, userId int32) error
|
||||
}
|
|
@ -1,622 +0,0 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
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/tester"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"io"
|
||||
)
|
||||
|
||||
type TesterHandlers struct {
|
||||
problemsUC tester.ProblemUseCase
|
||||
contestsUC tester.ContestUseCase
|
||||
}
|
||||
|
||||
func NewTesterHandlers(problemsUC tester.ProblemUseCase, contestsUC tester.ContestUseCase) *TesterHandlers {
|
||||
return &TesterHandlers{
|
||||
problemsUC: problemsUC,
|
||||
contestsUC: contestsUC,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error {
|
||||
contestsList, err := h.contestsUC.ListContests(c.Context(), models.ContestsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListContestsResponse{
|
||||
Contests: make([]testerv1.ContestsListItem, len(contestsList.Contests)),
|
||||
Pagination: P2P(contestsList.Pagination),
|
||||
}
|
||||
|
||||
for i, contest := range contestsList.Contests {
|
||||
resp.Contests[i] = CLI2CLI(*contest)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error {
|
||||
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: P2P(problemsList.Pagination),
|
||||
}
|
||||
|
||||
for i, problem := range problemsList.Problems {
|
||||
resp.Problems[i] = PLI2PLI(*problem)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) CreateContest(c *fiber.Ctx) error {
|
||||
id, err := h.contestsUC.CreateContest(c.Context(), "Название контеста")
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateContestResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) DeleteContest(c *fiber.Ctx, id int32) error {
|
||||
err := h.contestsUC.DeleteContest(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetContest(c *fiber.Ctx, id int32) error {
|
||||
contest, err := h.contestsUC.ReadContestById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
//token, ok := c.Locals(TokenKey).(*models.JWT)
|
||||
//if !ok {
|
||||
// return c.SendStatus(fiber.StatusUnauthorized)
|
||||
//}
|
||||
|
||||
tasks, err := h.contestsUC.ReadTasks(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
var participantId int32 = 2
|
||||
|
||||
solutions, err := h.contestsUC.ReadBestSolutions(c.Context(), id, participantId)
|
||||
|
||||
m := make(map[int32]*models.Solution)
|
||||
|
||||
for i := 0; i < len(solutions); i++ {
|
||||
m[solutions[i].TaskPosition] = solutions[i]
|
||||
}
|
||||
resp := testerv1.GetContestResponse{
|
||||
Contest: C2C(*contest),
|
||||
Tasks: make([]struct {
|
||||
Solution testerv1.Solution `json:"solution"`
|
||||
Task testerv1.TasksListItem `json:"task"`
|
||||
}, len(tasks)),
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
solution := testerv1.Solution{}
|
||||
if sol, ok := m[task.Position]; ok {
|
||||
solution = S2S(*sol)
|
||||
}
|
||||
resp.Tasks[i] = struct {
|
||||
Solution testerv1.Solution `json:"solution"`
|
||||
Task testerv1.TasksListItem `json:"task"`
|
||||
}{
|
||||
Solution: solution,
|
||||
Task: TLI2TLI(*task),
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error {
|
||||
err := h.contestsUC.DeleteParticipant(c.Context(), params.ParticipantId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) AddParticipant(c *fiber.Ctx, params testerv1.AddParticipantParams) error {
|
||||
id, err := h.contestsUC.AddParticipant(c.Context(), params.ContestId, params.UserId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.AddParticipantResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) DeleteTask(c *fiber.Ctx, id int32) error {
|
||||
err := h.contestsUC.DeleteTask(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error {
|
||||
id, err := h.contestsUC.AddTask(c.Context(), params.ContestId, params.ProblemId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.AddTaskResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) CreateProblem(c *fiber.Ctx) error {
|
||||
id, err := h.problemsUC.CreateProblem(c.Context(), "Название задачи")
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateProblemResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) DeleteProblem(c *fiber.Ctx, id int32) error {
|
||||
err := h.problemsUC.DeleteProblem(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetProblem(c *fiber.Ctx, id int32) error {
|
||||
problem, err := h.problemsUC.ReadProblemById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(
|
||||
testerv1.GetProblemResponse{Problem: *PR2PR(problem)},
|
||||
)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error {
|
||||
participantsList, err := h.contestsUC.ListParticipants(c.Context(), models.ParticipantsFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
ContestId: params.ContestId,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListParticipantsResponse{
|
||||
Participants: make([]testerv1.ParticipantsListItem, len(participantsList.Participants)),
|
||||
Pagination: P2P(participantsList.Pagination),
|
||||
}
|
||||
|
||||
for i, participant := range participantsList.Participants {
|
||||
resp.Participants[i] = PTLI2PTLI(*participant)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) UpdateProblem(c *fiber.Ctx, id int32) error {
|
||||
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)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) UploadProblem(c *fiber.Ctx, id int32) error {
|
||||
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
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) UpdateContest(c *fiber.Ctx, id int32) error {
|
||||
var req testerv1.UpdateContestRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.contestsUC.UpdateContest(c.Context(), id, models.ContestUpdate{
|
||||
Title: req.Title,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error {
|
||||
var req testerv1.UpdateParticipantRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.contestsUC.UpdateParticipant(c.Context(), params.ParticipantId, models.ParticipantUpdate{
|
||||
Name: req.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error {
|
||||
solutionsList, err := h.contestsUC.ListSolutions(c.Context(), models.SolutionsFilter{
|
||||
ContestId: params.ContestId,
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
ParticipantId: params.ParticipantId,
|
||||
TaskId: params.TaskId,
|
||||
Language: params.Language,
|
||||
Order: params.Order,
|
||||
State: params.State,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListSolutionsResponse{
|
||||
Solutions: make([]testerv1.SolutionsListItem, len(solutionsList.Solutions)),
|
||||
Pagination: P2P(solutionsList.Pagination),
|
||||
}
|
||||
|
||||
for i, solution := range solutionsList.Solutions {
|
||||
resp.Solutions[i] = SLI2SLI(*solution)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
const (
|
||||
maxSolutionSize int64 = 10 * 1024 * 1024
|
||||
)
|
||||
|
||||
func (h *TesterHandlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error {
|
||||
s, err := c.FormFile("solution")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Size == 0 || s.Size > maxSolutionSize {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
f, err := s.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := h.contestsUC.CreateSolution(c.Context(), &models.SolutionCreation{
|
||||
TaskId: params.TaskId,
|
||||
ParticipantId: 1,
|
||||
Language: params.Language,
|
||||
Penalty: 0,
|
||||
Solution: string(b),
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateSolutionResponse{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetSolution(c *fiber.Ctx, id int32) error {
|
||||
solution, err := h.contestsUC.ReadSolution(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(
|
||||
testerv1.GetSolutionResponse{Solution: S2S(*solution)},
|
||||
)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetTask(c *fiber.Ctx, id int32) error {
|
||||
contest, err := h.contestsUC.ReadContestById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.ReadTasks(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
t, err := h.contestsUC.ReadTask(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.GetTaskResponse{
|
||||
Contest: C2C(*contest),
|
||||
Tasks: make([]testerv1.TasksListItem, len(tasks)),
|
||||
Task: *T2T(t),
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
resp.Tasks[i] = TLI2TLI(*task)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func (h *TesterHandlers) GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error {
|
||||
contest, err := h.contestsUC.ReadContestById(c.Context(), params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
monitor, err := h.contestsUC.ReadMonitor(c.Context(), params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
tasks, err := h.contestsUC.ReadTasks(c.Context(), params.ContestId)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.GetMonitorResponse{
|
||||
Contest: C2C(*contest),
|
||||
Tasks: make([]testerv1.TasksListItem, len(tasks)),
|
||||
Participants: make([]testerv1.ParticipantsStat, len(monitor.Participants)),
|
||||
SummaryPerProblem: make([]testerv1.ProblemStatSummary, len(monitor.Summary)),
|
||||
}
|
||||
|
||||
for i, participant := range monitor.Participants {
|
||||
resp.Participants[i] = testerv1.ParticipantsStat{
|
||||
Id: participant.Id,
|
||||
Name: participant.Name,
|
||||
PenaltyInTotal: participant.PenaltyInTotal,
|
||||
Solutions: make([]testerv1.SolutionsListItem, len(participant.Solutions)),
|
||||
SolvedInTotal: participant.SolvedInTotal,
|
||||
}
|
||||
|
||||
for j, solution := range participant.Solutions {
|
||||
resp.Participants[i].Solutions[j] = SLI2SLI(*solution)
|
||||
}
|
||||
}
|
||||
|
||||
for i, problem := range monitor.Summary {
|
||||
resp.SummaryPerProblem[i] = testerv1.ProblemStatSummary{
|
||||
Id: problem.Id,
|
||||
Success: problem.Success,
|
||||
Total: problem.Total,
|
||||
}
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
resp.Tasks[i] = TLI2TLI(*task)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func P2P(p models.Pagination) testerv1.Pagination {
|
||||
return testerv1.Pagination{
|
||||
Page: p.Page,
|
||||
Total: p.Total,
|
||||
}
|
||||
}
|
||||
|
||||
func C2C(c models.Contest) testerv1.Contest {
|
||||
return testerv1.Contest{
|
||||
Id: c.Id,
|
||||
Title: c.Title,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func CLI2CLI(c models.ContestsListItem) testerv1.ContestsListItem {
|
||||
return testerv1.ContestsListItem{
|
||||
Id: c.Id,
|
||||
Title: c.Title,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func PLI2PLI(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 TLI2TLI(t models.TasksListItem) testerv1.TasksListItem {
|
||||
return testerv1.TasksListItem{
|
||||
Id: t.Id,
|
||||
Position: t.Position,
|
||||
Title: t.Title,
|
||||
MemoryLimit: t.MemoryLimit,
|
||||
ProblemId: t.ProblemId,
|
||||
TimeLimit: t.TimeLimit,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func T2T(t *models.Task) *testerv1.Task {
|
||||
return &testerv1.Task{
|
||||
Id: t.Id,
|
||||
Title: t.Title,
|
||||
MemoryLimit: t.MemoryLimit,
|
||||
TimeLimit: t.TimeLimit,
|
||||
|
||||
InputFormatHtml: t.InputFormatHtml,
|
||||
LegendHtml: t.LegendHtml,
|
||||
NotesHtml: t.NotesHtml,
|
||||
OutputFormatHtml: t.OutputFormatHtml,
|
||||
Position: t.Position,
|
||||
ScoringHtml: t.ScoringHtml,
|
||||
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func PR2PR(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,
|
||||
}
|
||||
}
|
||||
|
||||
func PTLI2PTLI(p models.ParticipantsListItem) testerv1.ParticipantsListItem {
|
||||
return testerv1.ParticipantsListItem{
|
||||
Id: p.Id,
|
||||
UserId: p.UserId,
|
||||
Name: p.Name,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func SLI2SLI(s models.SolutionsListItem) testerv1.SolutionsListItem {
|
||||
return testerv1.SolutionsListItem{
|
||||
Id: s.Id,
|
||||
|
||||
ParticipantId: s.ParticipantId,
|
||||
ParticipantName: s.ParticipantName,
|
||||
|
||||
State: s.State,
|
||||
Score: s.Score,
|
||||
Penalty: s.Penalty,
|
||||
TimeStat: s.TimeStat,
|
||||
MemoryStat: s.MemoryStat,
|
||||
Language: s.Language,
|
||||
|
||||
TaskId: s.TaskId,
|
||||
TaskPosition: s.TaskPosition,
|
||||
TaskTitle: s.TaskTitle,
|
||||
|
||||
ContestId: s.ContestId,
|
||||
ContestTitle: s.ContestTitle,
|
||||
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func S2S(s models.Solution) testerv1.Solution {
|
||||
return testerv1.Solution{
|
||||
Id: s.Id,
|
||||
|
||||
ParticipantId: s.ParticipantId,
|
||||
ParticipantName: s.ParticipantName,
|
||||
|
||||
Solution: s.Solution,
|
||||
|
||||
State: s.State,
|
||||
Score: s.Score,
|
||||
Penalty: s.Penalty,
|
||||
TimeStat: s.TimeStat,
|
||||
MemoryStat: s.MemoryStat,
|
||||
Language: s.Language,
|
||||
|
||||
TaskId: s.TaskId,
|
||||
TaskPosition: s.TaskPosition,
|
||||
TaskTitle: s.TaskTitle,
|
||||
|
||||
ContestId: s.ContestId,
|
||||
ContestTitle: s.ContestTitle,
|
||||
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package tester
|
||||
|
||||
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 ProblemPostgresRepository interface {
|
||||
BeginTx(ctx context.Context) (Tx, error)
|
||||
DB() Querier
|
||||
CreateProblem(ctx context.Context, q Querier, title string) (int32, error)
|
||||
ReadProblemById(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
|
||||
}
|
||||
|
||||
type ContestRepository interface {
|
||||
CreateContest(ctx context.Context, title string) (int32, error)
|
||||
ReadContestById(ctx context.Context, id int32) (*models.Contest, error)
|
||||
DeleteContest(ctx context.Context, id int32) error
|
||||
AddTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
|
||||
DeleteTask(ctx context.Context, taskId int32) error
|
||||
AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
DeleteParticipant(ctx context.Context, participantId int32) error
|
||||
ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
|
||||
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
|
||||
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
|
||||
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
|
||||
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
|
||||
ReadSolution(ctx context.Context, id int32) (*models.Solution, error)
|
||||
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
|
||||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
ReadTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error)
|
||||
ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
func handlePgErr(err error, op string) error {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) {
|
||||
return pkg.Wrap(pkg.ErrBadInput, err, op, pgErr.Message)
|
||||
}
|
||||
if pgerrcode.IsNoData(pgErr.Code) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, pgErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "no rows found")
|
||||
}
|
||||
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unexpected error")
|
||||
}
|
|
@ -1,690 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ContestRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewContestRepository(db *sqlx.DB) *ContestRepository {
|
||||
return &ContestRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
const createContestQuery = "INSERT INTO contests (title) VALUES (?) RETURNING id"
|
||||
|
||||
func (r *ContestRepository) CreateContest(ctx context.Context, title string) (int32, error) {
|
||||
const op = "ContestRepository.CreateContest"
|
||||
|
||||
query := r.db.Rebind(createContestQuery)
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, query, title)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const readContestByIdQuery = "SELECT * from contests WHERE id=? LIMIT 1"
|
||||
|
||||
func (r *ContestRepository) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
const op = "ContestRepository.ReadContestById"
|
||||
|
||||
var contest models.Contest
|
||||
query := r.db.Rebind(readContestByIdQuery)
|
||||
err := r.db.GetContext(ctx, &contest, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
return &contest, nil
|
||||
}
|
||||
|
||||
const deleteContestQuery = "DELETE FROM contests WHERE id=?"
|
||||
|
||||
func (r *ContestRepository) DeleteContest(ctx context.Context, id int32) error {
|
||||
const op = "ContestRepository.DeleteContest"
|
||||
|
||||
query := r.db.Rebind(deleteContestQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const addTaskQuery = `INSERT INTO tasks (problem_id, contest_id, position)
|
||||
VALUES (?, ?, COALESCE((SELECT MAX(position) FROM tasks WHERE contest_id = ?), 0) + 1)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
func (r *ContestRepository) AddTask(ctx context.Context, contestId int32, problemId int32) (int32, error) {
|
||||
const op = "ContestRepository.AddTask"
|
||||
|
||||
query := r.db.Rebind(addTaskQuery)
|
||||
rows, err := r.db.QueryxContext(ctx, query, problemId, contestId, contestId)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const deleteTaskQuery = "DELETE FROM tasks WHERE id=?"
|
||||
|
||||
func (r *ContestRepository) DeleteTask(ctx context.Context, taskId int32) error {
|
||||
const op = "ContestRepository.DeleteTask"
|
||||
|
||||
query := r.db.Rebind(deleteTaskQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, taskId)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const addParticipantQuery = "INSERT INTO participants (user_id ,contest_id, name) VALUES (?, ?, ?) RETURNING id"
|
||||
|
||||
func (r *ContestRepository) AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) {
|
||||
const op = "ContestRepository.AddParticipant"
|
||||
|
||||
query := r.db.Rebind(addParticipantQuery)
|
||||
name := ""
|
||||
rows, err := r.db.QueryxContext(ctx, query, contestId, userId, name)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const deleteParticipantQuery = "DELETE FROM participants WHERE id=?"
|
||||
|
||||
func (r *ContestRepository) DeleteParticipant(ctx context.Context, participantId int32) error {
|
||||
const op = "ContestRepository.DeleteParticipant"
|
||||
|
||||
query := r.db.Rebind(deleteParticipantQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, participantId)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const readTasksQuery = `SELECT tasks.id,
|
||||
problem_id,
|
||||
contest_id,
|
||||
position,
|
||||
title,
|
||||
memory_limit,
|
||||
time_limit,
|
||||
tasks.created_at,
|
||||
tasks.updated_at
|
||||
FROM tasks
|
||||
INNER JOIN problems ON tasks.problem_id = problems.id
|
||||
WHERE contest_id = ? ORDER BY position`
|
||||
|
||||
func (r *ContestRepository) ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
|
||||
const op = "ContestRepository.ReadTasks"
|
||||
|
||||
var tasks []*models.TasksListItem
|
||||
query := r.db.Rebind(readTasksQuery)
|
||||
err := r.db.SelectContext(ctx, &tasks, query, contestId)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
const (
|
||||
readContestsListQuery = `SELECT id, title, created_at, updated_at FROM contests LIMIT ? OFFSET ?`
|
||||
countContestsQuery = "SELECT COUNT(*) FROM contests"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
|
||||
const op = "ContestRepository.ReadTasks"
|
||||
|
||||
var contests []*models.ContestsListItem
|
||||
query := r.db.Rebind(readContestsListQuery)
|
||||
err := r.db.SelectContext(ctx, &contests, query, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
query = r.db.Rebind(countContestsQuery)
|
||||
var count int32
|
||||
err = r.db.GetContext(ctx, &count, query)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ContestsList{
|
||||
Contests: contests,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
readParticipantsListQuery = `SELECT id, user_id, name, created_at, updated_at FROM participants WHERE contest_id = ? LIMIT ? OFFSET ?`
|
||||
countParticipantsQuery = "SELECT COUNT(*) FROM participants WHERE contest_id = ?"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
|
||||
const op = "ContestRepository.ReadParticipants"
|
||||
|
||||
if filter.PageSize > 20 {
|
||||
filter.PageSize = 1
|
||||
}
|
||||
|
||||
var participants []*models.ParticipantsListItem
|
||||
query := r.db.Rebind(readParticipantsListQuery)
|
||||
err := r.db.SelectContext(ctx, &participants, query, filter.ContestId, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
query = r.db.Rebind(countParticipantsQuery)
|
||||
var count int32
|
||||
err = r.db.GetContext(ctx, &count, query, filter.ContestId)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ParticipantsList{
|
||||
Participants: participants,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
updateContestQuery = "UPDATE contests SET title = COALESCE(?, title) WHERE id = ?"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
|
||||
const op = "ContestRepository.UpdateContest"
|
||||
|
||||
query := r.db.Rebind(updateContestQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, contestUpdate.Title, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
updateParticipantQuery = "UPDATE participants SET name = COALESCE(?, name) WHERE id = ?"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
|
||||
const op = "ContestRepository.UpdateParticipant"
|
||||
|
||||
query := r.db.Rebind(updateParticipantQuery)
|
||||
_, err := r.db.ExecContext(ctx, query, participantUpdate.Name, id)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
readSolutionQuery = "SELECT * FROM solutions WHERE id = ?"
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) {
|
||||
const op = "ContestRepository.ReadSolution"
|
||||
|
||||
query := r.db.Rebind(readSolutionQuery)
|
||||
var solution models.Solution
|
||||
err := r.db.GetContext(ctx, &solution, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &solution, nil
|
||||
}
|
||||
|
||||
const (
|
||||
createSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id`
|
||||
)
|
||||
|
||||
func (r *ContestRepository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
|
||||
const op = "ContestRepository.CreateSolution"
|
||||
|
||||
query := r.db.Rebind(createSolutionQuery)
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx,
|
||||
query,
|
||||
creation.TaskId,
|
||||
creation.ParticipantId,
|
||||
creation.Language,
|
||||
creation.Penalty,
|
||||
creation.Solution,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (r *ContestRepository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
|
||||
const op = "ContestRepository.ListSolutions"
|
||||
|
||||
baseQuery := `
|
||||
SELECT 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
|
||||
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)
|
||||
}
|
||||
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)
|
||||
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...)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var solutions []*models.SolutionsListItem
|
||||
for rows.Next() {
|
||||
var solution models.SolutionsListItem
|
||||
err = rows.StructScan(&solution)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
solutions = append(solutions, &solution)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.SolutionsList{
|
||||
Solutions: solutions,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(totalCount, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
readTaskQuery = `
|
||||
SELECT
|
||||
t.id,
|
||||
t.position,
|
||||
p.title,
|
||||
p.time_limit,
|
||||
p.memory_limit,
|
||||
t.problem_id,
|
||||
t.contest_id,
|
||||
p.legend_html,
|
||||
p.input_format_html,
|
||||
p.output_format_html,
|
||||
p.notes_html,
|
||||
p.scoring_html,
|
||||
t.created_at,
|
||||
t.updated_at
|
||||
FROM tasks t
|
||||
LEFT JOIN problems p ON t.problem_id = p.id
|
||||
WHERE t.id = ?
|
||||
`
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ReadTask(ctx context.Context, id int32) (*models.Task, error) {
|
||||
const op = "ContestRepository.ReadTask"
|
||||
|
||||
query := r.db.Rebind(readTaskQuery)
|
||||
var task models.Task
|
||||
err := r.db.GetContext(ctx, &task, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// state=5 - AC
|
||||
readStatisticsQuery = `
|
||||
SELECT t.id as task_id,
|
||||
t.position,
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN s.state = 5 THEN 1 END) as success
|
||||
FROM tasks t
|
||||
LEFT JOIN solutions s ON t.id = s.task_id
|
||||
WHERE t.contest_id = ?
|
||||
GROUP BY t.id, t.position
|
||||
ORDER BY t.position;
|
||||
`
|
||||
|
||||
solutionsQuery = `
|
||||
WITH RankedSolutions AS (
|
||||
SELECT
|
||||
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 as contest_title,
|
||||
|
||||
s.updated_at,
|
||||
s.created_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY s.participant_id, s.task_id
|
||||
ORDER BY
|
||||
CASE WHEN s.state = 5 THEN 0 ELSE 1 END,
|
||||
s.created_at
|
||||
) as rn
|
||||
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 t.contest_id = ?
|
||||
)
|
||||
SELECT
|
||||
rs.id,
|
||||
|
||||
rs.participant_id,
|
||||
rs.participant_name,
|
||||
|
||||
rs.state,
|
||||
rs.score,
|
||||
rs.penalty,
|
||||
rs.time_stat,
|
||||
rs.memory_stat,
|
||||
rs.language,
|
||||
|
||||
rs.task_id,
|
||||
rs.task_position,
|
||||
rs.task_title,
|
||||
|
||||
rs.contest_id,
|
||||
rs.contest_title,
|
||||
|
||||
rs.updated_at,
|
||||
rs.created_at
|
||||
FROM RankedSolutions rs
|
||||
WHERE rs.rn = 1;
|
||||
|
||||
`
|
||||
|
||||
participantsQuery = `
|
||||
WITH Attempts AS (
|
||||
SELECT
|
||||
s.participant_id,
|
||||
s.task_id,
|
||||
COUNT(*) FILTER (WHERE s.state != 5 AND s.created_at < (
|
||||
SELECT MIN(s2.created_at)
|
||||
FROM solutions s2
|
||||
WHERE s2.participant_id = s.participant_id
|
||||
AND s2.task_id = s.task_id
|
||||
AND s2.state = 5
|
||||
)) as failed_attempts,
|
||||
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
|
||||
GROUP BY s.participant_id, s.task_id
|
||||
)
|
||||
SELECT
|
||||
p.id,
|
||||
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
|
||||
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
|
||||
GROUP BY p.id, p.name
|
||||
`
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
|
||||
const op = "ContestRepository.ReadMonitor"
|
||||
|
||||
query := r.db.Rebind(readStatisticsQuery)
|
||||
rows, err := r.db.QueryxContext(ctx, query, contestId)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var monitor models.Monitor
|
||||
for rows.Next() {
|
||||
var stat models.ProblemStatSummary
|
||||
err = rows.StructScan(&stat)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
monitor.Summary = append(monitor.Summary, &stat)
|
||||
}
|
||||
|
||||
var solutions []*models.SolutionsListItem
|
||||
err = r.db.SelectContext(ctx, &solutions, r.db.Rebind(solutionsQuery), contestId)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
penalty := int32(20) // FIXME
|
||||
namedQuery := r.db.Rebind(participantsQuery)
|
||||
rows3, err := r.db.NamedQueryContext(ctx, namedQuery, map[string]interface{}{
|
||||
"contest_id": contestId,
|
||||
"penalty": penalty,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
defer rows3.Close()
|
||||
|
||||
solutionsMap := make(map[int32][]*models.SolutionsListItem)
|
||||
for _, solution := range solutions {
|
||||
solutionsMap[solution.ParticipantId] = append(solutionsMap[solution.ParticipantId], solution)
|
||||
}
|
||||
|
||||
for rows3.Next() {
|
||||
var stat models.ParticipantsStat
|
||||
err = rows3.StructScan(&stat)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
if sols, ok := solutionsMap[stat.Id]; ok {
|
||||
stat.Solutions = sols
|
||||
}
|
||||
|
||||
monitor.Participants = append(monitor.Participants, &stat)
|
||||
}
|
||||
|
||||
return &monitor, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// state=5 - AC
|
||||
readBestSolutions = `
|
||||
WITH contest_tasks AS (
|
||||
SELECT t.id AS task_id,
|
||||
t.position AS task_position,
|
||||
t.contest_id,
|
||||
t.problem_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
p.title AS task_title,
|
||||
c.title AS contest_title
|
||||
FROM tasks t
|
||||
LEFT JOIN problems p ON p.id = t.problem_id
|
||||
LEFT JOIN contests c ON c.id = t.contest_id
|
||||
WHERE t.contest_id = ?
|
||||
),
|
||||
best_solutions AS (
|
||||
SELECT DISTINCT ON (s.task_id)
|
||||
*
|
||||
FROM solutions s
|
||||
WHERE s.participant_id = ?
|
||||
ORDER BY s.task_id, s.score DESC, s.created_at DESC
|
||||
)
|
||||
SELECT
|
||||
s.id,
|
||||
s.participant_id,
|
||||
p.name AS participant_name,
|
||||
s.solution,
|
||||
s.state,
|
||||
s.score,
|
||||
s.penalty,
|
||||
s.time_stat,
|
||||
s.memory_stat,
|
||||
s.language,
|
||||
ct.task_id,
|
||||
ct.task_position,
|
||||
ct.task_title,
|
||||
ct.contest_id,
|
||||
ct.contest_title,
|
||||
s.updated_at,
|
||||
s.created_at
|
||||
FROM contest_tasks ct
|
||||
LEFT JOIN best_solutions s ON s.task_id = ct.task_id
|
||||
LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL
|
||||
ORDER BY ct.task_position
|
||||
`
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) {
|
||||
const op = "ContestRepository.ReadBestSolutions"
|
||||
var solutions []*models.Solution
|
||||
query := r.db.Rebind(readBestSolutions)
|
||||
err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId)
|
||||
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return solutions, nil
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContestRepository_CreateContest(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 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)
|
||||
|
||||
id, err := contestRepo.CreateContest(context.Background(), title)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), id)
|
||||
})
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
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)
|
||||
|
||||
err = 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)
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
err = 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())
|
||||
|
||||
t.Run("valid participant addition", func(t *testing.T) {
|
||||
contestId := int32(1)
|
||||
userId := int32(1)
|
||||
name := ""
|
||||
|
||||
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)
|
||||
|
||||
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())
|
||||
|
||||
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)
|
||||
|
||||
err = contestRepo.DeleteParticipant(context.Background(), id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/tester"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type ProblemRepository struct {
|
||||
_db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewProblemRepository(db *sqlx.DB) *ProblemRepository {
|
||||
return &ProblemRepository{
|
||||
_db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProblemRepository) BeginTx(ctx context.Context) (tester.Tx, error) {
|
||||
tx, err := r._db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (r *ProblemRepository) DB() tester.Querier {
|
||||
return r._db
|
||||
}
|
||||
|
||||
const createProblemQuery = "INSERT INTO problems (title) VALUES (?) RETURNING id"
|
||||
|
||||
func (r *ProblemRepository) CreateProblem(ctx context.Context, q tester.Querier, title string) (int32, error) {
|
||||
const op = "ProblemRepository.CreateProblem"
|
||||
|
||||
query := q.Rebind(createProblemQuery)
|
||||
rows, err := q.QueryxContext(ctx, query, title)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var id int32
|
||||
rows.Next()
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const readProblemQuery = "SELECT * from problems WHERE id=? LIMIT 1"
|
||||
|
||||
func (r *ProblemRepository) ReadProblemById(ctx context.Context, q tester.Querier, id int32) (*models.Problem, error) {
|
||||
const op = "ProblemRepository.ReadProblemById"
|
||||
|
||||
var problem models.Problem
|
||||
query := q.Rebind(readProblemQuery)
|
||||
err := q.GetContext(ctx, &problem, query, id)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &problem, nil
|
||||
}
|
||||
|
||||
const deleteProblemQuery = "DELETE FROM problems WHERE id=?"
|
||||
|
||||
func (r *ProblemRepository) DeleteProblem(ctx context.Context, q tester.Querier, id int32) error {
|
||||
const op = "ProblemRepository.DeleteProblem"
|
||||
|
||||
query := q.Rebind(deleteProblemQuery)
|
||||
_, err := q.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return 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 ? OFFSET ?`
|
||||
CountProblemsQuery = "SELECT COUNT(*) FROM problems"
|
||||
)
|
||||
|
||||
func (r *ProblemRepository) ListProblems(ctx context.Context, q tester.Querier, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
const op = "ContestRepository.ListProblems"
|
||||
|
||||
if filter.PageSize > 20 || filter.PageSize < 1 {
|
||||
filter.PageSize = 1
|
||||
}
|
||||
|
||||
var problems []*models.ProblemsListItem
|
||||
query := q.Rebind(ListProblemsQuery)
|
||||
err := q.SelectContext(ctx, &problems, query, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
query = q.Rebind(CountProblemsQuery)
|
||||
|
||||
var count int32
|
||||
err = q.GetContext(ctx, &count, query)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.ProblemsList{
|
||||
Problems: problems,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filter.PageSize),
|
||||
Page: filter.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
UpdateProblemQuery = `UPDATE problems
|
||||
SET title = COALESCE(?, title),
|
||||
time_limit = COALESCE(?, time_limit),
|
||||
memory_limit = COALESCE(?, memory_limit),
|
||||
|
||||
legend = COALESCE(?, legend),
|
||||
input_format = COALESCE(?, input_format),
|
||||
output_format = COALESCE(?, output_format),
|
||||
notes = COALESCE(?, notes),
|
||||
scoring = COALESCE(?, scoring),
|
||||
|
||||
legend_html = COALESCE(?, legend_html),
|
||||
input_format_html = COALESCE(?, input_format_html),
|
||||
output_format_html = COALESCE(?, output_format_html),
|
||||
notes_html = COALESCE(?, notes_html),
|
||||
scoring_html = COALESCE(?, scoring_html)
|
||||
|
||||
WHERE id=?`
|
||||
)
|
||||
|
||||
func (r *ProblemRepository) UpdateProblem(ctx context.Context, q tester.Querier, id int32, problem models.ProblemUpdate) error {
|
||||
const op = "ProblemRepository.UpdateProblem"
|
||||
|
||||
query := q.Rebind(UpdateProblemQuery)
|
||||
_, err := q.ExecContext(ctx, query,
|
||||
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,
|
||||
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
return handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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()
|
||||
|
||||
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())
|
||||
|
||||
t.Run("valid problem creation", func(t *testing.T) {
|
||||
title := "Problem title"
|
||||
|
||||
rows := sqlmock.NewRows([]string{"id"}).AddRow(1)
|
||||
|
||||
mock.ExpectQuery(sqlxDB.Rebind(createProblemQuery)).WithArgs(title).WillReturnRows(rows)
|
||||
|
||||
id, err := problemRepo.CreateProblem(context.Background(), title)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), id)
|
||||
})
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
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)
|
||||
|
||||
err = problemRepo.DeleteProblem(context.Background(), id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package tester
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type ProblemUseCase interface {
|
||||
CreateProblem(ctx context.Context, title string) (int32, error)
|
||||
ReadProblemById(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
|
||||
}
|
||||
|
||||
type ContestUseCase interface {
|
||||
CreateContest(ctx context.Context, title string) (int32, error)
|
||||
ReadContestById(ctx context.Context, id int32) (*models.Contest, error)
|
||||
DeleteContest(ctx context.Context, id int32) error
|
||||
AddTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
|
||||
DeleteTask(ctx context.Context, taskId int32) error
|
||||
AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
|
||||
DeleteParticipant(ctx context.Context, participantId int32) error
|
||||
ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
|
||||
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
|
||||
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
|
||||
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
|
||||
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
|
||||
ReadSolution(ctx context.Context, id int32) (*models.Solution, error)
|
||||
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
|
||||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
ReadTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error)
|
||||
ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error)
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/tester"
|
||||
)
|
||||
|
||||
type ContestUseCase struct {
|
||||
contestRepo tester.ContestRepository
|
||||
}
|
||||
|
||||
func NewContestUseCase(
|
||||
contestRepo tester.ContestRepository,
|
||||
) *ContestUseCase {
|
||||
return &ContestUseCase{
|
||||
contestRepo: contestRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateContest(ctx context.Context, title string) (int32, error) {
|
||||
return uc.contestRepo.CreateContest(ctx, title)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
|
||||
return uc.contestRepo.ReadContestById(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteContest(ctx context.Context, id int32) error {
|
||||
return uc.contestRepo.DeleteContest(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) AddTask(ctx context.Context, contestId int32, taskId int32) (id int32, err error) {
|
||||
return uc.contestRepo.AddTask(ctx, contestId, taskId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteTask(ctx context.Context, taskId int32) error {
|
||||
return uc.contestRepo.DeleteTask(ctx, taskId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) AddParticipant(ctx context.Context, contestId int32, userId int32) (id int32, err error) {
|
||||
return uc.contestRepo.AddParticipant(ctx, contestId, userId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) DeleteParticipant(ctx context.Context, participantId int32) error {
|
||||
return uc.contestRepo.DeleteParticipant(ctx, participantId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
|
||||
return uc.contestRepo.ReadTasks(ctx, contestId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
|
||||
return uc.contestRepo.ListContests(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
|
||||
return uc.contestRepo.ListParticipants(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
|
||||
return uc.contestRepo.UpdateContest(ctx, id, contestUpdate)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
|
||||
return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) {
|
||||
return uc.contestRepo.ReadSolution(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
|
||||
return uc.contestRepo.CreateSolution(ctx, creation)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
|
||||
return uc.contestRepo.ListSolutions(ctx, filter)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadTask(ctx context.Context, id int32) (*models.Task, error) {
|
||||
return uc.contestRepo.ReadTask(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
|
||||
return uc.contestRepo.ReadMonitor(ctx, contestId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) {
|
||||
return uc.contestRepo.ReadBestSolutions(ctx, contestId, participantId)
|
||||
}
|
14
internal/users/delivery.go
Normal file
14
internal/users/delivery.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type UsersHandlers interface {
|
||||
ListUsers(c *fiber.Ctx, params testerv1.ListUsersParams) error
|
||||
CreateUser(c *fiber.Ctx) error
|
||||
DeleteUser(c *fiber.Ctx, id int32) error
|
||||
GetUser(c *fiber.Ctx, id int32) error
|
||||
UpdateUser(c *fiber.Ctx, id int32) error
|
||||
}
|
204
internal/users/delivery/rest/handlers.go
Normal file
204
internal/users/delivery/rest/handlers.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
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/users"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
usersUC users.UseCase
|
||||
}
|
||||
|
||||
func NewHandlers(usersUC users.UseCase) *Handlers {
|
||||
return &Handlers{
|
||||
usersUC: usersUC,
|
||||
}
|
||||
}
|
||||
|
||||
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 (h *Handlers) CreateUser(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:
|
||||
var req = &testerv1.CreateUserRequest{}
|
||||
err := c.BodyParser(req)
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
id, err := h.usersUC.CreateUser(ctx,
|
||||
&models.UserCreation{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Role: models.RoleStudent,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.CreateUserResponse{Id: id})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) GetUser(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, models.RoleStudent:
|
||||
user, err := h.usersUC.ReadUserById(c.Context(), id)
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
return c.JSON(testerv1.GetUserResponse{
|
||||
User: UserDTO(*user),
|
||||
})
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handlers) UpdateUser(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:
|
||||
var req = &testerv1.UpdateUserRequest{}
|
||||
err := c.BodyParser(req)
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
err = h.usersUC.UpdateUser(c.Context(), id, &models.UserUpdate{
|
||||
Username: req.Username,
|
||||
Role: RoleDTO(req.Role),
|
||||
})
|
||||
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) DeleteUser(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:
|
||||
ctx := c.Context()
|
||||
|
||||
err := h.usersUC.DeleteUser(ctx, 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) ListUsers(c *fiber.Ctx, params testerv1.ListUsersParams) 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:
|
||||
usersList, err := h.usersUC.ListUsers(c.Context(), models.UsersListFilters{
|
||||
PageSize: params.PageSize,
|
||||
Page: params.Page,
|
||||
})
|
||||
if err != nil {
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
resp := testerv1.ListUsersResponse{
|
||||
Users: make([]testerv1.User, len(usersList.Users)),
|
||||
Pagination: PaginationDTO(usersList.Pagination),
|
||||
}
|
||||
|
||||
for i, user := range usersList.Users {
|
||||
resp.Users[i] = UserDTO(*user)
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
default:
|
||||
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
|
||||
}
|
||||
}
|
||||
|
||||
func RoleDTO(i *int32) *models.Role {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
ii := models.Role(*i)
|
||||
return &ii
|
||||
}
|
||||
|
||||
func PaginationDTO(p models.Pagination) testerv1.Pagination {
|
||||
return testerv1.Pagination{
|
||||
Page: p.Page,
|
||||
Total: p.Total,
|
||||
}
|
||||
}
|
||||
|
||||
// UserDTO sanitizes password
|
||||
func UserDTO(u models.User) testerv1.User {
|
||||
return testerv1.User{
|
||||
Id: u.Id,
|
||||
Username: u.Username,
|
||||
Role: int32(u.Role),
|
||||
CreatedAt: u.CreatedAt,
|
||||
ModifiedAt: u.UpdatedAt,
|
||||
}
|
||||
}
|
33
internal/users/pg_repository.go
Normal file
33
internal/users/pg_repository.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package users
|
||||
|
||||
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
|
||||
CreateUser(ctx context.Context, q Querier, user *models.UserCreation) (int32, error)
|
||||
ReadUserByUsername(ctx context.Context, q Querier, username string) (*models.User, error)
|
||||
ReadUserById(ctx context.Context, q Querier, id int32) (*models.User, error)
|
||||
UpdateUser(ctx context.Context, q Querier, id int32, update *models.UserUpdate) error
|
||||
DeleteUser(ctx context.Context, q Querier, id int32) error
|
||||
ListUsers(ctx context.Context, q Querier, filters models.UsersListFilters) (*models.UsersList, error)
|
||||
}
|
156
internal/users/repository/pg_repository.go
Normal file
156
internal/users/repository/pg_repository.go
Normal file
|
@ -0,0 +1,156 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"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) (users.Tx, error) {
|
||||
tx, err := r._db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (r *Repository) DB() users.Querier {
|
||||
return r._db
|
||||
}
|
||||
|
||||
const CreateUserQuery = `
|
||||
INSERT INTO users
|
||||
(username, hashed_pwd, role)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
func (r *Repository) CreateUser(ctx context.Context, q users.Querier, user *models.UserCreation) (int32, error) {
|
||||
const op = "Caller.CreateUser"
|
||||
|
||||
rows, err := q.QueryxContext(
|
||||
ctx,
|
||||
CreateUserQuery,
|
||||
user.Username,
|
||||
user.Password,
|
||||
user.Role,
|
||||
)
|
||||
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 ReadUserByUsernameQuery = "SELECT * from users WHERE username=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) ReadUserByUsername(ctx context.Context, q users.Querier, username string) (*models.User, error) {
|
||||
const op = "Caller.ReadUserByUsername"
|
||||
|
||||
var user models.User
|
||||
err := q.GetContext(ctx, &user, ReadUserByUsernameQuery, username)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
const ReadUserByIdQuery = "SELECT * from users WHERE id=$1 LIMIT 1"
|
||||
|
||||
func (r *Repository) ReadUserById(ctx context.Context, q users.Querier, id int32) (*models.User, error) {
|
||||
const op = "Caller.ReadUserById"
|
||||
|
||||
var user models.User
|
||||
err := q.GetContext(ctx, &user, ReadUserByIdQuery, id)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
const UpdateUserQuery = `
|
||||
UPDATE users
|
||||
SET username = COALESCE($1, username),
|
||||
role = COALESCE($2, role)
|
||||
WHERE id = $3
|
||||
`
|
||||
|
||||
func (r *Repository) UpdateUser(ctx context.Context, q users.Querier, id int32, update *models.UserUpdate) error {
|
||||
const op = "Caller.UpdateUser"
|
||||
|
||||
_, err := q.ExecContext(
|
||||
ctx,
|
||||
UpdateUserQuery,
|
||||
update.Username,
|
||||
update.Role,
|
||||
id,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const DeleteUserQuery = "DELETE FROM users WHERE id = $1"
|
||||
|
||||
func (r *Repository) DeleteUser(ctx context.Context, q users.Querier, id int32) error {
|
||||
const op = "Caller.DeleteUser"
|
||||
|
||||
_, err := q.ExecContext(ctx, DeleteUserQuery, id)
|
||||
if err != nil {
|
||||
return pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ListUsersQuery = "SELECT * FROM users LIMIT $1 OFFSET $2"
|
||||
CountUsersQuery = "SELECT COUNT(*) FROM users"
|
||||
)
|
||||
|
||||
func (r *Repository) ListUsers(ctx context.Context, q users.Querier, filters models.UsersListFilters) (*models.UsersList, error) {
|
||||
const op = "Caller.ListUsers"
|
||||
|
||||
list := make([]*models.User, 0)
|
||||
err := q.SelectContext(ctx, &list, ListUsersQuery, filters.PageSize, filters.Offset())
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
var count int32
|
||||
err = q.GetContext(ctx, &count, CountUsersQuery)
|
||||
if err != nil {
|
||||
return nil, pkg.HandlePgErr(err, op)
|
||||
}
|
||||
|
||||
return &models.UsersList{
|
||||
Users: list,
|
||||
Pagination: models.Pagination{
|
||||
Total: models.Total(count, filters.PageSize),
|
||||
Page: filters.Page,
|
||||
},
|
||||
}, nil
|
||||
}
|
222
internal/users/repository/pg_repository_test.go
Normal file
222
internal/users/repository/pg_repository_test.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users/repository"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// 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_CreateUser(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 expectedId int32 = 1
|
||||
user := &models.UserCreation{
|
||||
Username: "testuser",
|
||||
Password: "hashed-password",
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.CreateUserQuery).
|
||||
WithArgs(user.Username, sqlmock.AnyArg(), user.Role).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
|
||||
|
||||
id, err := repo.CreateUser(ctx, db, user)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedId, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_ReadUserByUsername(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.User{
|
||||
Id: 1,
|
||||
Username: "testuser",
|
||||
HashedPassword: "hashed-password",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
|
||||
columns := []string{
|
||||
"id",
|
||||
"username",
|
||||
"hashed_pwd",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"role",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).AddRow(
|
||||
expected.Id,
|
||||
expected.Username,
|
||||
expected.HashedPassword,
|
||||
expected.CreatedAt,
|
||||
expected.UpdatedAt,
|
||||
expected.Role,
|
||||
)
|
||||
|
||||
mock.ExpectQuery(repository.ReadUserByUsernameQuery).WithArgs(expected.Username).WillReturnRows(rows)
|
||||
|
||||
user, err := repo.ReadUserByUsername(ctx, db, expected.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, user)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
username := "testuser"
|
||||
|
||||
mock.ExpectQuery(repository.ReadUserByUsernameQuery).WithArgs(username).WillReturnError(sql.ErrNoRows)
|
||||
|
||||
user, err := repo.ReadUserByUsername(ctx, db, username)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, user)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_ReadUserById(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.User{
|
||||
Id: 1,
|
||||
Username: "testuser",
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
|
||||
mock.ExpectQuery(repository.ReadUserByIdQuery).
|
||||
WithArgs(expected.Id).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "username", "role"}).
|
||||
AddRow(expected.Id, expected.Username, expected.Role))
|
||||
|
||||
user, err := repo.ReadUserById(ctx, db, expected.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, user)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
userID := int32(1)
|
||||
|
||||
mock.ExpectQuery(repository.ReadUserByIdQuery).WithArgs(userID).WillReturnError(sql.ErrNoRows)
|
||||
|
||||
user, err := repo.ReadUserById(ctx, db, userID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, user)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_UpdateUser(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
userID := int32(1)
|
||||
username := "testuser"
|
||||
role := models.RoleStudent
|
||||
update := &models.UserUpdate{
|
||||
Username: &username,
|
||||
Role: &role,
|
||||
}
|
||||
|
||||
mock.ExpectExec(repository.UpdateUserQuery).
|
||||
WithArgs(update.Username, update.Role, userID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.UpdateUser(ctx, db, userID, update)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteUser(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := int32(1)
|
||||
|
||||
mock.ExpectExec(repository.DeleteUserQuery).
|
||||
WithArgs(userID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err := repo.DeleteUser(ctx, db, userID)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_ListUsers(t *testing.T) {
|
||||
db, mock := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewRepository(db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
filters := models.UsersListFilters{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
expectedUsers := []*models.User{
|
||||
{Id: 1, Username: "user1", Role: models.RoleAdmin},
|
||||
{Id: 2, Username: "user2", Role: models.RoleStudent},
|
||||
}
|
||||
totalCount := int32(2)
|
||||
|
||||
mock.ExpectQuery(repository.ListUsersQuery).
|
||||
WithArgs(filters.PageSize, filters.Offset()).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "username", "role"}).
|
||||
AddRow(expectedUsers[0].Id, expectedUsers[0].Username, expectedUsers[0].Role).
|
||||
AddRow(expectedUsers[1].Id, expectedUsers[1].Username, expectedUsers[1].Role))
|
||||
|
||||
mock.ExpectQuery(repository.CountUsersQuery).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalCount))
|
||||
|
||||
result, err := repo.ListUsers(ctx, db, filters)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedUsers, result.Users)
|
||||
assert.Equal(t, models.Pagination{Total: 1, Page: 1}, result.Pagination)
|
||||
})
|
||||
}
|
15
internal/users/usecase.go
Normal file
15
internal/users/usecase.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
CreateUser(ctx context.Context, user *models.UserCreation) (int32, error)
|
||||
ReadUserById(ctx context.Context, id int32) (*models.User, error)
|
||||
ReadUserByUsername(ctx context.Context, username string) (*models.User, error)
|
||||
UpdateUser(ctx context.Context, id int32, update *models.UserUpdate) error
|
||||
DeleteUser(ctx context.Context, id int32) error
|
||||
ListUsers(ctx context.Context, filters models.UsersListFilters) (*models.UsersList, error)
|
||||
}
|
164
internal/users/usecase/usecase.go
Normal file
164
internal/users/usecase/usecase.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/users"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
type UsersUC struct {
|
||||
sessionRepo sessions.ValkeyRepository
|
||||
usersRepo users.Repository
|
||||
}
|
||||
|
||||
func NewUseCase(sessionRepo sessions.ValkeyRepository, usersRepo users.Repository) *UsersUC {
|
||||
return &UsersUC{
|
||||
sessionRepo: sessionRepo,
|
||||
usersRepo: usersRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UsersUC) CreateUser(ctx context.Context, user *models.UserCreation) (int32, error) {
|
||||
const op = "UseCase.CreateUser"
|
||||
|
||||
err := user.HashPassword()
|
||||
if err != nil {
|
||||
return 0, pkg.Wrap(pkg.ErrBadInput, err, op, "bad password")
|
||||
}
|
||||
|
||||
id, err := u.usersRepo.CreateUser(ctx, u.usersRepo.DB(), user)
|
||||
if err != nil {
|
||||
return 0, pkg.Wrap(nil, err, op, "can't create user")
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (u *UsersUC) ListUsers(ctx context.Context, filters models.UsersListFilters) (*models.UsersList, error) {
|
||||
const op = "UseCase.ListUsers"
|
||||
|
||||
usersList, err := u.usersRepo.ListUsers(ctx, u.usersRepo.DB(), filters)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "can't list users")
|
||||
}
|
||||
|
||||
return usersList, nil
|
||||
}
|
||||
|
||||
func (u *UsersUC) UpdateUser(ctx context.Context, id int32, update *models.UserUpdate) error {
|
||||
const op = "UseCase.UpdateUser"
|
||||
|
||||
tx, err := u.usersRepo.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot start transaction")
|
||||
}
|
||||
|
||||
err = u.usersRepo.UpdateUser(ctx, tx, id, update)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot update user")
|
||||
}
|
||||
err = u.sessionRepo.DeleteAllSessions(ctx, id)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete all sessions")
|
||||
}
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadUserByUsername is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *UsersUC) ReadUserByUsername(ctx context.Context, username string) (*models.User, error) {
|
||||
const op = "UseCase.ReadUserByUsername"
|
||||
|
||||
user, err := u.usersRepo.ReadUserByUsername(ctx, u.usersRepo.DB(), username)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "can't read user by username")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (u *UsersUC) ReadUserById(ctx context.Context, id int32) (*models.User, error) {
|
||||
const op = "UseCase.ReadUserById"
|
||||
|
||||
user, err := u.usersRepo.ReadUserById(ctx, u.usersRepo.DB(), id)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "can't read user by id")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (u *UsersUC) DeleteUser(ctx context.Context, id int32) error {
|
||||
const op = "UseCase.DeleteUser"
|
||||
|
||||
tx, err := u.usersRepo.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot start transaction")
|
||||
}
|
||||
|
||||
err = u.usersRepo.DeleteUser(ctx, tx, id)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete user")
|
||||
}
|
||||
|
||||
err = u.sessionRepo.DeleteAllSessions(ctx, id)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete all sessions")
|
||||
}
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
func ValidEmail(str string) error {
|
||||
emailAddress, err := mail.ParseAddress(str)
|
||||
if err != nil || emailAddress.Address != str {
|
||||
return errors.New("invalid email")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidUsername(str string) error {
|
||||
if len(str) < 5 {
|
||||
return errors.New("too short username")
|
||||
}
|
||||
if len(str) > 70 {
|
||||
return errors.New("too long username")
|
||||
}
|
||||
if err := ValidEmail(str); err == nil {
|
||||
return errors.New("username cannot be an email")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidPassword(str string) error {
|
||||
if len(str) < 5 {
|
||||
return errors.New("too short password")
|
||||
}
|
||||
if len(str) > 70 {
|
||||
return errors.New("too long password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidRole(role models.Role) error {
|
||||
switch role {
|
||||
case models.RoleAdmin:
|
||||
return nil
|
||||
case models.RoleTeacher:
|
||||
return nil
|
||||
case models.RoleStudent:
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid role")
|
||||
}
|
||||
*/
|
Loading…
Add table
Add a link
Reference in a new issue