core tests
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DashboardPresenter } from './DashboardPresenter';
|
||||
import { DashboardDTO } from '../dto/DashboardDTO';
|
||||
|
||||
describe('DashboardPresenter', () => {
|
||||
it('should return the data as is (identity transformation)', () => {
|
||||
const presenter = new DashboardPresenter();
|
||||
const mockData: DashboardDTO = {
|
||||
driver: {
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
avatar: 'http://example.com/avatar.png',
|
||||
},
|
||||
statistics: {
|
||||
rating: 1500,
|
||||
rank: 10,
|
||||
starts: 50,
|
||||
wins: 5,
|
||||
podiums: 15,
|
||||
leagues: 3,
|
||||
},
|
||||
upcomingRaces: [],
|
||||
championshipStandings: [],
|
||||
recentActivity: [],
|
||||
};
|
||||
|
||||
const result = presenter.present(mockData);
|
||||
|
||||
expect(result).toBe(mockData);
|
||||
});
|
||||
});
|
||||
20
core/dashboard/domain/errors/DriverNotFoundError.test.ts
Normal file
20
core/dashboard/domain/errors/DriverNotFoundError.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DriverNotFoundError } from './DriverNotFoundError';
|
||||
|
||||
describe('DriverNotFoundError', () => {
|
||||
it('should create an error with the correct message and properties', () => {
|
||||
const driverId = 'driver-123';
|
||||
const error = new DriverNotFoundError(driverId);
|
||||
|
||||
expect(error.message).toBe(`Driver with ID "${driverId}" not found`);
|
||||
expect(error.name).toBe('DriverNotFoundError');
|
||||
expect(error.type).toBe('domain');
|
||||
expect(error.context).toBe('dashboard');
|
||||
expect(error.kind).toBe('not_found');
|
||||
});
|
||||
|
||||
it('should be an instance of Error', () => {
|
||||
const error = new DriverNotFoundError('123');
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
145
core/health/use-cases/CheckApiHealthUseCase.test.ts
Normal file
145
core/health/use-cases/CheckApiHealthUseCase.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* CheckApiHealthUseCase Test
|
||||
*
|
||||
* Tests for the health check use case that orchestrates health checks and emits events.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CheckApiHealthUseCase, CheckApiHealthUseCasePorts } from './CheckApiHealthUseCase';
|
||||
import { HealthCheckQuery, HealthCheckResult } from '../ports/HealthCheckQuery';
|
||||
import { HealthEventPublisher } from '../ports/HealthEventPublisher';
|
||||
|
||||
describe('CheckApiHealthUseCase', () => {
|
||||
let mockHealthCheckAdapter: HealthCheckQuery;
|
||||
let mockEventPublisher: HealthEventPublisher;
|
||||
let useCase: CheckApiHealthUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
mockHealthCheckAdapter = {
|
||||
performHealthCheck: vi.fn(),
|
||||
getStatus: vi.fn(),
|
||||
getHealth: vi.fn(),
|
||||
getReliability: vi.fn(),
|
||||
isAvailable: vi.fn(),
|
||||
};
|
||||
|
||||
mockEventPublisher = {
|
||||
publishHealthCheckCompleted: vi.fn(),
|
||||
publishHealthCheckFailed: vi.fn(),
|
||||
publishHealthCheckTimeout: vi.fn(),
|
||||
publishConnected: vi.fn(),
|
||||
publishDisconnected: vi.fn(),
|
||||
publishDegraded: vi.fn(),
|
||||
publishChecking: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new CheckApiHealthUseCase({
|
||||
healthCheckAdapter: mockHealthCheckAdapter,
|
||||
eventPublisher: mockEventPublisher,
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should perform health check and publish completed event when healthy', async () => {
|
||||
const mockResult: HealthCheckResult = {
|
||||
healthy: true,
|
||||
responseTime: 100,
|
||||
timestamp: new Date('2024-01-01T00:00:00Z'),
|
||||
};
|
||||
|
||||
mockHealthCheckAdapter.performHealthCheck.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockHealthCheckAdapter.performHealthCheck).toHaveBeenCalledTimes(1);
|
||||
expect(mockEventPublisher.publishHealthCheckCompleted).toHaveBeenCalledWith({
|
||||
healthy: true,
|
||||
responseTime: 100,
|
||||
timestamp: mockResult.timestamp,
|
||||
});
|
||||
expect(mockEventPublisher.publishHealthCheckFailed).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
|
||||
it('should perform health check and publish failed event when unhealthy', async () => {
|
||||
const mockResult: HealthCheckResult = {
|
||||
healthy: false,
|
||||
responseTime: 200,
|
||||
error: 'Connection timeout',
|
||||
timestamp: new Date('2024-01-01T00:00:00Z'),
|
||||
};
|
||||
|
||||
mockHealthCheckAdapter.performHealthCheck.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockHealthCheckAdapter.performHealthCheck).toHaveBeenCalledTimes(1);
|
||||
expect(mockEventPublisher.publishHealthCheckFailed).toHaveBeenCalledWith({
|
||||
error: 'Connection timeout',
|
||||
timestamp: mockResult.timestamp,
|
||||
});
|
||||
expect(mockEventPublisher.publishHealthCheckCompleted).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
|
||||
it('should handle errors during health check and publish failed event', async () => {
|
||||
const errorMessage = 'Network error';
|
||||
mockHealthCheckAdapter.performHealthCheck.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockHealthCheckAdapter.performHealthCheck).toHaveBeenCalledTimes(1);
|
||||
expect(mockEventPublisher.publishHealthCheckFailed).toHaveBeenCalledWith({
|
||||
error: errorMessage,
|
||||
timestamp: expect.any(Date),
|
||||
});
|
||||
expect(mockEventPublisher.publishHealthCheckCompleted).not.toHaveBeenCalled();
|
||||
expect(result.healthy).toBe(false);
|
||||
expect(result.responseTime).toBe(0);
|
||||
expect(result.error).toBe(errorMessage);
|
||||
expect(result.timestamp).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should handle non-Error objects during health check', async () => {
|
||||
mockHealthCheckAdapter.performHealthCheck.mockRejectedValue('String error');
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockEventPublisher.publishHealthCheckFailed).toHaveBeenCalledWith({
|
||||
error: 'String error',
|
||||
timestamp: expect.any(Date),
|
||||
});
|
||||
expect(result.error).toBe('String error');
|
||||
});
|
||||
|
||||
it('should handle unknown errors during health check', async () => {
|
||||
mockHealthCheckAdapter.performHealthCheck.mockRejectedValue(null);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockEventPublisher.publishHealthCheckFailed).toHaveBeenCalledWith({
|
||||
error: 'Unknown error',
|
||||
timestamp: expect.any(Date),
|
||||
});
|
||||
expect(result.error).toBe('Unknown error');
|
||||
});
|
||||
|
||||
it('should use default error message when result has no error', async () => {
|
||||
const mockResult: HealthCheckResult = {
|
||||
healthy: false,
|
||||
responseTime: 150,
|
||||
timestamp: new Date('2024-01-01T00:00:00Z'),
|
||||
};
|
||||
|
||||
mockHealthCheckAdapter.performHealthCheck.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(mockEventPublisher.publishHealthCheckFailed).toHaveBeenCalledWith({
|
||||
error: 'Unknown error',
|
||||
timestamp: mockResult.timestamp,
|
||||
});
|
||||
expect(result.error).toBe('Unknown error');
|
||||
});
|
||||
});
|
||||
});
|
||||
54
core/health/use-cases/GetConnectionStatusUseCase.test.ts
Normal file
54
core/health/use-cases/GetConnectionStatusUseCase.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { GetConnectionStatusUseCase, GetConnectionStatusUseCasePorts } from './GetConnectionStatusUseCase';
|
||||
import { HealthCheckQuery, ConnectionHealth } from '../ports/HealthCheckQuery';
|
||||
|
||||
describe('GetConnectionStatusUseCase', () => {
|
||||
it('should return connection status and metrics from the health check adapter', async () => {
|
||||
// Arrange
|
||||
const mockHealth: ConnectionHealth = {
|
||||
status: 'connected',
|
||||
lastCheck: new Date('2024-01-01T10:00:00Z'),
|
||||
lastSuccess: new Date('2024-01-01T10:00:00Z'),
|
||||
lastFailure: null,
|
||||
consecutiveFailures: 0,
|
||||
totalRequests: 100,
|
||||
successfulRequests: 99,
|
||||
failedRequests: 1,
|
||||
averageResponseTime: 150,
|
||||
};
|
||||
const mockReliability = 0.99;
|
||||
|
||||
const mockHealthCheckAdapter = {
|
||||
getHealth: vi.fn().mockReturnValue(mockHealth),
|
||||
getReliability: vi.fn().mockReturnValue(mockReliability),
|
||||
performHealthCheck: vi.fn(),
|
||||
getStatus: vi.fn(),
|
||||
isAvailable: vi.fn(),
|
||||
} as unknown as HealthCheckQuery;
|
||||
|
||||
const ports: GetConnectionStatusUseCasePorts = {
|
||||
healthCheckAdapter: mockHealthCheckAdapter,
|
||||
};
|
||||
|
||||
const useCase = new GetConnectionStatusUseCase(ports);
|
||||
|
||||
// Act
|
||||
const result = await useCase.execute();
|
||||
|
||||
// Assert
|
||||
expect(mockHealthCheckAdapter.getHealth).toHaveBeenCalled();
|
||||
expect(mockHealthCheckAdapter.getReliability).toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
status: 'connected',
|
||||
reliability: 0.99,
|
||||
totalRequests: 100,
|
||||
successfulRequests: 99,
|
||||
failedRequests: 1,
|
||||
consecutiveFailures: 0,
|
||||
averageResponseTime: 150,
|
||||
lastCheck: mockHealth.lastCheck,
|
||||
lastSuccess: mockHealth.lastSuccess,
|
||||
lastFailure: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { GetDriverRankingsUseCase, GetDriverRankingsUseCasePorts } from './GetDriverRankingsUseCase';
|
||||
import { ValidationError } from '../../../shared/errors/ValidationError';
|
||||
|
||||
describe('GetDriverRankingsUseCase', () => {
|
||||
let mockLeaderboardsRepository: any;
|
||||
let mockEventPublisher: any;
|
||||
let ports: GetDriverRankingsUseCasePorts;
|
||||
let useCase: GetDriverRankingsUseCase;
|
||||
|
||||
const mockDrivers = [
|
||||
{ id: '1', name: 'Alice', rating: 2000, raceCount: 10, teamId: 't1', teamName: 'Team A' },
|
||||
{ id: '2', name: 'Bob', rating: 1500, raceCount: 5, teamId: 't2', teamName: 'Team B' },
|
||||
{ id: '3', name: 'Charlie', rating: 1800, raceCount: 8 },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockLeaderboardsRepository = {
|
||||
findAllDrivers: vi.fn().mockResolvedValue([...mockDrivers]),
|
||||
};
|
||||
mockEventPublisher = {
|
||||
publishDriverRankingsAccessed: vi.fn().mockResolvedValue(undefined),
|
||||
publishLeaderboardsError: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
ports = {
|
||||
leaderboardsRepository: mockLeaderboardsRepository,
|
||||
eventPublisher: mockEventPublisher,
|
||||
};
|
||||
useCase = new GetDriverRankingsUseCase(ports);
|
||||
});
|
||||
|
||||
it('should return all drivers sorted by rating DESC by default', async () => {
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.drivers).toHaveLength(3);
|
||||
expect(result.drivers[0].name).toBe('Alice');
|
||||
expect(result.drivers[1].name).toBe('Charlie');
|
||||
expect(result.drivers[2].name).toBe('Bob');
|
||||
expect(result.drivers[0].rank).toBe(1);
|
||||
expect(result.drivers[1].rank).toBe(2);
|
||||
expect(result.drivers[2].rank).toBe(3);
|
||||
expect(mockEventPublisher.publishDriverRankingsAccessed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should filter drivers by search term', async () => {
|
||||
const result = await useCase.execute({ search: 'ali' });
|
||||
|
||||
expect(result.drivers).toHaveLength(1);
|
||||
expect(result.drivers[0].name).toBe('Alice');
|
||||
});
|
||||
|
||||
it('should filter drivers by minRating', async () => {
|
||||
const result = await useCase.execute({ minRating: 1700 });
|
||||
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers.map(d => d.name)).toContain('Alice');
|
||||
expect(result.drivers.map(d => d.name)).toContain('Charlie');
|
||||
});
|
||||
|
||||
it('should filter drivers by teamId', async () => {
|
||||
const result = await useCase.execute({ teamId: 't1' });
|
||||
|
||||
expect(result.drivers).toHaveLength(1);
|
||||
expect(result.drivers[0].name).toBe('Alice');
|
||||
});
|
||||
|
||||
it('should sort drivers by name ASC', async () => {
|
||||
const result = await useCase.execute({ sortBy: 'name', sortOrder: 'asc' });
|
||||
|
||||
expect(result.drivers[0].name).toBe('Alice');
|
||||
expect(result.drivers[1].name).toBe('Bob');
|
||||
expect(result.drivers[2].name).toBe('Charlie');
|
||||
});
|
||||
|
||||
it('should paginate results', async () => {
|
||||
const result = await useCase.execute({ page: 2, limit: 1 });
|
||||
|
||||
expect(result.drivers).toHaveLength(1);
|
||||
expect(result.drivers[0].name).toBe('Charlie'); // Alice (1), Charlie (2), Bob (3)
|
||||
expect(result.pagination.total).toBe(3);
|
||||
expect(result.pagination.totalPages).toBe(3);
|
||||
expect(result.pagination.page).toBe(2);
|
||||
});
|
||||
|
||||
it('should throw ValidationError for invalid page', async () => {
|
||||
await expect(useCase.execute({ page: 0 })).rejects.toThrow(ValidationError);
|
||||
expect(mockEventPublisher.publishLeaderboardsError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw ValidationError for invalid limit', async () => {
|
||||
await expect(useCase.execute({ limit: 0 })).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should throw ValidationError for invalid sortBy', async () => {
|
||||
await expect(useCase.execute({ sortBy: 'invalid' as any })).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { GetGlobalLeaderboardsUseCase, GetGlobalLeaderboardsUseCasePorts } from './GetGlobalLeaderboardsUseCase';
|
||||
|
||||
describe('GetGlobalLeaderboardsUseCase', () => {
|
||||
let mockLeaderboardsRepository: any;
|
||||
let mockEventPublisher: any;
|
||||
let ports: GetGlobalLeaderboardsUseCasePorts;
|
||||
let useCase: GetGlobalLeaderboardsUseCase;
|
||||
|
||||
const mockDrivers = [
|
||||
{ id: 'd1', name: 'Alice', rating: 2000, raceCount: 10 },
|
||||
{ id: 'd2', name: 'Bob', rating: 1500, raceCount: 5 },
|
||||
];
|
||||
|
||||
const mockTeams = [
|
||||
{ id: 't1', name: 'Team A', rating: 2500, memberCount: 5, raceCount: 20 },
|
||||
{ id: 't2', name: 'Team B', rating: 2200, memberCount: 3, raceCount: 15 },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockLeaderboardsRepository = {
|
||||
findAllDrivers: vi.fn().mockResolvedValue([...mockDrivers]),
|
||||
findAllTeams: vi.fn().mockResolvedValue([...mockTeams]),
|
||||
};
|
||||
mockEventPublisher = {
|
||||
publishGlobalLeaderboardsAccessed: vi.fn().mockResolvedValue(undefined),
|
||||
publishLeaderboardsError: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
ports = {
|
||||
leaderboardsRepository: mockLeaderboardsRepository,
|
||||
eventPublisher: mockEventPublisher,
|
||||
};
|
||||
useCase = new GetGlobalLeaderboardsUseCase(ports);
|
||||
});
|
||||
|
||||
it('should return top drivers and teams', async () => {
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers[0].name).toBe('Alice');
|
||||
expect(result.drivers[1].name).toBe('Bob');
|
||||
|
||||
expect(result.teams).toHaveLength(2);
|
||||
expect(result.teams[0].name).toBe('Team A');
|
||||
expect(result.teams[1].name).toBe('Team B');
|
||||
|
||||
expect(mockEventPublisher.publishGlobalLeaderboardsAccessed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should respect driver and team limits', async () => {
|
||||
const result = await useCase.execute({ driverLimit: 1, teamLimit: 1 });
|
||||
|
||||
expect(result.drivers).toHaveLength(1);
|
||||
expect(result.drivers[0].name).toBe('Alice');
|
||||
expect(result.teams).toHaveLength(1);
|
||||
expect(result.teams[0].name).toBe('Team A');
|
||||
});
|
||||
|
||||
it('should handle errors and publish error event', async () => {
|
||||
mockLeaderboardsRepository.findAllDrivers.mockRejectedValue(new Error('Repo error'));
|
||||
|
||||
await expect(useCase.execute()).rejects.toThrow('Repo error');
|
||||
expect(mockEventPublisher.publishLeaderboardsError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { GetTeamRankingsUseCase, GetTeamRankingsUseCasePorts } from './GetTeamRankingsUseCase';
|
||||
import { ValidationError } from '../../../shared/errors/ValidationError';
|
||||
|
||||
describe('GetTeamRankingsUseCase', () => {
|
||||
let mockLeaderboardsRepository: any;
|
||||
let mockEventPublisher: any;
|
||||
let ports: GetTeamRankingsUseCasePorts;
|
||||
let useCase: GetTeamRankingsUseCase;
|
||||
|
||||
const mockTeams = [
|
||||
{ id: 't1', name: 'Team A', rating: 2500, memberCount: 0, raceCount: 20 },
|
||||
{ id: 't2', name: 'Team B', rating: 2200, memberCount: 0, raceCount: 15 },
|
||||
];
|
||||
|
||||
const mockDrivers = [
|
||||
{ id: 'd1', name: 'Alice', rating: 2000, raceCount: 10, teamId: 't1', teamName: 'Team A' },
|
||||
{ id: 'd2', name: 'Bob', rating: 1500, raceCount: 5, teamId: 't1', teamName: 'Team A' },
|
||||
{ id: 'd3', name: 'Charlie', rating: 1800, raceCount: 8, teamId: 't2', teamName: 'Team B' },
|
||||
{ id: 'd4', name: 'David', rating: 1600, raceCount: 2, teamId: 't3', teamName: 'Discovered Team' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockLeaderboardsRepository = {
|
||||
findAllTeams: vi.fn().mockResolvedValue([...mockTeams]),
|
||||
findAllDrivers: vi.fn().mockResolvedValue([...mockDrivers]),
|
||||
};
|
||||
mockEventPublisher = {
|
||||
publishTeamRankingsAccessed: vi.fn().mockResolvedValue(undefined),
|
||||
publishLeaderboardsError: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
ports = {
|
||||
leaderboardsRepository: mockLeaderboardsRepository,
|
||||
eventPublisher: mockEventPublisher,
|
||||
};
|
||||
useCase = new GetTeamRankingsUseCase(ports);
|
||||
});
|
||||
|
||||
it('should return teams with aggregated member counts', async () => {
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.teams).toHaveLength(3); // Team A, Team B, and discovered Team t3
|
||||
|
||||
const teamA = result.teams.find(t => t.id === 't1');
|
||||
expect(teamA?.memberCount).toBe(2);
|
||||
|
||||
const teamB = result.teams.find(t => t.id === 't2');
|
||||
expect(teamB?.memberCount).toBe(1);
|
||||
|
||||
const teamDiscovered = result.teams.find(t => t.id === 't3');
|
||||
expect(teamDiscovered?.memberCount).toBe(1);
|
||||
expect(teamDiscovered?.name).toBe('Discovered Team');
|
||||
|
||||
expect(mockEventPublisher.publishTeamRankingsAccessed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should filter teams by search term', async () => {
|
||||
const result = await useCase.execute({ search: 'team a' });
|
||||
|
||||
expect(result.teams).toHaveLength(1);
|
||||
expect(result.teams[0].name).toBe('Team A');
|
||||
});
|
||||
|
||||
it('should filter teams by minMemberCount', async () => {
|
||||
const result = await useCase.execute({ minMemberCount: 2 });
|
||||
|
||||
expect(result.teams).toHaveLength(1);
|
||||
expect(result.teams[0].id).toBe('t1');
|
||||
});
|
||||
|
||||
it('should sort teams by rating DESC by default', async () => {
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.teams[0].id).toBe('t1'); // 2500
|
||||
expect(result.teams[1].id).toBe('t2'); // 2200
|
||||
expect(result.teams[2].id).toBe('t3'); // 0
|
||||
});
|
||||
|
||||
it('should throw ValidationError for invalid minMemberCount', async () => {
|
||||
await expect(useCase.execute({ minMemberCount: -1 })).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CreateLeagueUseCase } from './CreateLeagueUseCase';
|
||||
import { LeagueCreateCommand } from '../ports/LeagueCreateCommand';
|
||||
|
||||
describe('CreateLeagueUseCase', () => {
|
||||
let mockLeagueRepository: any;
|
||||
let mockEventPublisher: any;
|
||||
let useCase: CreateLeagueUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLeagueRepository = {
|
||||
create: vi.fn().mockImplementation((data) => Promise.resolve(data)),
|
||||
updateStats: vi.fn().mockResolvedValue(undefined),
|
||||
updateFinancials: vi.fn().mockResolvedValue(undefined),
|
||||
updateStewardingMetrics: vi.fn().mockResolvedValue(undefined),
|
||||
updatePerformanceMetrics: vi.fn().mockResolvedValue(undefined),
|
||||
updateRatingMetrics: vi.fn().mockResolvedValue(undefined),
|
||||
updateTrendMetrics: vi.fn().mockResolvedValue(undefined),
|
||||
updateSuccessRateMetrics: vi.fn().mockResolvedValue(undefined),
|
||||
updateResolutionTimeMetrics: vi.fn().mockResolvedValue(undefined),
|
||||
updateComplexSuccessRateMetrics: vi.fn().mockResolvedValue(undefined),
|
||||
updateComplexResolutionTimeMetrics: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
mockEventPublisher = {
|
||||
emitLeagueCreated: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
useCase = new CreateLeagueUseCase(mockLeagueRepository, mockEventPublisher);
|
||||
});
|
||||
|
||||
it('should create a league and initialize all metrics', async () => {
|
||||
const command: LeagueCreateCommand = {
|
||||
name: 'New League',
|
||||
ownerId: 'owner-1',
|
||||
visibility: 'public',
|
||||
approvalRequired: false,
|
||||
lateJoinAllowed: true,
|
||||
bonusPointsEnabled: true,
|
||||
penaltiesEnabled: true,
|
||||
protestsEnabled: true,
|
||||
appealsEnabled: true,
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.name).toBe('New League');
|
||||
expect(result.ownerId).toBe('owner-1');
|
||||
expect(mockLeagueRepository.create).toHaveBeenCalled();
|
||||
expect(mockLeagueRepository.updateStats).toHaveBeenCalled();
|
||||
expect(mockLeagueRepository.updateFinancials).toHaveBeenCalled();
|
||||
expect(mockEventPublisher.emitLeagueCreated).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if name is missing', async () => {
|
||||
const command: any = { ownerId: 'owner-1' };
|
||||
await expect(useCase.execute(command)).rejects.toThrow('League name is required');
|
||||
});
|
||||
|
||||
it('should throw error if ownerId is missing', async () => {
|
||||
const command: any = { name: 'League' };
|
||||
await expect(useCase.execute(command)).rejects.toThrow('Owner ID is required');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { DemoteAdminUseCase } from './DemoteAdminUseCase';
|
||||
|
||||
describe('DemoteAdminUseCase', () => {
|
||||
let mockLeagueRepository: any;
|
||||
let mockDriverRepository: any;
|
||||
let mockEventPublisher: any;
|
||||
let useCase: DemoteAdminUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLeagueRepository = {
|
||||
updateLeagueMember: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
mockDriverRepository = {};
|
||||
mockEventPublisher = {};
|
||||
useCase = new DemoteAdminUseCase(mockLeagueRepository, mockDriverRepository, mockEventPublisher as any);
|
||||
});
|
||||
|
||||
it('should update member role to member', async () => {
|
||||
const command = {
|
||||
leagueId: 'l1',
|
||||
targetDriverId: 'd1',
|
||||
actorId: 'owner-1',
|
||||
};
|
||||
|
||||
await useCase.execute(command);
|
||||
|
||||
expect(mockLeagueRepository.updateLeagueMember).toHaveBeenCalledWith('l1', 'd1', { role: 'member' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { GetLeagueRosterUseCase } from './GetLeagueRosterUseCase';
|
||||
|
||||
describe('GetLeagueRosterUseCase', () => {
|
||||
let mockLeagueRepository: any;
|
||||
let mockEventPublisher: any;
|
||||
let useCase: GetLeagueRosterUseCase;
|
||||
|
||||
const mockLeague = { id: 'league-1' };
|
||||
const mockMembers = [
|
||||
{ driverId: 'd1', name: 'Owner', role: 'owner', joinDate: new Date() },
|
||||
{ driverId: 'd2', name: 'Admin', role: 'admin', joinDate: new Date() },
|
||||
{ driverId: 'd3', name: 'Member', role: 'member', joinDate: new Date() },
|
||||
];
|
||||
const mockRequests = [
|
||||
{ id: 'r1', driverId: 'd4', name: 'Requester', requestDate: new Date() },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockLeagueRepository = {
|
||||
findById: vi.fn().mockResolvedValue(mockLeague),
|
||||
getLeagueMembers: vi.fn().mockResolvedValue(mockMembers),
|
||||
getPendingRequests: vi.fn().mockResolvedValue(mockRequests),
|
||||
};
|
||||
mockEventPublisher = {
|
||||
emitLeagueRosterAccessed: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
useCase = new GetLeagueRosterUseCase(mockLeagueRepository, mockEventPublisher);
|
||||
});
|
||||
|
||||
it('should return roster with members, requests and stats', async () => {
|
||||
const result = await useCase.execute({ leagueId: 'league-1' });
|
||||
|
||||
expect(result.members).toHaveLength(3);
|
||||
expect(result.pendingRequests).toHaveLength(1);
|
||||
expect(result.stats.adminCount).toBe(2); // owner + admin
|
||||
expect(result.stats.driverCount).toBe(1);
|
||||
expect(mockEventPublisher.emitLeagueRosterAccessed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if league not found', async () => {
|
||||
mockLeagueRepository.findById.mockResolvedValue(null);
|
||||
await expect(useCase.execute({ leagueId: 'invalid' })).rejects.toThrow('League with id invalid not found');
|
||||
});
|
||||
});
|
||||
52
core/leagues/application/use-cases/GetLeagueUseCase.test.ts
Normal file
52
core/leagues/application/use-cases/GetLeagueUseCase.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { GetLeagueUseCase, GetLeagueQuery } from './GetLeagueUseCase';
|
||||
|
||||
describe('GetLeagueUseCase', () => {
|
||||
let mockLeagueRepository: any;
|
||||
let mockEventPublisher: any;
|
||||
let useCase: GetLeagueUseCase;
|
||||
|
||||
const mockLeague = {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
ownerId: 'owner-1',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockLeagueRepository = {
|
||||
findById: vi.fn().mockResolvedValue(mockLeague),
|
||||
};
|
||||
mockEventPublisher = {
|
||||
emitLeagueAccessed: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
useCase = new GetLeagueUseCase(mockLeagueRepository, mockEventPublisher);
|
||||
});
|
||||
|
||||
it('should return league data', async () => {
|
||||
const query: GetLeagueQuery = { leagueId: 'league-1' };
|
||||
const result = await useCase.execute(query);
|
||||
|
||||
expect(result).toEqual(mockLeague);
|
||||
expect(mockLeagueRepository.findById).toHaveBeenCalledWith('league-1');
|
||||
expect(mockEventPublisher.emitLeagueAccessed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit event if driverId is provided', async () => {
|
||||
const query: GetLeagueQuery = { leagueId: 'league-1', driverId: 'driver-1' };
|
||||
await useCase.execute(query);
|
||||
|
||||
expect(mockEventPublisher.emitLeagueAccessed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if league not found', async () => {
|
||||
mockLeagueRepository.findById.mockResolvedValue(null);
|
||||
const query: GetLeagueQuery = { leagueId: 'non-existent' };
|
||||
|
||||
await expect(useCase.execute(query)).rejects.toThrow('League with id non-existent not found');
|
||||
});
|
||||
|
||||
it('should throw error if leagueId is missing', async () => {
|
||||
const query: any = {};
|
||||
await expect(useCase.execute(query)).rejects.toThrow('League ID is required');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SearchLeaguesUseCase, SearchLeaguesQuery } from './SearchLeaguesUseCase';
|
||||
|
||||
describe('SearchLeaguesUseCase', () => {
|
||||
let mockLeagueRepository: any;
|
||||
let useCase: SearchLeaguesUseCase;
|
||||
|
||||
const mockLeagues = [
|
||||
{ id: '1', name: 'League 1' },
|
||||
{ id: '2', name: 'League 2' },
|
||||
{ id: '3', name: 'League 3' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockLeagueRepository = {
|
||||
search: vi.fn().mockResolvedValue([...mockLeagues]),
|
||||
};
|
||||
useCase = new SearchLeaguesUseCase(mockLeagueRepository);
|
||||
});
|
||||
|
||||
it('should return search results with default limit', async () => {
|
||||
const query: SearchLeaguesQuery = { query: 'test' };
|
||||
const result = await useCase.execute(query);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(mockLeagueRepository.search).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
it('should respect limit and offset', async () => {
|
||||
const query: SearchLeaguesQuery = { query: 'test', limit: 1, offset: 1 };
|
||||
const result = await useCase.execute(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('2');
|
||||
});
|
||||
|
||||
it('should throw error if query is missing', async () => {
|
||||
const query: any = { query: '' };
|
||||
await expect(useCase.execute(query)).rejects.toThrow('Search query is required');
|
||||
});
|
||||
});
|
||||
@@ -1,278 +1,72 @@
|
||||
import { calculateRaceDates, getNextWeekday, type ScheduleConfig } from '@core/racing/domain/services/ScheduleCalculator';
|
||||
import type { Weekday } from '@core/racing/domain/types/Weekday';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateRaceDates, getNextWeekday, ScheduleConfig } from './ScheduleCalculator';
|
||||
|
||||
describe('ScheduleCalculator', () => {
|
||||
describe('calculateRaceDates', () => {
|
||||
describe('with empty or invalid input', () => {
|
||||
it('should return empty array when weekdays is empty', () => {
|
||||
// Given
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: [],
|
||||
frequency: 'weekly',
|
||||
rounds: 8,
|
||||
startDate: new Date('2024-01-01'),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
expect(result.raceDates).toEqual([]);
|
||||
expect(result.seasonDurationWeeks).toBe(0);
|
||||
});
|
||||
|
||||
it('should return empty array when rounds is 0', () => {
|
||||
// Given
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Sat'] as Weekday[],
|
||||
frequency: 'weekly',
|
||||
rounds: 0,
|
||||
startDate: new Date('2024-01-01'),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
expect(result.raceDates).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when rounds is negative', () => {
|
||||
// Given
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Sat'] as Weekday[],
|
||||
frequency: 'weekly',
|
||||
rounds: -5,
|
||||
startDate: new Date('2024-01-01'),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
expect(result.raceDates).toEqual([]);
|
||||
});
|
||||
it('should return empty array if no weekdays or rounds', () => {
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: [],
|
||||
frequency: 'weekly',
|
||||
rounds: 10,
|
||||
startDate: new Date('2024-01-01'),
|
||||
};
|
||||
expect(calculateRaceDates(config).raceDates).toHaveLength(0);
|
||||
});
|
||||
|
||||
describe('weekly scheduling', () => {
|
||||
it('should schedule 8 races on Saturdays starting from a Saturday', () => {
|
||||
// Given - January 6, 2024 is a Saturday
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Sat'] as Weekday[],
|
||||
frequency: 'weekly',
|
||||
rounds: 8,
|
||||
startDate: new Date('2024-01-06'),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
expect(result.raceDates.length).toBe(8);
|
||||
// All dates should be Saturdays
|
||||
result.raceDates.forEach(date => {
|
||||
expect(date.getDay()).toBe(6); // Saturday
|
||||
});
|
||||
// First race should be Jan 6
|
||||
expect(result.raceDates[0]!.toISOString().split('T')[0]).toBe('2024-01-06');
|
||||
// Last race should be 7 weeks later (Feb 24)
|
||||
expect(result.raceDates[7]!.toISOString().split('T')[0]).toBe('2024-02-24');
|
||||
});
|
||||
|
||||
it('should schedule races on multiple weekdays', () => {
|
||||
// Given
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Wed', 'Sat'] as Weekday[],
|
||||
frequency: 'weekly',
|
||||
rounds: 8,
|
||||
startDate: new Date('2024-01-01'), // Monday
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
expect(result.raceDates.length).toBe(8);
|
||||
// Should alternate between Wednesday and Saturday
|
||||
result.raceDates.forEach(date => {
|
||||
const day = date.getDay();
|
||||
expect([3, 6]).toContain(day); // Wed=3, Sat=6
|
||||
});
|
||||
});
|
||||
|
||||
it('should schedule 8 races on Sundays', () => {
|
||||
// Given - January 7, 2024 is a Sunday
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Sun'] as Weekday[],
|
||||
frequency: 'weekly',
|
||||
rounds: 8,
|
||||
startDate: new Date('2024-01-01'),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
expect(result.raceDates.length).toBe(8);
|
||||
result.raceDates.forEach(date => {
|
||||
expect(date.getDay()).toBe(0); // Sunday
|
||||
});
|
||||
});
|
||||
it('should schedule weekly races', () => {
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Mon'],
|
||||
frequency: 'weekly',
|
||||
rounds: 3,
|
||||
startDate: new Date('2024-01-01'), // Monday
|
||||
};
|
||||
const result = calculateRaceDates(config);
|
||||
expect(result.raceDates).toHaveLength(3);
|
||||
expect(result.raceDates[0].getDay()).toBe(1);
|
||||
expect(result.raceDates[1].getDay()).toBe(1);
|
||||
expect(result.raceDates[2].getDay()).toBe(1);
|
||||
// Check dates are 7 days apart
|
||||
const diff = result.raceDates[1].getTime() - result.raceDates[0].getTime();
|
||||
expect(diff).toBe(7 * 24 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
describe('bi-weekly scheduling', () => {
|
||||
it('should schedule races every 2 weeks on Saturdays', () => {
|
||||
// Given - January 6, 2024 is a Saturday
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Sat'] as Weekday[],
|
||||
frequency: 'everyNWeeks',
|
||||
rounds: 4,
|
||||
startDate: new Date('2024-01-06'),
|
||||
intervalWeeks: 2,
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
expect(result.raceDates.length).toBe(4);
|
||||
// First race Jan 6
|
||||
expect(result.raceDates[0]!.toISOString().split('T')[0]).toBe('2024-01-06');
|
||||
// Second race 2 weeks later (Jan 20)
|
||||
expect(result.raceDates[1]!.toISOString().split('T')[0]).toBe('2024-01-20');
|
||||
// Third race 2 weeks later (Feb 3)
|
||||
expect(result.raceDates[2]!.toISOString().split('T')[0]).toBe('2024-02-03');
|
||||
// Fourth race 2 weeks later (Feb 17)
|
||||
expect(result.raceDates[3]!.toISOString().split('T')[0]).toBe('2024-02-17');
|
||||
});
|
||||
it('should schedule bi-weekly races', () => {
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Mon'],
|
||||
frequency: 'everyNWeeks',
|
||||
intervalWeeks: 2,
|
||||
rounds: 2,
|
||||
startDate: new Date('2024-01-01'),
|
||||
};
|
||||
const result = calculateRaceDates(config);
|
||||
expect(result.raceDates).toHaveLength(2);
|
||||
const diff = result.raceDates[1].getTime() - result.raceDates[0].getTime();
|
||||
expect(diff).toBe(14 * 24 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
describe('with start and end dates', () => {
|
||||
it('should evenly distribute races across the date range', () => {
|
||||
// Given - 3 month season
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Sat'] as Weekday[],
|
||||
frequency: 'weekly',
|
||||
rounds: 8,
|
||||
startDate: new Date('2024-01-06'),
|
||||
endDate: new Date('2024-03-30'),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
expect(result.raceDates.length).toBe(8);
|
||||
// First race should be at or near start
|
||||
expect(result.raceDates[0]!.toISOString().split('T')[0]).toBe('2024-01-06');
|
||||
// Races should be spread across the range, not consecutive weeks
|
||||
});
|
||||
|
||||
it('should use all available days if fewer than rounds requested', () => {
|
||||
// Given - short period with only 3 Saturdays
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Sat'] as Weekday[],
|
||||
frequency: 'weekly',
|
||||
rounds: 10,
|
||||
startDate: new Date('2024-01-06'),
|
||||
endDate: new Date('2024-01-21'),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
// Only 3 Saturdays in this range: Jan 6, 13, 20
|
||||
expect(result.raceDates.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('season duration calculation', () => {
|
||||
it('should calculate correct season duration in weeks', () => {
|
||||
// Given
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Sat'] as Weekday[],
|
||||
frequency: 'weekly',
|
||||
rounds: 8,
|
||||
startDate: new Date('2024-01-06'),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
// 8 races, 1 week apart = 7 weeks duration
|
||||
expect(result.seasonDurationWeeks).toBe(7);
|
||||
});
|
||||
|
||||
it('should return 0 duration for single race', () => {
|
||||
// Given
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Sat'] as Weekday[],
|
||||
frequency: 'weekly',
|
||||
rounds: 1,
|
||||
startDate: new Date('2024-01-06'),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = calculateRaceDates(config);
|
||||
|
||||
// Then
|
||||
expect(result.raceDates.length).toBe(1);
|
||||
expect(result.seasonDurationWeeks).toBe(0);
|
||||
});
|
||||
it('should distribute races between start and end date', () => {
|
||||
const config: ScheduleConfig = {
|
||||
weekdays: ['Mon', 'Wed', 'Fri'],
|
||||
frequency: 'weekly',
|
||||
rounds: 2,
|
||||
startDate: new Date('2024-01-01'), // Mon
|
||||
endDate: new Date('2024-01-15'), // Mon
|
||||
};
|
||||
const result = calculateRaceDates(config);
|
||||
expect(result.raceDates).toHaveLength(2);
|
||||
// Use getTime() to avoid timezone issues in comparison
|
||||
const expectedDate = new Date('2024-01-01');
|
||||
expectedDate.setHours(12, 0, 0, 0);
|
||||
expect(result.raceDates[0].getTime()).toBe(expectedDate.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextWeekday', () => {
|
||||
it('should return next Saturday from a Monday', () => {
|
||||
// Given - January 1, 2024 is a Monday
|
||||
const fromDate = new Date('2024-01-01');
|
||||
|
||||
// When
|
||||
const result = getNextWeekday(fromDate, 'Sat');
|
||||
|
||||
// Then
|
||||
expect(result.toISOString().split('T')[0]).toBe('2024-01-06');
|
||||
expect(result.getDay()).toBe(6);
|
||||
});
|
||||
|
||||
it('should return next occurrence when already on that weekday', () => {
|
||||
// Given - January 6, 2024 is a Saturday
|
||||
const fromDate = new Date('2024-01-06');
|
||||
|
||||
// When
|
||||
const result = getNextWeekday(fromDate, 'Sat');
|
||||
|
||||
// Then
|
||||
// Should return NEXT Saturday (7 days later), not same day
|
||||
expect(result.toISOString().split('T')[0]).toBe('2024-01-13');
|
||||
});
|
||||
|
||||
it('should return next Sunday from a Friday', () => {
|
||||
// Given - January 5, 2024 is a Friday
|
||||
const fromDate = new Date('2024-01-05');
|
||||
|
||||
// When
|
||||
const result = getNextWeekday(fromDate, 'Sun');
|
||||
|
||||
// Then
|
||||
expect(result.toISOString().split('T')[0]).toBe('2024-01-07');
|
||||
expect(result.getDay()).toBe(0);
|
||||
});
|
||||
|
||||
it('should return next Wednesday from a Thursday', () => {
|
||||
// Given - January 4, 2024 is a Thursday
|
||||
const fromDate = new Date('2024-01-04');
|
||||
|
||||
// When
|
||||
const result = getNextWeekday(fromDate, 'Wed');
|
||||
|
||||
// Then
|
||||
// Next Wednesday is 6 days later
|
||||
expect(result.toISOString().split('T')[0]).toBe('2024-01-10');
|
||||
expect(result.getDay()).toBe(3);
|
||||
it('should return the next Monday', () => {
|
||||
const from = new Date('2024-01-01'); // Monday
|
||||
const next = getNextWeekday(from, 'Mon');
|
||||
expect(next.getDay()).toBe(1);
|
||||
expect(next.getDate()).toBe(8);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,19 +8,19 @@ describe('SkillLevelService', () => {
|
||||
expect(SkillLevelService.getSkillLevel(5000)).toBe('pro');
|
||||
});
|
||||
|
||||
it('should return advanced for rating >= 2500 and < 3000', () => {
|
||||
it('should return advanced for rating >= 2500', () => {
|
||||
expect(SkillLevelService.getSkillLevel(2500)).toBe('advanced');
|
||||
expect(SkillLevelService.getSkillLevel(2999)).toBe('advanced');
|
||||
});
|
||||
|
||||
it('should return intermediate for rating >= 1800 and < 2500', () => {
|
||||
it('should return intermediate for rating >= 1800', () => {
|
||||
expect(SkillLevelService.getSkillLevel(1800)).toBe('intermediate');
|
||||
expect(SkillLevelService.getSkillLevel(2499)).toBe('intermediate');
|
||||
});
|
||||
|
||||
it('should return beginner for rating < 1800', () => {
|
||||
expect(SkillLevelService.getSkillLevel(1799)).toBe('beginner');
|
||||
expect(SkillLevelService.getSkillLevel(500)).toBe('beginner');
|
||||
expect(SkillLevelService.getSkillLevel(0)).toBe('beginner');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,14 +33,12 @@ describe('SkillLevelService', () => {
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(4500)).toBe('pro');
|
||||
});
|
||||
|
||||
it('should return advanced for rating >= 3000 and < 4500', () => {
|
||||
it('should return advanced for rating >= 3000', () => {
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(3000)).toBe('advanced');
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(4499)).toBe('advanced');
|
||||
});
|
||||
|
||||
it('should return intermediate for rating >= 2000 and < 3000', () => {
|
||||
it('should return intermediate for rating >= 2000', () => {
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(2000)).toBe('intermediate');
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(2999)).toBe('intermediate');
|
||||
});
|
||||
|
||||
it('should return beginner for rating < 2000', () => {
|
||||
|
||||
@@ -1,54 +1,35 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AverageStrengthOfFieldCalculator } from './StrengthOfFieldCalculator';
|
||||
import { AverageStrengthOfFieldCalculator, DriverRating } from './StrengthOfFieldCalculator';
|
||||
|
||||
describe('AverageStrengthOfFieldCalculator', () => {
|
||||
const calculator = new AverageStrengthOfFieldCalculator();
|
||||
|
||||
it('should calculate average SOF and round it', () => {
|
||||
const ratings = [
|
||||
{ driverId: 'd1', rating: 1500 },
|
||||
{ driverId: 'd2', rating: 2000 },
|
||||
{ driverId: 'd3', rating: 1750 },
|
||||
];
|
||||
|
||||
const sof = calculator.calculate(ratings);
|
||||
|
||||
expect(sof).toBe(1750);
|
||||
});
|
||||
|
||||
it('should handle rounding correctly', () => {
|
||||
const ratings = [
|
||||
{ driverId: 'd1', rating: 1000 },
|
||||
{ driverId: 'd2', rating: 1001 },
|
||||
];
|
||||
|
||||
const sof = calculator.calculate(ratings);
|
||||
|
||||
expect(sof).toBe(1001); // (1000 + 1001) / 2 = 1000.5 -> 1001
|
||||
});
|
||||
|
||||
it('should return null for empty ratings', () => {
|
||||
it('should return null for empty list', () => {
|
||||
expect(calculator.calculate([])).toBeNull();
|
||||
});
|
||||
|
||||
it('should filter out non-positive ratings', () => {
|
||||
const ratings = [
|
||||
{ driverId: 'd1', rating: 1500 },
|
||||
{ driverId: 'd2', rating: 0 },
|
||||
{ driverId: 'd3', rating: -100 },
|
||||
it('should return null if no valid ratings (>0)', () => {
|
||||
const ratings: DriverRating[] = [
|
||||
{ driverId: '1', rating: 0 },
|
||||
{ driverId: '2', rating: -100 },
|
||||
];
|
||||
|
||||
const sof = calculator.calculate(ratings);
|
||||
|
||||
expect(sof).toBe(1500);
|
||||
});
|
||||
|
||||
it('should return null if all ratings are non-positive', () => {
|
||||
const ratings = [
|
||||
{ driverId: 'd1', rating: 0 },
|
||||
{ driverId: 'd2', rating: -500 },
|
||||
];
|
||||
|
||||
expect(calculator.calculate(ratings)).toBeNull();
|
||||
});
|
||||
|
||||
it('should calculate average of valid ratings', () => {
|
||||
const ratings: DriverRating[] = [
|
||||
{ driverId: '1', rating: 1000 },
|
||||
{ driverId: '2', rating: 2000 },
|
||||
{ driverId: '3', rating: 0 }, // Should be ignored
|
||||
];
|
||||
expect(calculator.calculate(ratings)).toBe(1500);
|
||||
});
|
||||
|
||||
it('should round the result', () => {
|
||||
const ratings: DriverRating[] = [
|
||||
{ driverId: '1', rating: 1000 },
|
||||
{ driverId: '2', rating: 1001 },
|
||||
];
|
||||
expect(calculator.calculate(ratings)).toBe(1001); // (1000+1001)/2 = 1000.5 -> 1001
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user