Files
2025-12-19 21:04:12 +03:00

829 lines
19 KiB
Go

package service
import (
"context"
"errors"
"testing"
"time"
"datarush/internal/results/domain"
"datarush/internal/results/repository"
"github.com/google/uuid"
)
type MockRepository struct {
CreateFunc func(ctx context.Context, result *domain.Result) error
GetFunc func(ctx context.Context, id uuid.UUID) (*domain.Result, error)
UpdateFunc func(ctx context.Context, result *domain.Result) error
DeleteFunc func(ctx context.Context, id uuid.UUID) error
ListFunc func(ctx context.Context, opts repository.ListResultsOptions) ([]domain.Result, int, error)
GetByCompetitionAndUserFunc func(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error)
GetLeaderboardFunc func(ctx context.Context, competitionID uuid.UUID, limit int, offset int) ([]domain.Result, int, 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 *MockRepository) Create(ctx context.Context, result *domain.Result) error {
if m.CreateFunc != nil {
return m.CreateFunc(ctx, result)
}
return nil
}
func (m *MockRepository) Get(ctx context.Context, id uuid.UUID) (*domain.Result, error) {
if m.GetFunc != nil {
return m.GetFunc(ctx, id)
}
return nil, errors.New("not implemented")
}
func (m *MockRepository) Update(ctx context.Context, result *domain.Result) error {
if m.UpdateFunc != nil {
return m.UpdateFunc(ctx, result)
}
return nil
}
func (m *MockRepository) Delete(ctx context.Context, id uuid.UUID) error {
if m.DeleteFunc != nil {
return m.DeleteFunc(ctx, id)
}
return nil
}
func (m *MockRepository) List(ctx context.Context, opts repository.ListResultsOptions) ([]domain.Result, int, error) {
if m.ListFunc != nil {
return m.ListFunc(ctx, opts)
}
return nil, 0, errors.New("not implemented")
}
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) {
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) {
if m.UpdateStatusFunc != nil {
return m.UpdateStatusFunc(ctx, id, status)
}
return nil, errors.New("not implemented")
}
func (m *MockRepository) RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error {
if m.RecalculateRanksFunc != nil {
return m.RecalculateRanksFunc(ctx, competitionID)
}
return nil
}
func TestNewService(t *testing.T) {
repo := &MockRepository{}
svc := NewService(repo)
if svc == nil {
t.Fatal("Expected service to be created, got nil")
}
if svc.repo == nil {
t.Error("Expected repository to be set")
}
}
func TestCreateResult(t *testing.T) {
tests := []struct {
name string
input *domain.Result
repoError error
expectError bool
}{
{
name: "successful creation with new ID",
input: &domain.Result{
CompetitionID: uuid.New(),
UserID: uuid.New(),
Score: 95.5,
Status: domain.ResultStatusCompleted,
},
repoError: nil,
expectError: false,
},
{
name: "successful creation with existing ID",
input: &domain.Result{
ID: uuid.New(),
CompetitionID: uuid.New(),
UserID: uuid.New(),
Score: 95.5,
Status: domain.ResultStatusCompleted,
},
repoError: nil,
expectError: false,
},
{
name: "repository error",
input: &domain.Result{
CompetitionID: uuid.New(),
UserID: uuid.New(),
Score: 95.5,
},
repoError: errors.New("database error"),
expectError: true,
},
{
name: "empty status defaults to pending",
input: &domain.Result{
CompetitionID: uuid.New(),
UserID: uuid.New(),
Score: 95.5,
},
repoError: nil,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &MockRepository{
CreateFunc: func(ctx context.Context, result *domain.Result) error {
return tt.repoError
},
}
svc := NewService(repo)
result, err := svc.CreateResult(context.Background(), tt.input)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result.ID == uuid.Nil {
t.Error("Expected ID to be set")
}
if result.CreatedAt.IsZero() {
t.Error("Expected CreatedAt to be set")
}
if result.UpdatedAt.IsZero() {
t.Error("Expected UpdatedAt to be set")
}
if tt.input.Status == "" && result.Status != domain.ResultStatusPending {
t.Errorf("Expected status to default to PENDING, got %v", result.Status)
}
})
}
}
func TestGetResult(t *testing.T) {
resultID := uuid.New()
expectedResult := &domain.Result{
ID: resultID,
CompetitionID: uuid.New(),
UserID: uuid.New(),
Score: 95.5,
Status: domain.ResultStatusCompleted,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
tests := []struct {
name string
resultID uuid.UUID
repoResult *domain.Result
repoError error
expectError bool
}{
{
name: "successful get",
resultID: resultID,
repoResult: expectedResult,
repoError: nil,
expectError: false,
},
{
name: "result not found",
resultID: uuid.New(),
repoResult: nil,
repoError: domain.ErrResultNotFound,
expectError: true,
},
{
name: "repository error",
resultID: resultID,
repoResult: nil,
repoError: errors.New("database error"),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &MockRepository{
GetFunc: func(ctx context.Context, id uuid.UUID) (*domain.Result, error) {
if id != tt.resultID {
return nil, domain.ErrResultNotFound
}
return tt.repoResult, tt.repoError
},
}
svc := NewService(repo)
result, err := svc.GetResult(context.Background(), tt.resultID)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result.ID != expectedResult.ID {
t.Errorf("Expected ID %v, got %v", expectedResult.ID, result.ID)
}
})
}
}
func TestUpdateResult(t *testing.T) {
resultID := uuid.New()
originalTime := time.Now().Add(-1 * time.Hour)
tests := []struct {
name string
input *domain.Result
repoError error
expectError bool
}{
{
name: "successful update",
input: &domain.Result{
ID: resultID,
CompetitionID: uuid.New(),
UserID: uuid.New(),
Score: 100.0,
Status: domain.ResultStatusCompleted,
CreatedAt: originalTime,
UpdatedAt: originalTime,
},
repoError: nil,
expectError: false,
},
{
name: "repository error",
input: &domain.Result{
ID: resultID,
CompetitionID: uuid.New(),
UserID: uuid.New(),
Score: 100.0,
},
repoError: errors.New("update failed"),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &MockRepository{
UpdateFunc: func(ctx context.Context, result *domain.Result) error {
return tt.repoError
},
GetFunc: func(ctx context.Context, id uuid.UUID) (*domain.Result, error) {
if tt.repoError != nil {
return nil, tt.repoError
}
return tt.input, nil
},
}
svc := NewService(repo)
beforeUpdate := time.Now()
result, err := svc.UpdateResult(context.Background(), tt.input)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if !result.UpdatedAt.After(originalTime) {
t.Error("Expected UpdatedAt to be updated")
}
if result.UpdatedAt.Before(beforeUpdate) {
t.Error("Expected UpdatedAt to be recent")
}
})
}
}
func TestDeleteResult(t *testing.T) {
resultID := uuid.New()
tests := []struct {
name string
resultID uuid.UUID
repoError error
expectError bool
}{
{
name: "successful delete",
resultID: resultID,
repoError: nil,
expectError: false,
},
{
name: "result not found",
resultID: uuid.New(),
repoError: domain.ErrResultNotFound,
expectError: true,
},
{
name: "repository error",
resultID: resultID,
repoError: errors.New("delete failed"),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &MockRepository{
DeleteFunc: func(ctx context.Context, id uuid.UUID) error {
return tt.repoError
},
}
svc := NewService(repo)
err := svc.DeleteResult(context.Background(), tt.resultID)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
})
}
}
func TestListResults(t *testing.T) {
competitionID := uuid.New()
userID := uuid.New()
status := domain.ResultStatusCompleted
minScore := 50.0
maxScore := 100.0
results := []domain.Result{
{
ID: uuid.New(),
CompetitionID: competitionID,
UserID: userID,
Score: 95.0,
Status: status,
},
{
ID: uuid.New(),
CompetitionID: competitionID,
UserID: uuid.New(),
Score: 85.0,
Status: status,
},
}
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: "successful list with filters",
pageSize: 10,
pageToken: 0,
competitionID: &competitionID,
userID: &userID,
status: &status,
minScore: &minScore,
maxScore: &maxScore,
repoResults: results,
repoTotal: 2,
repoError: nil,
expectError: false,
expectedTotal: 2,
expectedNext: 0,
},
{
name: "pagination with next page",
pageSize: 10,
pageToken: 0,
competitionID: &competitionID,
repoResults: results,
repoTotal: 25,
repoError: nil,
expectError: false,
expectedTotal: 25,
expectedNext: 1,
},
{
name: "repository error",
pageSize: 10,
pageToken: 0,
repoResults: nil,
repoTotal: 0,
repoError: errors.New("database error"),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &MockRepository{
ListFunc: func(ctx context.Context, opts repository.ListResultsOptions) ([]domain.Result, int, error) {
return tt.repoResults, tt.repoTotal, tt.repoError
},
}
svc := NewService(repo)
results, total, nextPage, err := svc.ListResults(
context.Background(),
tt.pageSize,
tt.pageToken,
tt.competitionID,
tt.userID,
tt.status,
tt.minScore,
tt.maxScore,
)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if len(results) != len(tt.repoResults) {
t.Errorf("Expected %d results, got %d", len(tt.repoResults), len(results))
}
if total != tt.expectedTotal {
t.Errorf("Expected total %d, got %d", tt.expectedTotal, total)
}
if nextPage != tt.expectedNext {
t.Errorf("Expected next page token %d, got %d", tt.expectedNext, nextPage)
}
})
}
}
func TestGetByCompetitionAndUser(t *testing.T) {
competitionID := uuid.New()
userID := uuid.New()
expectedResult := &domain.Result{
ID: uuid.New(),
CompetitionID: competitionID,
UserID: userID,
Score: 95.5,
Status: domain.ResultStatusCompleted,
}
tests := []struct {
name string
competitionID uuid.UUID
userID uuid.UUID
repoResult *domain.Result
repoError error
expectError bool
}{
{
name: "successful get",
competitionID: competitionID,
userID: userID,
repoResult: expectedResult,
repoError: nil,
expectError: false,
},
{
name: "result not found",
competitionID: uuid.New(),
userID: uuid.New(),
repoResult: nil,
repoError: domain.ErrResultNotFound,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &MockRepository{
GetByCompetitionAndUserFunc: func(ctx context.Context, compID uuid.UUID, uID uuid.UUID) (*domain.Result, error) {
return tt.repoResult, tt.repoError
},
}
svc := NewService(repo)
result, err := svc.GetByCompetitionAndUser(context.Background(), tt.competitionID, tt.userID)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result.ID != expectedResult.ID {
t.Errorf("Expected ID %v, got %v", expectedResult.ID, result.ID)
}
})
}
}
func TestGetLeaderboard(t *testing.T) {
competitionID := uuid.New()
rank1, rank2, rank3 := 1, 2, 3
leaderboard := []domain.Result{
{
ID: uuid.New(),
CompetitionID: competitionID,
UserID: uuid.New(),
Score: 100.0,
Rank: &rank1,
Status: domain.ResultStatusCompleted,
},
{
ID: uuid.New(),
CompetitionID: competitionID,
UserID: uuid.New(),
Score: 95.0,
Rank: &rank2,
Status: domain.ResultStatusCompleted,
},
{
ID: uuid.New(),
CompetitionID: competitionID,
UserID: uuid.New(),
Score: 90.0,
Rank: &rank3,
Status: domain.ResultStatusCompleted,
},
}
tests := []struct {
name string
competitionID uuid.UUID
limit int32
offset int32
repoResults []domain.Result
repoTotal int
repoError error
expectError bool
}{
{
name: "successful leaderboard",
competitionID: competitionID,
limit: 10,
offset: 0,
repoResults: leaderboard,
repoTotal: 3,
repoError: nil,
expectError: false,
},
{
name: "empty leaderboard",
competitionID: uuid.New(),
limit: 10,
offset: 0,
repoResults: []domain.Result{},
repoTotal: 0,
repoError: nil,
expectError: false,
},
{
name: "repository error",
competitionID: competitionID,
limit: 10,
offset: 0,
repoResults: nil,
repoTotal: 0,
repoError: errors.New("database error"),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &MockRepository{
GetLeaderboardFunc: func(ctx context.Context, compID uuid.UUID, limit int, offset int) ([]domain.Result, int, error) {
return tt.repoResults, tt.repoTotal, tt.repoError
},
}
svc := NewService(repo)
results, total, err := svc.GetLeaderboard(context.Background(), tt.competitionID, tt.limit, tt.offset)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if len(results) != len(tt.repoResults) {
t.Errorf("Expected %d results, got %d", len(tt.repoResults), len(results))
}
if int32(total) != int32(tt.repoTotal) {
t.Errorf("Expected total %d, got %d", tt.repoTotal, total)
}
for i := 0; i < len(results)-1; i++ {
if results[i].Rank != nil && results[i+1].Rank != nil {
if *results[i].Rank > *results[i+1].Rank {
t.Error("Leaderboard should be sorted by rank ascending")
}
}
}
})
}
}
func TestUpdateStatus(t *testing.T) {
resultID := uuid.New()
updatedResult := &domain.Result{
ID: resultID,
Status: domain.ResultStatusCompleted,
UpdatedAt: time.Now(),
}
tests := []struct {
name string
resultID uuid.UUID
status domain.ResultStatus
repoResult *domain.Result
repoError error
expectError bool
}{
{
name: "successful status update",
resultID: resultID,
status: domain.ResultStatusCompleted,
repoResult: updatedResult,
repoError: nil,
expectError: false,
},
{
name: "result not found",
resultID: uuid.New(),
status: domain.ResultStatusCompleted,
repoResult: nil,
repoError: domain.ErrResultNotFound,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &MockRepository{
UpdateStatusFunc: func(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) {
return tt.repoResult, tt.repoError
},
}
svc := NewService(repo)
result, err := svc.UpdateStatus(context.Background(), tt.resultID, tt.status)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result.Status != tt.status {
t.Errorf("Expected status %v, got %v", tt.status, result.Status)
}
})
}
}
func TestRecalculateRanks(t *testing.T) {
competitionID := uuid.New()
tests := []struct {
name string
competitionID uuid.UUID
repoError error
expectError bool
}{
{
name: "successful recalculation",
competitionID: competitionID,
repoError: nil,
expectError: false,
},
{
name: "repository error",
competitionID: competitionID,
repoError: errors.New("recalculation failed"),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &MockRepository{
RecalculateRanksFunc: func(ctx context.Context, compID uuid.UUID) error {
return tt.repoError
},
}
svc := NewService(repo)
err := svc.RecalculateRanks(context.Background(), tt.competitionID)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
})
}
}