add results service
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"datarush/internal/results/domain"
|
||||
pb "datarush/pkg/api/results"
|
||||
submissionPb "datarush/pkg/api/submission"
|
||||
taskPb "datarush/pkg/api/task"
|
||||
userPb "datarush/pkg/api/user"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type IResultService interface {
|
||||
CreateResult(ctx context.Context, result *domain.Result) (*domain.Result, error)
|
||||
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)
|
||||
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)
|
||||
UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error)
|
||||
RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error
|
||||
}
|
||||
|
||||
type ResultsHandler struct {
|
||||
pb.UnimplementedResultsServiceServer
|
||||
service IResultService
|
||||
userServiceAddr string
|
||||
submissionServiceAddr string
|
||||
taskServiceAddr string
|
||||
userClient userPb.UserServiceClient
|
||||
submissionClient submissionPb.SubmissionServiceClient
|
||||
taskClient taskPb.TaskServiceClient
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewResultsHandler(s IResultService, userServiceAddr, submissionServiceAddr, taskServiceAddr string) *ResultsHandler {
|
||||
return &ResultsHandler{
|
||||
service: s,
|
||||
userServiceAddr: userServiceAddr,
|
||||
submissionServiceAddr: submissionServiceAddr,
|
||||
taskServiceAddr: taskServiceAddr,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ResultsHandler) getUserClient(ctx context.Context) (userPb.UserServiceClient, error) {
|
||||
h.mu.RLock()
|
||||
if h.userClient != nil {
|
||||
client := h.userClient
|
||||
h.mu.RUnlock()
|
||||
return client, nil
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.userClient != nil {
|
||||
return h.userClient, nil
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(ctx, h.userServiceAddr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithBlock())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.userClient = userPb.NewUserServiceClient(conn)
|
||||
return h.userClient, nil
|
||||
}
|
||||
|
||||
func (h *ResultsHandler) getSubmissionClient(ctx context.Context) (submissionPb.SubmissionServiceClient, error) {
|
||||
h.mu.RLock()
|
||||
if h.submissionClient != nil {
|
||||
client := h.submissionClient
|
||||
h.mu.RUnlock()
|
||||
return client, nil
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.submissionClient != nil {
|
||||
return h.submissionClient, nil
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(ctx, h.submissionServiceAddr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithBlock())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.submissionClient = submissionPb.NewSubmissionServiceClient(conn)
|
||||
return h.submissionClient, nil
|
||||
}
|
||||
|
||||
func (h *ResultsHandler) getTaskClient(ctx context.Context) (taskPb.TaskServiceClient, error) {
|
||||
h.mu.RLock()
|
||||
if h.taskClient != nil {
|
||||
client := h.taskClient
|
||||
h.mu.RUnlock()
|
||||
return client, nil
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.taskClient != nil {
|
||||
return h.taskClient, nil
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(ctx, h.taskServiceAddr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithBlock())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.taskClient = taskPb.NewTaskServiceClient(conn)
|
||||
return h.taskClient, nil
|
||||
}
|
||||
|
||||
func (h *ResultsHandler) fetchUsername(ctx context.Context, userID string) string {
|
||||
client, err := h.getUserClient(ctx)
|
||||
if err != nil {
|
||||
log.Printf("failed to get user client: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
user, err := client.GetProfile(ctx, &userPb.GetProfileRequest{UserId: userID})
|
||||
if err != nil {
|
||||
log.Printf("failed to get user profile for user %s: %v", userID, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return user.GetUsername()
|
||||
}
|
||||
|
||||
func (h *ResultsHandler) fetchTaskStatuses(ctx context.Context, competitionID, userID string) []*pb.TaskStatus {
|
||||
client, err := h.getSubmissionClient(ctx)
|
||||
if err != nil {
|
||||
log.Printf("failed to get submission client: %v", err)
|
||||
return []*pb.TaskStatus{}
|
||||
}
|
||||
|
||||
resp, err := client.ListSubmissions(ctx, &submissionPb.ListSubmissionsRequest{
|
||||
CompetitionId: competitionID,
|
||||
UserId: userID,
|
||||
PageSize: 1000,
|
||||
PageToken: 0,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("failed to list submissions for user %s in competition %s: %v", userID, competitionID, err)
|
||||
return []*pb.TaskStatus{}
|
||||
}
|
||||
|
||||
taskMap := make(map[string]*pb.TaskStatus)
|
||||
for _, submission := range resp.GetSubmissions() {
|
||||
taskID := submission.GetTaskId()
|
||||
|
||||
existing, exists := taskMap[taskID]
|
||||
if !exists {
|
||||
taskMap[taskID] = &pb.TaskStatus{
|
||||
TaskId: taskID,
|
||||
TaskTitle: "",
|
||||
EarnedPoints: submission.GetEarnedPoints(),
|
||||
MaxPoints: 0,
|
||||
Position: nil,
|
||||
}
|
||||
} else {
|
||||
if submission.GetEarnedPoints() > existing.EarnedPoints {
|
||||
existing.EarnedPoints = submission.GetEarnedPoints()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
taskClient, err := h.getTaskClient(ctx)
|
||||
if err != nil {
|
||||
log.Printf("failed to get task client: %v", err)
|
||||
} else {
|
||||
for taskID, status := range taskMap {
|
||||
task, err := taskClient.GetTask(ctx, &taskPb.GetTaskRequest{TaskId: taskID})
|
||||
if err != nil {
|
||||
log.Printf("failed to get task %s: %v", taskID, err)
|
||||
continue
|
||||
}
|
||||
status.TaskTitle = task.GetTitle()
|
||||
status.MaxPoints = task.GetMaxPoints()
|
||||
}
|
||||
}
|
||||
|
||||
taskStatuses := make([]*pb.TaskStatus, 0, len(taskMap))
|
||||
for _, status := range taskMap {
|
||||
taskStatuses = append(taskStatuses, status)
|
||||
}
|
||||
|
||||
return taskStatuses
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
pbResult := &pb.UserResult{
|
||||
UserId: userID,
|
||||
Username: username,
|
||||
TotalScore: int32(result.Score),
|
||||
OverallPosition: 0,
|
||||
TaskStatuses: taskStatuses,
|
||||
}
|
||||
|
||||
if result.Rank != nil {
|
||||
pbResult.OverallPosition = int32(*result.Rank)
|
||||
}
|
||||
|
||||
return pbResult
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
results, total, err := h.service.GetLeaderboard(ctx, competitionID, req.GetPageSize(), req.GetPageToken())
|
||||
if err != nil {
|
||||
log.Printf("failed to get competition results: %v", err)
|
||||
return nil, status.Errorf(codes.Internal, "failed to get competition results: %v", err)
|
||||
}
|
||||
|
||||
pbResults := make([]*pb.UserResult, len(results))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range results {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
pbResults[idx] = h.enrichUserResult(ctx, &results[idx], req.GetCompetitionId())
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
var nextPageToken int32
|
||||
if int(req.GetPageToken()+req.GetPageSize()) < int(total) {
|
||||
nextPageToken = req.GetPageToken() + 1
|
||||
}
|
||||
|
||||
return &pb.GetCompetitionResultsResponse{
|
||||
Results: pbResults,
|
||||
TotalCount: total,
|
||||
NextPageToken: nextPageToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(req.GetUserId())
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user id format: %v", err)
|
||||
}
|
||||
|
||||
result, err := h.service.GetByCompetitionAndUser(ctx, competitionID, userID)
|
||||
if err != nil {
|
||||
if err == domain.ErrResultNotFound {
|
||||
return nil, status.Errorf(codes.NotFound, "result not found for user in competition")
|
||||
}
|
||||
log.Printf("failed to get user competition results: %v", err)
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user competition results: %v", err)
|
||||
}
|
||||
|
||||
pbResult := h.enrichUserResult(ctx, result, req.GetCompetitionId())
|
||||
|
||||
return &pb.GetUserCompetitionResultsResponse{
|
||||
Result: pbResult,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if err := h.service.RecalculateRanks(ctx, competitionID); err != nil {
|
||||
log.Printf("failed to recalculate results: %v", err)
|
||||
return nil, status.Errorf(codes.Internal, "failed to recalculate results: %v", err)
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"datarush/internal/results/domain"
|
||||
pb "datarush/pkg/api/results"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type MockResultService struct {
|
||||
CreateResultFunc func(ctx context.Context, result *domain.Result) (*domain.Result, error)
|
||||
GetResultFunc func(ctx context.Context, id uuid.UUID) (*domain.Result, error)
|
||||
UpdateResultFunc func(ctx context.Context, result *domain.Result) (*domain.Result, error)
|
||||
DeleteResultFunc func(ctx context.Context, id uuid.UUID) error
|
||||
ListResultsFunc func(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)
|
||||
GetByCompetitionAndUserFunc func(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error)
|
||||
GetLeaderboardFunc func(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error)
|
||||
UpdateStatusFunc func(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error)
|
||||
RecalculateRanksFunc func(ctx context.Context, competitionID uuid.UUID) error
|
||||
}
|
||||
|
||||
func (m *MockResultService) CreateResult(ctx context.Context, result *domain.Result) (*domain.Result, error) {
|
||||
if m.CreateResultFunc != nil {
|
||||
return m.CreateResultFunc(ctx, result)
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (m *MockResultService) GetResult(ctx context.Context, id uuid.UUID) (*domain.Result, error) {
|
||||
if m.GetResultFunc != nil {
|
||||
return m.GetResultFunc(ctx, id)
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (m *MockResultService) UpdateResult(ctx context.Context, result *domain.Result) (*domain.Result, error) {
|
||||
if m.UpdateResultFunc != nil {
|
||||
return m.UpdateResultFunc(ctx, result)
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (m *MockResultService) DeleteResult(ctx context.Context, id uuid.UUID) error {
|
||||
if m.DeleteResultFunc != nil {
|
||||
return m.DeleteResultFunc(ctx, id)
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
if m.UpdateStatusFunc != nil {
|
||||
return m.UpdateStatusFunc(ctx, id, status)
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (m *MockResultService) RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error {
|
||||
if m.RecalculateRanksFunc != nil {
|
||||
return m.RecalculateRanksFunc(ctx, competitionID)
|
||||
}
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func TestNewResultsHandler(t *testing.T) {
|
||||
mockService := &MockResultService{}
|
||||
handler := NewResultsHandler(mockService, "localhost:50051", "localhost:50055", "localhost:50056")
|
||||
|
||||
if handler == nil {
|
||||
t.Fatal("Expected handler to be created, got nil")
|
||||
}
|
||||
|
||||
if handler.service == nil {
|
||||
t.Error("Expected service to be set")
|
||||
}
|
||||
|
||||
if handler.userServiceAddr != "localhost:50051" {
|
||||
t.Errorf("Expected userServiceAddr to be localhost:50051, got %s", handler.userServiceAddr)
|
||||
}
|
||||
|
||||
if handler.submissionServiceAddr != "localhost:50055" {
|
||||
t.Errorf("Expected submissionServiceAddr to be localhost:50055, got %s", handler.submissionServiceAddr)
|
||||
}
|
||||
|
||||
if handler.taskServiceAddr != "localhost:50056" {
|
||||
t.Errorf("Expected taskServiceAddr to be localhost:50056, got %s", handler.taskServiceAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCompetitionResults_InvalidCompetitionID(t *testing.T) {
|
||||
mockService := &MockResultService{}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
req := &pb.GetCompetitionResultsRequest{
|
||||
CompetitionId: "invalid-uuid",
|
||||
PageSize: 10,
|
||||
PageToken: 0,
|
||||
}
|
||||
|
||||
resp, err := handler.GetCompetitionResults(context.Background(), req)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid competition ID, got nil")
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
t.Error("Expected nil response for invalid competition ID")
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatal("Expected gRPC status error")
|
||||
}
|
||||
|
||||
if st.Code() != codes.InvalidArgument {
|
||||
t.Errorf("Expected code InvalidArgument, got %v", st.Code())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCompetitionResults_ServiceError(t *testing.T) {
|
||||
mockService := &MockResultService{
|
||||
GetLeaderboardFunc: func(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error) {
|
||||
return nil, 0, errors.New("database error")
|
||||
},
|
||||
}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
competitionID := uuid.New()
|
||||
req := &pb.GetCompetitionResultsRequest{
|
||||
CompetitionId: competitionID.String(),
|
||||
PageSize: 10,
|
||||
PageToken: 0,
|
||||
}
|
||||
|
||||
resp, err := handler.GetCompetitionResults(context.Background(), req)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error from service, got nil")
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
t.Error("Expected nil response on service error")
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatal("Expected gRPC status error")
|
||||
}
|
||||
|
||||
if st.Code() != codes.Internal {
|
||||
t.Errorf("Expected code Internal, got %v", st.Code())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCompetitionResults_EmptyLeaderboard(t *testing.T) {
|
||||
mockService := &MockResultService{
|
||||
GetLeaderboardFunc: func(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error) {
|
||||
return []domain.Result{}, 0, nil
|
||||
},
|
||||
}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
competitionID := uuid.New()
|
||||
req := &pb.GetCompetitionResultsRequest{
|
||||
CompetitionId: competitionID.String(),
|
||||
PageSize: 10,
|
||||
PageToken: 0,
|
||||
}
|
||||
|
||||
resp, err := handler.GetCompetitionResults(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("Expected response, got nil")
|
||||
}
|
||||
|
||||
if len(resp.Results) != 0 {
|
||||
t.Errorf("Expected 0 results, got %d", len(resp.Results))
|
||||
}
|
||||
|
||||
if resp.TotalCount != 0 {
|
||||
t.Errorf("Expected total count 0, got %d", resp.TotalCount)
|
||||
}
|
||||
|
||||
if resp.NextPageToken != 0 {
|
||||
t.Errorf("Expected next page token 0, got %d", resp.NextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserCompetitionResults_InvalidCompetitionID(t *testing.T) {
|
||||
mockService := &MockResultService{}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
req := &pb.GetUserCompetitionResultsRequest{
|
||||
CompetitionId: "invalid-uuid",
|
||||
UserId: uuid.New().String(),
|
||||
}
|
||||
|
||||
resp, err := handler.GetUserCompetitionResults(context.Background(), req)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid competition ID, got nil")
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
t.Error("Expected nil response for invalid competition ID")
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatal("Expected gRPC status error")
|
||||
}
|
||||
|
||||
if st.Code() != codes.InvalidArgument {
|
||||
t.Errorf("Expected code InvalidArgument, got %v", st.Code())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserCompetitionResults_InvalidUserID(t *testing.T) {
|
||||
mockService := &MockResultService{}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
req := &pb.GetUserCompetitionResultsRequest{
|
||||
CompetitionId: uuid.New().String(),
|
||||
UserId: "invalid-uuid",
|
||||
}
|
||||
|
||||
resp, err := handler.GetUserCompetitionResults(context.Background(), req)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid user ID, got nil")
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
t.Error("Expected nil response for invalid user ID")
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatal("Expected gRPC status error")
|
||||
}
|
||||
|
||||
if st.Code() != codes.InvalidArgument {
|
||||
t.Errorf("Expected code InvalidArgument, got %v", st.Code())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserCompetitionResults_NotFound(t *testing.T) {
|
||||
mockService := &MockResultService{
|
||||
GetByCompetitionAndUserFunc: func(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) {
|
||||
return nil, domain.ErrResultNotFound
|
||||
},
|
||||
}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
req := &pb.GetUserCompetitionResultsRequest{
|
||||
CompetitionId: uuid.New().String(),
|
||||
UserId: uuid.New().String(),
|
||||
}
|
||||
|
||||
resp, err := handler.GetUserCompetitionResults(context.Background(), req)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for not found result, got nil")
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
t.Error("Expected nil response for not found result")
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatal("Expected gRPC status error")
|
||||
}
|
||||
|
||||
if st.Code() != codes.NotFound {
|
||||
t.Errorf("Expected code NotFound, got %v", st.Code())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserCompetitionResults_Success(t *testing.T) {
|
||||
rank := 1
|
||||
competitionID := uuid.New()
|
||||
userID := uuid.New()
|
||||
|
||||
mockService := &MockResultService{
|
||||
GetByCompetitionAndUserFunc: func(ctx context.Context, compID uuid.UUID, uID uuid.UUID) (*domain.Result, error) {
|
||||
return &domain.Result{
|
||||
ID: uuid.New(),
|
||||
CompetitionID: compID,
|
||||
UserID: uID,
|
||||
Score: 95.5,
|
||||
Rank: &rank,
|
||||
Status: domain.ResultStatusCompleted,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
req := &pb.GetUserCompetitionResultsRequest{
|
||||
CompetitionId: competitionID.String(),
|
||||
UserId: userID.String(),
|
||||
}
|
||||
|
||||
resp, err := handler.GetUserCompetitionResults(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("Expected response, got nil")
|
||||
}
|
||||
|
||||
if resp.Result == nil {
|
||||
t.Fatal("Expected result, got nil")
|
||||
}
|
||||
|
||||
if resp.Result.UserId != userID.String() {
|
||||
t.Errorf("Expected user ID %s, got %s", userID.String(), resp.Result.UserId)
|
||||
}
|
||||
|
||||
if resp.Result.TotalScore != 95 {
|
||||
t.Errorf("Expected total score 95, got %d", resp.Result.TotalScore)
|
||||
}
|
||||
|
||||
if resp.Result.OverallPosition != 1 {
|
||||
t.Errorf("Expected overall position 1, got %d", resp.Result.OverallPosition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecalculateResults_InvalidCompetitionID(t *testing.T) {
|
||||
mockService := &MockResultService{}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
req := &pb.RecalculateResultsRequest{
|
||||
CompetitionId: "invalid-uuid",
|
||||
}
|
||||
|
||||
resp, err := handler.RecalculateResults(context.Background(), req)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid competition ID, got nil")
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
t.Error("Expected nil response for invalid competition ID")
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatal("Expected gRPC status error")
|
||||
}
|
||||
|
||||
if st.Code() != codes.InvalidArgument {
|
||||
t.Errorf("Expected code InvalidArgument, got %v", st.Code())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecalculateResults_ServiceError(t *testing.T) {
|
||||
mockService := &MockResultService{
|
||||
RecalculateRanksFunc: func(ctx context.Context, competitionID uuid.UUID) error {
|
||||
return errors.New("recalculation failed")
|
||||
},
|
||||
}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
req := &pb.RecalculateResultsRequest{
|
||||
CompetitionId: uuid.New().String(),
|
||||
}
|
||||
|
||||
resp, err := handler.RecalculateResults(context.Background(), req)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error from service, got nil")
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
t.Error("Expected nil response on service error")
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatal("Expected gRPC status error")
|
||||
}
|
||||
|
||||
if st.Code() != codes.Internal {
|
||||
t.Errorf("Expected code Internal, got %v", st.Code())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecalculateResults_Success(t *testing.T) {
|
||||
mockService := &MockResultService{
|
||||
RecalculateRanksFunc: func(ctx context.Context, competitionID uuid.UUID) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
req := &pb.RecalculateResultsRequest{
|
||||
CompetitionId: uuid.New().String(),
|
||||
}
|
||||
|
||||
resp, err := handler.RecalculateResults(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("Expected response, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchUsername(t *testing.T) {
|
||||
handler := &ResultsHandler{
|
||||
userServiceAddr: "invalid-address",
|
||||
}
|
||||
|
||||
// Test with invalid address - should return empty string
|
||||
username := handler.fetchUsername(context.Background(), uuid.New().String())
|
||||
|
||||
if username != "" {
|
||||
t.Errorf("Expected empty username on error, got %s", username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchTaskStatuses(t *testing.T) {
|
||||
handler := &ResultsHandler{
|
||||
submissionServiceAddr: "invalid-address",
|
||||
}
|
||||
|
||||
// Test with invalid address - should return empty slice
|
||||
statuses := handler.fetchTaskStatuses(context.Background(), uuid.New().String(), uuid.New().String())
|
||||
|
||||
if statuses == nil {
|
||||
t.Fatal("Expected empty slice, got nil")
|
||||
}
|
||||
|
||||
if len(statuses) != 0 {
|
||||
t.Errorf("Expected 0 statuses on error, got %d", len(statuses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichUserResult(t *testing.T) {
|
||||
rank := 1
|
||||
result := &domain.Result{
|
||||
ID: uuid.New(),
|
||||
CompetitionID: uuid.New(),
|
||||
UserID: uuid.New(),
|
||||
Score: 95.5,
|
||||
Rank: &rank,
|
||||
Status: domain.ResultStatusCompleted,
|
||||
}
|
||||
|
||||
handler := &ResultsHandler{
|
||||
userServiceAddr: "invalid-address",
|
||||
submissionServiceAddr: "invalid-address",
|
||||
taskServiceAddr: "invalid-address",
|
||||
}
|
||||
|
||||
competitionID := uuid.New().String()
|
||||
pbResult := handler.enrichUserResult(context.Background(), result, competitionID)
|
||||
|
||||
if pbResult == nil {
|
||||
t.Fatal("Expected result, got nil")
|
||||
}
|
||||
|
||||
if pbResult.UserId != result.UserID.String() {
|
||||
t.Errorf("Expected user ID %s, got %s", result.UserID.String(), pbResult.UserId)
|
||||
}
|
||||
|
||||
if pbResult.TotalScore != int32(result.Score) {
|
||||
t.Errorf("Expected total score %d, got %d", int32(result.Score), pbResult.TotalScore)
|
||||
}
|
||||
|
||||
if pbResult.OverallPosition != int32(*result.Rank) {
|
||||
t.Errorf("Expected position %d, got %d", *result.Rank, pbResult.OverallPosition)
|
||||
}
|
||||
|
||||
if pbResult.Username != "" {
|
||||
t.Errorf("Expected empty username, got %s", pbResult.Username)
|
||||
}
|
||||
|
||||
if len(pbResult.TaskStatuses) != 0 {
|
||||
t.Errorf("Expected 0 task statuses, got %d", len(pbResult.TaskStatuses))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichUserResult_WithoutRank(t *testing.T) {
|
||||
result := &domain.Result{
|
||||
ID: uuid.New(),
|
||||
CompetitionID: uuid.New(),
|
||||
UserID: uuid.New(),
|
||||
Score: 95.5,
|
||||
Rank: nil,
|
||||
Status: domain.ResultStatusCompleted,
|
||||
}
|
||||
|
||||
handler := &ResultsHandler{
|
||||
userServiceAddr: "invalid-address",
|
||||
submissionServiceAddr: "invalid-address",
|
||||
taskServiceAddr: "invalid-address",
|
||||
}
|
||||
|
||||
competitionID := uuid.New().String()
|
||||
pbResult := handler.enrichUserResult(context.Background(), result, competitionID)
|
||||
|
||||
if pbResult == nil {
|
||||
t.Fatal("Expected result, got nil")
|
||||
}
|
||||
|
||||
if pbResult.OverallPosition != 0 {
|
||||
t.Errorf("Expected position 0 for nil rank, got %d", pbResult.OverallPosition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCompetitionResults_Pagination(t *testing.T) {
|
||||
rank1, rank2, rank3 := 1, 2, 3
|
||||
results := []domain.Result{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
CompetitionID: uuid.New(),
|
||||
UserID: uuid.New(),
|
||||
Score: 100.0,
|
||||
Rank: &rank1,
|
||||
Status: domain.ResultStatusCompleted,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
CompetitionID: uuid.New(),
|
||||
UserID: uuid.New(),
|
||||
Score: 95.0,
|
||||
Rank: &rank2,
|
||||
Status: domain.ResultStatusCompleted,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
CompetitionID: uuid.New(),
|
||||
UserID: uuid.New(),
|
||||
Score: 90.0,
|
||||
Rank: &rank3,
|
||||
Status: domain.ResultStatusCompleted,
|
||||
},
|
||||
}
|
||||
|
||||
mockService := &MockResultService{
|
||||
GetLeaderboardFunc: func(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error) {
|
||||
return results, 25, nil
|
||||
},
|
||||
}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
competitionID := uuid.New()
|
||||
req := &pb.GetCompetitionResultsRequest{
|
||||
CompetitionId: competitionID.String(),
|
||||
PageSize: 10,
|
||||
PageToken: 0,
|
||||
}
|
||||
|
||||
resp, err := handler.GetCompetitionResults(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("Expected response, got nil")
|
||||
}
|
||||
|
||||
if len(resp.Results) != 3 {
|
||||
t.Errorf("Expected 3 results, got %d", len(resp.Results))
|
||||
}
|
||||
|
||||
if resp.TotalCount != 25 {
|
||||
t.Errorf("Expected total count 25, got %d", resp.TotalCount)
|
||||
}
|
||||
|
||||
if resp.NextPageToken != 1 {
|
||||
t.Errorf("Expected next page token 1, got %d", resp.NextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCompetitionResults_LastPage(t *testing.T) {
|
||||
rank1 := 1
|
||||
results := []domain.Result{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
CompetitionID: uuid.New(),
|
||||
UserID: uuid.New(),
|
||||
Score: 100.0,
|
||||
Rank: &rank1,
|
||||
Status: domain.ResultStatusCompleted,
|
||||
},
|
||||
}
|
||||
|
||||
mockService := &MockResultService{
|
||||
GetLeaderboardFunc: func(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error) {
|
||||
return results, 1, nil
|
||||
},
|
||||
}
|
||||
handler := NewResultsHandler(mockService, "", "", "")
|
||||
|
||||
competitionID := uuid.New()
|
||||
req := &pb.GetCompetitionResultsRequest{
|
||||
CompetitionId: competitionID.String(),
|
||||
PageSize: 10,
|
||||
PageToken: 0,
|
||||
}
|
||||
|
||||
resp, err := handler.GetCompetitionResults(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("Expected response, got nil")
|
||||
}
|
||||
|
||||
if resp.NextPageToken != 0 {
|
||||
t.Errorf("Expected next page token 0 on last page, got %d", resp.NextPageToken)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user