This commit is contained in:
Albert
2025-12-19 23:12:25 +04:00
22 changed files with 290 additions and 109 deletions
+2 -2
View File
@@ -30,8 +30,8 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=${CGO_ENABLED} \
GOOS=${GOOS} \
GOARCH=${GOARCH} \
go build -trimpath \
-ldflags "-s -w -X main.BuildTime=${BUILD_TIME} -X main.Version=${VERSION}" \
go build -a -installsuffix cgo -trimpath \
-ldflags '-extldflags "-static" -s -w -X main.BuildTime=${BUILD_TIME} -X main.Version=${VERSION}' \
-o /out/${SERVICE} ./cmd/${SERVICE} && \
chmod +x /out/${SERVICE}
+1 -1
View File
@@ -29,4 +29,4 @@ func main() {
log.Println("shutting down results server...")
srv.Stop()
log.Println("results server stopped")
}
}
+1 -1
View File
@@ -29,4 +29,4 @@ func main() {
log.Println("shutting down task server...")
srv.Stop()
log.Println("task server stopped")
}
}
+1 -1
View File
@@ -29,4 +29,4 @@ func main() {
log.Println("shutting down user server...")
srv.Stop()
log.Println("user server stopped")
}
}
+5 -5
View File
@@ -28,11 +28,11 @@ func Load() (*Config, error) {
_ = godotenv.Load()
return &Config{
DBHost: getEnv("POSTGRES_HOST", "localhost"),
DBPort: mustGetInt("POSTGRES_PORT", 5432), //nolint:mnd // false-positive
DBUser: getEnv("POSTGRES_USERNAME", "postgres"),
DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"),
DBName: getEnv("POSTGRES_DATABASE", "postgres"),
DBHost: getEnv("POSTGRES_HOST", "localhost"),
DBPort: mustGetInt("POSTGRES_PORT", 5432), //nolint:mnd // false-positive
DBUser: getEnv("POSTGRES_USERNAME", "postgres"),
DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"),
DBName: getEnv("POSTGRES_DATABASE", "postgres"),
}, nil
}
+1 -1
View File
@@ -8,4 +8,4 @@ var (
ErrDuplicateResult = errors.New("duplicate result")
ErrCompetitionNotFound = errors.New("competition not found")
ErrUserNotFound = errors.New("user not found")
)
)
+13 -2
View File
@@ -29,7 +29,18 @@ type Result struct {
UpdatedAt time.Time
}
func NewResult(id uuid.UUID, competitionID uuid.UUID, userID uuid.UUID, score float64, rank *int, status ResultStatus, submissionID *uuid.UUID, metadata map[string]interface{}, createdAt time.Time, updatedAt time.Time) *Result {
func NewResult(
id uuid.UUID,
competitionID uuid.UUID,
userID uuid.UUID,
score float64,
rank *int,
status ResultStatus,
submissionID *uuid.UUID,
metadata map[string]interface{},
createdAt time.Time,
updatedAt time.Time,
) *Result {
return &Result{
ID: id,
CompetitionID: competitionID,
@@ -42,4 +53,4 @@ func NewResult(id uuid.UUID, competitionID uuid.UUID, userID uuid.UUID, score fl
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
}
}
+37 -7
View File
@@ -24,9 +24,23 @@ type IResultService interface {
GetResult(ctx context.Context, id uuid.UUID) (*domain.Result, error)
UpdateResult(ctx context.Context, result *domain.Result) (*domain.Result, error)
DeleteResult(ctx context.Context, id uuid.UUID) error
ListResults(ctx context.Context, pageSize int32, pageToken int32, competitionID *uuid.UUID, userID *uuid.UUID, status *domain.ResultStatus, minScore *float64, maxScore *float64) (results []domain.Result, totalCount int32, nextPageToken int32, err error)
ListResults(
ctx context.Context,
pageSize int32,
pageToken int32,
competitionID *uuid.UUID,
userID *uuid.UUID,
status *domain.ResultStatus,
minScore *float64,
maxScore *float64,
) (results []domain.Result, totalCount int32, nextPageToken int32, err error)
GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error)
GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error)
GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int32,
offset int32,
) ([]domain.Result, int32, error)
UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error)
RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error
}
@@ -43,7 +57,10 @@ type ResultsHandler struct {
mu sync.RWMutex
}
func NewResultsHandler(s IResultService, userServiceAddr, submissionServiceAddr, taskServiceAddr string) *ResultsHandler {
func NewResultsHandler(
s IResultService,
userServiceAddr, submissionServiceAddr, taskServiceAddr string,
) *ResultsHandler {
return &ResultsHandler{
service: s,
userServiceAddr: userServiceAddr,
@@ -210,7 +227,11 @@ func (h *ResultsHandler) fetchTaskStatuses(ctx context.Context, competitionID, u
return taskStatuses
}
func (h *ResultsHandler) enrichUserResult(ctx context.Context, result *domain.Result, competitionID string) *pb.UserResult {
func (h *ResultsHandler) enrichUserResult(
ctx context.Context,
result *domain.Result,
competitionID string,
) *pb.UserResult {
userID := result.UserID.String()
username := h.fetchUsername(ctx, userID)
taskStatuses := h.fetchTaskStatuses(ctx, competitionID, userID)
@@ -230,7 +251,10 @@ func (h *ResultsHandler) enrichUserResult(ctx context.Context, result *domain.Re
return pbResult
}
func (h *ResultsHandler) GetCompetitionResults(ctx context.Context, req *pb.GetCompetitionResultsRequest) (*pb.GetCompetitionResultsResponse, error) {
func (h *ResultsHandler) GetCompetitionResults(
ctx context.Context,
req *pb.GetCompetitionResultsRequest,
) (*pb.GetCompetitionResultsResponse, error) {
competitionID, err := uuid.Parse(req.GetCompetitionId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err)
@@ -267,7 +291,10 @@ func (h *ResultsHandler) GetCompetitionResults(ctx context.Context, req *pb.GetC
}, nil
}
func (h *ResultsHandler) GetUserCompetitionResults(ctx context.Context, req *pb.GetUserCompetitionResultsRequest) (*pb.GetUserCompetitionResultsResponse, error) {
func (h *ResultsHandler) GetUserCompetitionResults(
ctx context.Context,
req *pb.GetUserCompetitionResultsRequest,
) (*pb.GetUserCompetitionResultsResponse, error) {
competitionID, err := uuid.Parse(req.GetCompetitionId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err)
@@ -294,7 +321,10 @@ func (h *ResultsHandler) GetUserCompetitionResults(ctx context.Context, req *pb.
}, nil
}
func (h *ResultsHandler) RecalculateResults(ctx context.Context, req *pb.RecalculateResultsRequest) (*emptypb.Empty, error) {
func (h *ResultsHandler) RecalculateResults(
ctx context.Context,
req *pb.RecalculateResultsRequest,
) (*emptypb.Empty, error) {
competitionID, err := uuid.Parse(req.GetCompetitionId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err)
+26 -4
View File
@@ -53,28 +53,50 @@ func (m *MockResultService) DeleteResult(ctx context.Context, id uuid.UUID) erro
return errors.New("not implemented")
}
func (m *MockResultService) ListResults(ctx context.Context, pageSize int32, pageToken int32, competitionID *uuid.UUID, userID *uuid.UUID, status *domain.ResultStatus, minScore *float64, maxScore *float64) (results []domain.Result, totalCount int32, nextPageToken int32, err error) {
func (m *MockResultService) ListResults(
ctx context.Context,
pageSize int32,
pageToken int32,
competitionID *uuid.UUID,
userID *uuid.UUID,
status *domain.ResultStatus,
minScore *float64,
maxScore *float64,
) (results []domain.Result, totalCount int32, nextPageToken int32, err error) {
if m.ListResultsFunc != nil {
return m.ListResultsFunc(ctx, pageSize, pageToken, competitionID, userID, status, minScore, maxScore)
}
return nil, 0, 0, errors.New("not implemented")
}
func (m *MockResultService) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) {
func (m *MockResultService) GetByCompetitionAndUser(
ctx context.Context,
competitionID uuid.UUID,
userID uuid.UUID,
) (*domain.Result, error) {
if m.GetByCompetitionAndUserFunc != nil {
return m.GetByCompetitionAndUserFunc(ctx, competitionID, userID)
}
return nil, errors.New("not implemented")
}
func (m *MockResultService) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error) {
func (m *MockResultService) GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int32,
offset int32,
) ([]domain.Result, int32, error) {
if m.GetLeaderboardFunc != nil {
return m.GetLeaderboardFunc(ctx, competitionID, limit, offset)
}
return nil, 0, errors.New("not implemented")
}
func (m *MockResultService) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) {
func (m *MockResultService) UpdateStatus(
ctx context.Context,
id uuid.UUID,
status domain.ResultStatus,
) (*domain.Result, error) {
if m.UpdateStatusFunc != nil {
return m.UpdateStatusFunc(ctx, id, status)
}
+26 -7
View File
@@ -3,13 +3,14 @@ package postgres
import (
"context"
"database/sql"
"datarush/internal/results/domain"
"datarush/internal/results/repository"
"encoding/json"
"fmt"
"strings"
"time"
"datarush/internal/results/domain"
"datarush/internal/results/repository"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/redis/go-redis/v9"
@@ -257,8 +258,13 @@ func (r *ResultRepository) List(ctx context.Context, opts repository.ListResults
return nil, 0, err
}
query := fmt.Sprintf(`SELECT id, competition_id, user_id, score, rank, status, submission_id, metadata, created_at, updated_at
FROM results %s ORDER BY score DESC, created_at ASC LIMIT $%d OFFSET $%d`, where, argId, argId+1)
query := fmt.Sprintf(
`SELECT id, competition_id, user_id, score, rank, status, submission_id, metadata, created_at, updated_at
FROM results %s ORDER BY score DESC, created_at ASC LIMIT $%d OFFSET $%d`,
where,
argId,
argId+1,
)
args = append(args, opts.PageSize, opts.Page*opts.PageSize)
rows, err := r.db.QueryContext(ctx, query, args...)
@@ -299,7 +305,11 @@ func (r *ResultRepository) List(ctx context.Context, opts repository.ListResults
return results, total, nil
}
func (r *ResultRepository) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) {
func (r *ResultRepository) GetByCompetitionAndUser(
ctx context.Context,
competitionID uuid.UUID,
userID uuid.UUID,
) (*domain.Result, error) {
query := `SELECT id, competition_id, user_id, score, rank, status, submission_id, metadata, created_at, updated_at
FROM results WHERE competition_id = $1 AND user_id = $2`
row := r.db.QueryRowContext(ctx, query, competitionID, userID)
@@ -334,7 +344,12 @@ func (r *ResultRepository) GetByCompetitionAndUser(ctx context.Context, competit
return &result, nil
}
func (r *ResultRepository) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int, offset int) ([]domain.Result, int, error) {
func (r *ResultRepository) GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int,
offset int,
) ([]domain.Result, int, error) {
if r.cacheEnabled {
key := r.leaderboardCacheKey(competitionID, limit, offset)
val, err := r.redisClient.Get(ctx, key).Result()
@@ -404,7 +419,11 @@ func (r *ResultRepository) GetLeaderboard(ctx context.Context, competitionID uui
return results, total, nil
}
func (r *ResultRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) {
func (r *ResultRepository) UpdateStatus(
ctx context.Context,
id uuid.UUID,
status domain.ResultStatus,
) (*domain.Result, error) {
query := `UPDATE results SET status = $1, updated_at = $2 WHERE id = $3`
now := time.Now()
res, err := r.db.ExecContext(ctx, query, status, now, id)
+2 -1
View File
@@ -2,6 +2,7 @@ package repository
import (
"context"
"datarush/internal/results/domain"
"github.com/google/uuid"
@@ -27,4 +28,4 @@ type ResultRepository interface {
GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int, offset int) ([]domain.Result, int, error)
UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error)
RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error
}
}
+22 -4
View File
@@ -52,7 +52,16 @@ func (s *Service) DeleteResult(ctx context.Context, id uuid.UUID) error {
return s.repo.Delete(ctx, id)
}
func (s *Service) ListResults(ctx context.Context, pageSize int32, pageToken int32, competitionID *uuid.UUID, userID *uuid.UUID, status *domain.ResultStatus, minScore *float64, maxScore *float64) ([]domain.Result, int32, int32, error) {
func (s *Service) ListResults(
ctx context.Context,
pageSize int32,
pageToken int32,
competitionID *uuid.UUID,
userID *uuid.UUID,
status *domain.ResultStatus,
minScore *float64,
maxScore *float64,
) ([]domain.Result, int32, int32, error) {
opts := repository.ListResultsOptions{
Page: int(pageToken),
PageSize: int(pageSize),
@@ -76,11 +85,20 @@ func (s *Service) ListResults(ctx context.Context, pageSize int32, pageToken int
return results, int32(total), nextPageToken, nil
}
func (s *Service) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) {
func (s *Service) GetByCompetitionAndUser(
ctx context.Context,
competitionID uuid.UUID,
userID uuid.UUID,
) (*domain.Result, error) {
return s.repo.GetByCompetitionAndUser(ctx, competitionID, userID)
}
func (s *Service) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error) {
func (s *Service) GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int32,
offset int32,
) ([]domain.Result, int32, error) {
results, total, err := s.repo.GetLeaderboard(ctx, competitionID, int(limit), int(offset))
if err != nil {
return nil, 0, err
@@ -94,4 +112,4 @@ func (s *Service) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.
func (s *Service) RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error {
return s.repo.RecalculateRanks(ctx, competitionID)
}
}
+30 -17
View File
@@ -59,21 +59,34 @@ func (m *MockRepository) List(ctx context.Context, opts repository.ListResultsOp
return nil, 0, errors.New("not implemented")
}
func (m *MockRepository) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) {
func (m *MockRepository) GetByCompetitionAndUser(
ctx context.Context,
competitionID uuid.UUID,
userID uuid.UUID,
) (*domain.Result, error) {
if m.GetByCompetitionAndUserFunc != nil {
return m.GetByCompetitionAndUserFunc(ctx, competitionID, userID)
}
return nil, errors.New("not implemented")
}
func (m *MockRepository) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int, offset int) ([]domain.Result, int, error) {
func (m *MockRepository) GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int,
offset int,
) ([]domain.Result, int, error) {
if m.GetLeaderboardFunc != nil {
return m.GetLeaderboardFunc(ctx, competitionID, limit, offset)
}
return nil, 0, errors.New("not implemented")
}
func (m *MockRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) {
func (m *MockRepository) UpdateStatus(
ctx context.Context,
id uuid.UUID,
status domain.ResultStatus,
) (*domain.Result, error) {
if m.UpdateStatusFunc != nil {
return m.UpdateStatusFunc(ctx, id, status)
}
@@ -423,20 +436,20 @@ func TestListResults(t *testing.T) {
}
tests := []struct {
name string
pageSize int32
pageToken int32
competitionID *uuid.UUID
userID *uuid.UUID
status *domain.ResultStatus
minScore *float64
maxScore *float64
repoResults []domain.Result
repoTotal int
repoError error
expectError bool
expectedTotal int32
expectedNext int32
name string
pageSize int32
pageToken int32
competitionID *uuid.UUID
userID *uuid.UUID
status *domain.ResultStatus
minScore *float64
maxScore *float64
repoResults []domain.Result
repoTotal int
repoError error
expectError bool
expectedTotal int32
expectedNext int32
}{
{
name: "successful list with filters",
+15 -5
View File
@@ -3,8 +3,9 @@ package grpc
import (
"context"
pb "datarush/pkg/api/task"
"google.golang.org/protobuf/types/known/emptypb"
pb "datarush/pkg/api/task"
)
type TaskService interface {
@@ -12,7 +13,10 @@ type TaskService interface {
GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error)
EditTask(ctx context.Context, req *pb.Task) (*pb.Task, error)
DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest) (*emptypb.Empty, error)
ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error)
ListCompetitionTasks(
ctx context.Context,
req *pb.ListCompetitionTasksRequest,
) (*pb.ListCompetitionTasksResponse, error)
GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error)
}
@@ -41,10 +45,16 @@ func (h *TaskHandler) DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest)
return h.service.DeleteTask(ctx, req)
}
func (h *TaskHandler) ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) {
func (h *TaskHandler) ListCompetitionTasks(
ctx context.Context,
req *pb.ListCompetitionTasksRequest,
) (*pb.ListCompetitionTasksResponse, error) {
return h.service.ListCompetitionTasks(ctx, req)
}
func (h *TaskHandler) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) {
func (h *TaskHandler) GetTaskAttachments(
ctx context.Context,
req *pb.GetTaskAttachmentsRequest,
) (*pb.GetTaskAttachmentsResponse, error) {
return h.service.GetTaskAttachments(ctx, req)
}
}
@@ -21,7 +21,8 @@ func (r *TaskRepository) CreateTask(ctx context.Context, t *task.Task) (*task.Ta
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at`
var createdTask task.Task
err := r.db.QueryRowxContext(ctx, query, t.CompetitionId, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type).StructScan(&createdTask)
err := r.db.QueryRowxContext(ctx, query, t.CompetitionId, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type).
StructScan(&createdTask)
if err != nil {
return nil, err
}
@@ -46,7 +47,8 @@ func (r *TaskRepository) EditTask(ctx context.Context, t *task.Task) (*task.Task
WHERE id = $7 RETURNING updated_at`
var updatedTask task.Task
err := r.db.QueryRowxContext(ctx, query, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type, t.Id).StructScan(&updatedTask)
err := r.db.QueryRowxContext(ctx, query, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type, t.Id).
StructScan(&updatedTask)
if err != nil {
return nil, err
}
@@ -65,7 +67,11 @@ func (r *TaskRepository) ListCompetitionTasks(ctx context.Context, competitionID
return tasks, err
}
func (r *TaskRepository) GetTaskAttachments(ctx context.Context, taskID string, showPrivate bool) ([]*task.TaskAttachment, error) {
func (r *TaskRepository) GetTaskAttachments(
ctx context.Context,
taskID string,
showPrivate bool,
) ([]*task.TaskAttachment, error) {
var attachments []*task.TaskAttachment
query := "SELECT * FROM task_attachments WHERE task_id = $1"
args := []interface{}{taskID}
@@ -76,4 +82,4 @@ func (r *TaskRepository) GetTaskAttachments(ctx context.Context, taskID string,
err := r.db.SelectContext(ctx, &attachments, query, args...)
return attachments, err
}
}
+10 -3
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
pb "datarush/pkg/api/task"
"google.golang.org/protobuf/types/known/emptypb"
@@ -41,7 +42,10 @@ func (s *TaskService) DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest)
return &emptypb.Empty{}, err
}
func (s *TaskService) ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) {
func (s *TaskService) ListCompetitionTasks(
ctx context.Context,
req *pb.ListCompetitionTasksRequest,
) (*pb.ListCompetitionTasksResponse, error) {
tasks, err := s.repo.ListCompetitionTasks(ctx, req.CompetitionId)
if err != nil {
return nil, err
@@ -49,7 +53,10 @@ func (s *TaskService) ListCompetitionTasks(ctx context.Context, req *pb.ListComp
return &pb.ListCompetitionTasksResponse{Tasks: tasks}, nil
}
func (s *TaskService) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) {
func (s *TaskService) GetTaskAttachments(
ctx context.Context,
req *pb.GetTaskAttachmentsRequest,
) (*pb.GetTaskAttachmentsResponse, error) {
showPrivate := false
if req.ShowPrivate != nil {
showPrivate = *req.ShowPrivate
@@ -59,4 +66,4 @@ func (s *TaskService) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAtt
return nil, err
}
return &pb.GetTaskAttachmentsResponse{Attachments: attachments}, nil
}
}
+24 -24
View File
@@ -7,8 +7,8 @@ import (
"datarush/internal/task/service/mocks"
pb "datarush/pkg/api/task"
"go.uber.org/mock/gomock"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -24,13 +24,13 @@ func TestTaskService(t *testing.T) {
t.Run("CreateTask", func(t *testing.T) {
task := &pb.Task{
CompetitionId: "comp1",
Title: "Test Task",
Description: "This is a test task",
CompetitionId: "comp1",
Title: "Test Task",
Description: "This is a test task",
InCompetitionPosition: 1,
MaxPoints: 100,
MaxAttempts: 10,
Type: pb.TaskType_TASK_TYPE_INPUT,
MaxPoints: 100,
MaxAttempts: 10,
Type: pb.TaskType_TASK_TYPE_INPUT,
}
mockRepo.EXPECT().CreateTask(ctx, task).Return(task, nil)
@@ -44,16 +44,16 @@ func TestTaskService(t *testing.T) {
taskID := "task1"
req := &pb.GetTaskRequest{TaskId: taskID}
expectedTask := &pb.Task{
Id: taskID,
CompetitionId: "comp1",
Title: "Test Task",
Description: "This is a test task",
Id: taskID,
CompetitionId: "comp1",
Title: "Test Task",
Description: "This is a test task",
InCompetitionPosition: 1,
MaxPoints: 100,
MaxAttempts: 10,
Type: pb.TaskType_TASK_TYPE_INPUT,
CreatedAt: timestamppb.Now(),
UpdatedAt: timestamppb.Now(),
MaxPoints: 100,
MaxAttempts: 10,
Type: pb.TaskType_TASK_TYPE_INPUT,
CreatedAt: timestamppb.Now(),
UpdatedAt: timestamppb.Now(),
}
mockRepo.EXPECT().GetTask(ctx, taskID).Return(expectedTask, nil)
@@ -65,14 +65,14 @@ func TestTaskService(t *testing.T) {
t.Run("EditTask", func(t *testing.T) {
task := &pb.Task{
Id: "task1",
CompetitionId: "comp1",
Title: "Updated Test Task",
Description: "This is an updated test task",
Id: "task1",
CompetitionId: "comp1",
Title: "Updated Test Task",
Description: "This is an updated test task",
InCompetitionPosition: 1,
MaxPoints: 150,
MaxAttempts: 5,
Type: pb.TaskType_TASK_TYPE_CHECKER,
MaxPoints: 150,
MaxAttempts: 5,
Type: pb.TaskType_TASK_TYPE_CHECKER,
}
mockRepo.EXPECT().EditTask(ctx, task).Return(task, nil)
@@ -137,4 +137,4 @@ func TestTaskService(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, expectedAttachments, resp.Attachments)
})
}
}
+19 -6
View File
@@ -3,15 +3,19 @@ package grpc
import (
"context"
pb "datarush/pkg/api/user"
"google.golang.org/protobuf/types/known/emptypb"
pb "datarush/pkg/api/user"
)
type UserService interface {
GetProfile(ctx context.Context, req *pb.GetProfileRequest) (*pb.User, error)
RegisterForCompetition(ctx context.Context, req *pb.RegisterForCompetitionRequest) (*emptypb.Empty, error)
UnregisterFromCompetition(ctx context.Context, req *pb.UnregisterFromCompetitionRequest) (*emptypb.Empty, error)
ListUserCompetitions(ctx context.Context, req *pb.ListUserCompetitionsRequest) (*pb.ListUserCompetitionsResponse, error)
ListUserCompetitions(
ctx context.Context,
req *pb.ListUserCompetitionsRequest,
) (*pb.ListUserCompetitionsResponse, error)
}
type UserHandler struct {
@@ -27,14 +31,23 @@ func (h *UserHandler) GetProfile(ctx context.Context, req *pb.GetProfileRequest)
return h.service.GetProfile(ctx, req)
}
func (h *UserHandler) RegisterForCompetition(ctx context.Context, req *pb.RegisterForCompetitionRequest) (*emptypb.Empty, error) {
func (h *UserHandler) RegisterForCompetition(
ctx context.Context,
req *pb.RegisterForCompetitionRequest,
) (*emptypb.Empty, error) {
return h.service.RegisterForCompetition(ctx, req)
}
func (h *UserHandler) UnregisterFromCompetition(ctx context.Context, req *pb.UnregisterFromCompetitionRequest) (*emptypb.Empty, error) {
func (h *UserHandler) UnregisterFromCompetition(
ctx context.Context,
req *pb.UnregisterFromCompetitionRequest,
) (*emptypb.Empty, error) {
return h.service.UnregisterFromCompetition(ctx, req)
}
func (h *UserHandler) ListUserCompetitions(ctx context.Context, req *pb.ListUserCompetitionsRequest) (*pb.ListUserCompetitionsResponse, error) {
func (h *UserHandler) ListUserCompetitions(
ctx context.Context,
req *pb.ListUserCompetitionsRequest,
) (*pb.ListUserCompetitionsResponse, error) {
return h.service.ListUserCompetitions(ctx, req)
}
}
+1 -1
View File
@@ -49,4 +49,4 @@ func AuthInterceptor(jwtSecret string) grpc.UnaryServerInterceptor {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
}
}
}
@@ -3,8 +3,9 @@ package postgres
import (
"context"
"datarush/pkg/api/user"
"github.com/jmoiron/sqlx"
"datarush/pkg/api/user"
)
type UserRepository struct {
@@ -17,22 +18,42 @@ func NewUserRepository(db *sqlx.DB) *UserRepository {
func (r *UserRepository) GetProfile(ctx context.Context, userID string) (*user.User, error) {
var u user.User
err := r.db.GetContext(ctx, &u, "SELECT id, username, email, full_name, avatar_url FROM users WHERE id = $1", userID)
err := r.db.GetContext(
ctx,
&u,
"SELECT id, username, email, full_name, avatar_url FROM users WHERE id = $1",
userID,
)
return &u, err
}
func (r *UserRepository) RegisterForCompetition(ctx context.Context, userID, competitionID string) error {
_, err := r.db.ExecContext(ctx, "INSERT INTO user_competitions (user_id, competition_id) VALUES ($1, $2)", userID, competitionID)
_, err := r.db.ExecContext(
ctx,
"INSERT INTO user_competitions (user_id, competition_id) VALUES ($1, $2)",
userID,
competitionID,
)
return err
}
func (r *UserRepository) UnregisterFromCompetition(ctx context.Context, userID, competitionID string) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM user_competitions WHERE user_id = $1 AND competition_id = $2", userID, competitionID)
_, err := r.db.ExecContext(
ctx,
"DELETE FROM user_competitions WHERE user_id = $1 AND competition_id = $2",
userID,
competitionID,
)
return err
}
func (r *UserRepository) ListUserCompetitions(ctx context.Context, userID string) ([]string, error) {
var competitionIDs []string
err := r.db.SelectContext(ctx, &competitionIDs, "SELECT competition_id FROM user_competitions WHERE user_id = $1", userID)
err := r.db.SelectContext(
ctx,
&competitionIDs,
"SELECT competition_id FROM user_competitions WHERE user_id = $1",
userID,
)
return competitionIDs, err
}
}
+15 -5
View File
@@ -4,9 +4,10 @@ import (
"context"
"errors"
"google.golang.org/protobuf/types/known/emptypb"
"datarush/internal/user/middleware"
pb "datarush/pkg/api/user"
"google.golang.org/protobuf/types/known/emptypb"
)
type UserRepository interface {
@@ -28,7 +29,10 @@ func (s *UserService) GetProfile(ctx context.Context, req *pb.GetProfileRequest)
return s.repo.GetProfile(ctx, req.UserId)
}
func (s *UserService) RegisterForCompetition(ctx context.Context, req *pb.RegisterForCompetitionRequest) (*emptypb.Empty, error) {
func (s *UserService) RegisterForCompetition(
ctx context.Context,
req *pb.RegisterForCompetitionRequest,
) (*emptypb.Empty, error) {
userID, ok := ctx.Value(middleware.UserIDKey{}).(string)
if !ok {
return nil, errors.New("user ID not found in context")
@@ -38,7 +42,10 @@ func (s *UserService) RegisterForCompetition(ctx context.Context, req *pb.Regist
return &emptypb.Empty{}, err
}
func (s *UserService) UnregisterFromCompetition(ctx context.Context, req *pb.UnregisterFromCompetitionRequest) (*emptypb.Empty, error) {
func (s *UserService) UnregisterFromCompetition(
ctx context.Context,
req *pb.UnregisterFromCompetitionRequest,
) (*emptypb.Empty, error) {
userID, ok := ctx.Value(middleware.UserIDKey{}).(string)
if !ok {
return nil, errors.New("user ID not found in context")
@@ -48,10 +55,13 @@ func (s *UserService) UnregisterFromCompetition(ctx context.Context, req *pb.Unr
return &emptypb.Empty{}, err
}
func (s *UserService) ListUserCompetitions(ctx context.Context, req *pb.ListUserCompetitionsRequest) (*pb.ListUserCompetitionsResponse, error) {
func (s *UserService) ListUserCompetitions(
ctx context.Context,
req *pb.ListUserCompetitionsRequest,
) (*pb.ListUserCompetitionsResponse, error) {
competitionIDs, err := s.repo.ListUserCompetitions(ctx, req.UserId)
if err != nil {
return nil, err
}
return &pb.ListUserCompetitionsResponse{CompetitionIds: competitionIDs}, nil
}
}
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"datarush/internal/user/service/mocks"
pb "datarush/pkg/api/user"
"go.uber.org/mock/gomock"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
func TestUserService(t *testing.T) {
@@ -98,4 +98,4 @@ func TestUserService(t *testing.T) {
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
}
}