core tests

This commit is contained in:
2026-01-24 19:19:16 +01:00
parent 5da14b1b21
commit 1e821c4a5c
8 changed files with 1499 additions and 0 deletions

View File

@@ -0,0 +1,320 @@
/**
* Unit tests for GetDashboardUseCase
*
* Tests cover:
* 1) Validation of driverId (empty and whitespace)
* 2) Driver not found
* 3) Filters invalid races (missing trackName, past dates)
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GetDashboardUseCase } from './GetDashboardUseCase';
import { ValidationError } from '../../../shared/errors/ValidationError';
import { DriverNotFoundError } from '../../domain/errors/DriverNotFoundError';
import { DashboardRepository } from '../ports/DashboardRepository';
import { DashboardEventPublisher } from '../ports/DashboardEventPublisher';
import { Logger } from '../../../shared/domain/Logger';
import { DriverData, RaceData, LeagueStandingData, ActivityData } from '../ports/DashboardRepository';
describe('GetDashboardUseCase', () => {
let mockDriverRepository: DashboardRepository;
let mockRaceRepository: DashboardRepository;
let mockLeagueRepository: DashboardRepository;
let mockActivityRepository: DashboardRepository;
let mockEventPublisher: DashboardEventPublisher;
let mockLogger: Logger;
let useCase: GetDashboardUseCase;
beforeEach(() => {
// Mock all ports with vi.fn()
mockDriverRepository = {
findDriverById: vi.fn(),
getUpcomingRaces: vi.fn(),
getLeagueStandings: vi.fn(),
getRecentActivity: vi.fn(),
getFriends: vi.fn(),
};
mockRaceRepository = {
findDriverById: vi.fn(),
getUpcomingRaces: vi.fn(),
getLeagueStandings: vi.fn(),
getRecentActivity: vi.fn(),
getFriends: vi.fn(),
};
mockLeagueRepository = {
findDriverById: vi.fn(),
getUpcomingRaces: vi.fn(),
getLeagueStandings: vi.fn(),
getRecentActivity: vi.fn(),
getFriends: vi.fn(),
};
mockActivityRepository = {
findDriverById: vi.fn(),
getUpcomingRaces: vi.fn(),
getLeagueStandings: vi.fn(),
getRecentActivity: vi.fn(),
getFriends: vi.fn(),
};
mockEventPublisher = {
publishDashboardAccessed: vi.fn(),
publishDashboardError: vi.fn(),
};
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
useCase = new GetDashboardUseCase({
driverRepository: mockDriverRepository,
raceRepository: mockRaceRepository,
leagueRepository: mockLeagueRepository,
activityRepository: mockActivityRepository,
eventPublisher: mockEventPublisher,
logger: mockLogger,
});
});
describe('Scenario 1: Validation of driverId', () => {
it('should throw ValidationError when driverId is empty string', async () => {
// Given
const query = { driverId: '' };
// When & Then
await expect(useCase.execute(query)).rejects.toThrow(ValidationError);
await expect(useCase.execute(query)).rejects.toThrow('Driver ID cannot be empty');
// Verify no repositories were called
expect(mockDriverRepository.findDriverById).not.toHaveBeenCalled();
expect(mockRaceRepository.getUpcomingRaces).not.toHaveBeenCalled();
expect(mockLeagueRepository.getLeagueStandings).not.toHaveBeenCalled();
expect(mockActivityRepository.getRecentActivity).not.toHaveBeenCalled();
expect(mockEventPublisher.publishDashboardAccessed).not.toHaveBeenCalled();
});
it('should throw ValidationError when driverId is whitespace only', async () => {
// Given
const query = { driverId: ' ' };
// When & Then
await expect(useCase.execute(query)).rejects.toThrow(ValidationError);
await expect(useCase.execute(query)).rejects.toThrow('Driver ID cannot be empty');
// Verify no repositories were called
expect(mockDriverRepository.findDriverById).not.toHaveBeenCalled();
expect(mockRaceRepository.getUpcomingRaces).not.toHaveBeenCalled();
expect(mockLeagueRepository.getLeagueStandings).not.toHaveBeenCalled();
expect(mockActivityRepository.getRecentActivity).not.toHaveBeenCalled();
expect(mockEventPublisher.publishDashboardAccessed).not.toHaveBeenCalled();
});
});
describe('Scenario 2: Driver not found', () => {
it('should throw DriverNotFoundError when driverRepository.findDriverById returns null', async () => {
// Given
const query = { driverId: 'driver-123' };
(mockDriverRepository.findDriverById as any).mockResolvedValue(null);
// When & Then
await expect(useCase.execute(query)).rejects.toThrow(DriverNotFoundError);
await expect(useCase.execute(query)).rejects.toThrow('Driver with ID "driver-123" not found');
// Verify driver repository was called
expect(mockDriverRepository.findDriverById).toHaveBeenCalledWith('driver-123');
// Verify other repositories were not called (since driver not found)
expect(mockRaceRepository.getUpcomingRaces).not.toHaveBeenCalled();
expect(mockLeagueRepository.getLeagueStandings).not.toHaveBeenCalled();
expect(mockActivityRepository.getRecentActivity).not.toHaveBeenCalled();
expect(mockEventPublisher.publishDashboardAccessed).not.toHaveBeenCalled();
});
});
describe('Scenario 3: Filters invalid races', () => {
it('should exclude races missing trackName', async () => {
// Given
const query = { driverId: 'driver-123' };
// Mock driver exists
(mockDriverRepository.findDriverById as any).mockResolvedValue({
id: 'driver-123',
name: 'Test Driver',
rating: 1500,
rank: 10,
starts: 50,
wins: 10,
podiums: 20,
leagues: 3,
} as DriverData);
// Mock races with missing trackName
(mockRaceRepository.getUpcomingRaces as any).mockResolvedValue([
{
id: 'race-1',
trackName: '', // Missing trackName
carType: 'GT3',
scheduledDate: new Date('2026-01-25T10:00:00.000Z'),
},
{
id: 'race-2',
trackName: 'Track A',
carType: 'GT3',
scheduledDate: new Date('2026-01-26T10:00:00.000Z'),
},
] as RaceData[]);
(mockLeagueRepository.getLeagueStandings as any).mockResolvedValue([]);
(mockActivityRepository.getRecentActivity as any).mockResolvedValue([]);
// When
const result = await useCase.execute(query);
// Then
expect(result.upcomingRaces).toHaveLength(1);
expect(result.upcomingRaces[0].trackName).toBe('Track A');
});
it('should exclude races with past scheduledDate', async () => {
// Given
const query = { driverId: 'driver-123' };
// Mock driver exists
(mockDriverRepository.findDriverById as any).mockResolvedValue({
id: 'driver-123',
name: 'Test Driver',
rating: 1500,
rank: 10,
starts: 50,
wins: 10,
podiums: 20,
leagues: 3,
} as DriverData);
// Mock races with past dates
(mockRaceRepository.getUpcomingRaces as any).mockResolvedValue([
{
id: 'race-1',
trackName: 'Track A',
carType: 'GT3',
scheduledDate: new Date('2026-01-23T10:00:00.000Z'), // Past
},
{
id: 'race-2',
trackName: 'Track B',
carType: 'GT3',
scheduledDate: new Date('2026-01-25T10:00:00.000Z'), // Future
},
] as RaceData[]);
(mockLeagueRepository.getLeagueStandings as any).mockResolvedValue([]);
(mockActivityRepository.getRecentActivity as any).mockResolvedValue([]);
// When
const result = await useCase.execute(query);
// Then
expect(result.upcomingRaces).toHaveLength(1);
expect(result.upcomingRaces[0].trackName).toBe('Track B');
});
it('should exclude races with missing trackName and past dates', async () => {
// Given
const query = { driverId: 'driver-123' };
// Mock driver exists
(mockDriverRepository.findDriverById as any).mockResolvedValue({
id: 'driver-123',
name: 'Test Driver',
rating: 1500,
rank: 10,
starts: 50,
wins: 10,
podiums: 20,
leagues: 3,
} as DriverData);
// Mock races with various invalid states
(mockRaceRepository.getUpcomingRaces as any).mockResolvedValue([
{
id: 'race-1',
trackName: '', // Missing trackName
carType: 'GT3',
scheduledDate: new Date('2026-01-25T10:00:00.000Z'), // Future
},
{
id: 'race-2',
trackName: 'Track A',
carType: 'GT3',
scheduledDate: new Date('2026-01-23T10:00:00.000Z'), // Past
},
{
id: 'race-3',
trackName: 'Track B',
carType: 'GT3',
scheduledDate: new Date('2026-01-26T10:00:00.000Z'), // Future
},
] as RaceData[]);
(mockLeagueRepository.getLeagueStandings as any).mockResolvedValue([]);
(mockActivityRepository.getRecentActivity as any).mockResolvedValue([]);
// When
const result = await useCase.execute(query);
// Then
expect(result.upcomingRaces).toHaveLength(1);
expect(result.upcomingRaces[0].trackName).toBe('Track B');
});
it('should include only valid races with trackName and future dates', async () => {
// Given
const query = { driverId: 'driver-123' };
// Mock driver exists
(mockDriverRepository.findDriverById as any).mockResolvedValue({
id: 'driver-123',
name: 'Test Driver',
rating: 1500,
rank: 10,
starts: 50,
wins: 10,
podiums: 20,
leagues: 3,
} as DriverData);
// Mock races with valid data
(mockRaceRepository.getUpcomingRaces as any).mockResolvedValue([
{
id: 'race-1',
trackName: 'Track A',
carType: 'GT3',
scheduledDate: new Date('2026-01-25T10:00:00.000Z'),
},
{
id: 'race-2',
trackName: 'Track B',
carType: 'GT4',
scheduledDate: new Date('2026-01-26T10:00:00.000Z'),
},
] as RaceData[]);
(mockLeagueRepository.getLeagueStandings as any).mockResolvedValue([]);
(mockActivityRepository.getRecentActivity as any).mockResolvedValue([]);
// When
const result = await useCase.execute(query);
// Then
expect(result.upcomingRaces).toHaveLength(2);
expect(result.upcomingRaces[0].trackName).toBe('Track A');
expect(result.upcomingRaces[1].trackName).toBe('Track B');
});
});
});

View File

@@ -0,0 +1,242 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { JoinLeagueUseCase } from './JoinLeagueUseCase';
import type { LeagueRepository } from '../ports/LeagueRepository';
import type { DriverRepository } from '../../../racing/domain/repositories/DriverRepository';
import type { EventPublisher } from '../../../shared/ports/EventPublisher';
import type { JoinLeagueCommand } from '../ports/JoinLeagueCommand';
const mockLeagueRepository = {
findById: vi.fn(),
addPendingRequests: vi.fn(),
addLeagueMembers: vi.fn(),
};
const mockDriverRepository = {
findDriverById: vi.fn(),
};
const mockEventPublisher = {
publish: vi.fn(),
};
describe('JoinLeagueUseCase', () => {
let useCase: JoinLeagueUseCase;
beforeEach(() => {
// Reset mocks
vi.clearAllMocks();
useCase = new JoinLeagueUseCase(
mockLeagueRepository as any,
mockDriverRepository as any,
mockEventPublisher as any
);
});
describe('Scenario 1: League missing', () => {
it('should throw "League not found" when league does not exist', async () => {
// Given
const command: JoinLeagueCommand = {
leagueId: 'league-123',
driverId: 'driver-456',
};
mockLeagueRepository.findById.mockImplementation(() => Promise.resolve(null));
// When & Then
await expect(useCase.execute(command)).rejects.toThrow('League not found');
expect(mockLeagueRepository.findById).toHaveBeenCalledWith('league-123');
});
});
describe('Scenario 2: Driver missing', () => {
it('should throw "Driver not found" when driver does not exist', async () => {
// Given
const command: JoinLeagueCommand = {
leagueId: 'league-123',
driverId: 'driver-456',
};
const mockLeague = {
id: 'league-123',
name: 'Test League',
description: null,
visibility: 'public' as const,
ownerId: 'owner-789',
status: 'active' as const,
createdAt: new Date(),
updatedAt: new Date(),
maxDrivers: null,
approvalRequired: true,
lateJoinAllowed: true,
raceFrequency: null,
raceDay: null,
raceTime: null,
tracks: null,
scoringSystem: null,
bonusPointsEnabled: false,
penaltiesEnabled: false,
protestsEnabled: false,
appealsEnabled: false,
stewardTeam: null,
gameType: null,
skillLevel: null,
category: null,
tags: null,
};
mockLeagueRepository.findById.mockImplementation(() => Promise.resolve(mockLeague));
mockDriverRepository.findDriverById.mockImplementation(() => Promise.resolve(null));
// When & Then
await expect(useCase.execute(command)).rejects.toThrow('Driver not found');
expect(mockLeagueRepository.findById).toHaveBeenCalledWith('league-123');
expect(mockDriverRepository.findDriverById).toHaveBeenCalledWith('driver-456');
});
});
describe('Scenario 3: approvalRequired path uses pending requests + time determinism', () => {
it('should add pending request with deterministic time when approvalRequired is true', async () => {
// Given
const command: JoinLeagueCommand = {
leagueId: 'league-123',
driverId: 'driver-456',
};
const mockLeague = {
id: 'league-123',
name: 'Test League',
description: null,
visibility: 'public' as const,
ownerId: 'owner-789',
status: 'active' as const,
createdAt: new Date(),
updatedAt: new Date(),
maxDrivers: null,
approvalRequired: true,
lateJoinAllowed: true,
raceFrequency: null,
raceDay: null,
raceTime: null,
tracks: null,
scoringSystem: null,
bonusPointsEnabled: false,
penaltiesEnabled: false,
protestsEnabled: false,
appealsEnabled: false,
stewardTeam: null,
gameType: null,
skillLevel: null,
category: null,
tags: null,
};
const mockDriver = {
id: 'driver-456',
name: 'Test Driver',
iracingId: 'iracing-123',
avatarUrl: null,
createdAt: new Date(),
updatedAt: new Date(),
};
// Freeze time for deterministic testing
const frozenTime = new Date('2024-01-01T00:00:00.000Z');
vi.setSystemTime(frozenTime);
mockLeagueRepository.findById.mockResolvedValue(mockLeague);
mockDriverRepository.findDriverById.mockResolvedValue(mockDriver);
// When
await useCase.execute(command);
// Then
expect(mockLeagueRepository.addPendingRequests).toHaveBeenCalledWith(
'league-123',
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
driverId: 'driver-456',
name: 'Test Driver',
requestDate: frozenTime,
}),
])
);
// Verify no members were added
expect(mockLeagueRepository.addLeagueMembers).not.toHaveBeenCalled();
// Reset system time
vi.useRealTimers();
});
});
describe('Scenario 4: no-approval path adds member', () => {
it('should add member when approvalRequired is false', async () => {
// Given
const command: JoinLeagueCommand = {
leagueId: 'league-123',
driverId: 'driver-456',
};
const mockLeague = {
id: 'league-123',
name: 'Test League',
description: null,
visibility: 'public' as const,
ownerId: 'owner-789',
status: 'active' as const,
createdAt: new Date(),
updatedAt: new Date(),
maxDrivers: null,
approvalRequired: false,
lateJoinAllowed: true,
raceFrequency: null,
raceDay: null,
raceTime: null,
tracks: null,
scoringSystem: null,
bonusPointsEnabled: false,
penaltiesEnabled: false,
protestsEnabled: false,
appealsEnabled: false,
stewardTeam: null,
gameType: null,
skillLevel: null,
category: null,
tags: null,
};
const mockDriver = {
id: 'driver-456',
name: 'Test Driver',
iracingId: 'iracing-123',
avatarUrl: null,
createdAt: new Date(),
updatedAt: new Date(),
};
mockLeagueRepository.findById.mockResolvedValue(mockLeague);
mockDriverRepository.findDriverById.mockResolvedValue(mockDriver);
// When
await useCase.execute(command);
// Then
expect(mockLeagueRepository.addLeagueMembers).toHaveBeenCalledWith(
'league-123',
expect.arrayContaining([
expect.objectContaining({
driverId: 'driver-456',
name: 'Test Driver',
role: 'member',
joinDate: expect.any(Date),
}),
])
);
// Verify no pending requests were added
expect(mockLeagueRepository.addPendingRequests).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,354 @@
/**
* Unit tests for CalculateRatingUseCase
*
* Tests business logic and orchestration using mocked ports.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CalculateRatingUseCase } from './CalculateRatingUseCase';
import { Driver } from '../../../racing/domain/entities/Driver';
import { Race } from '../../../racing/domain/entities/Race';
import { Result } from '../../../racing/domain/entities/result/Result';
import { Rating } from '../../domain/Rating';
import { RatingCalculatedEvent } from '../../domain/events/RatingCalculatedEvent';
// Mock repositories and publisher
const mockDriverRepository = {
findById: vi.fn(),
};
const mockRaceRepository = {
findById: vi.fn(),
};
const mockResultRepository = {
findByRaceId: vi.fn(),
};
const mockRatingRepository = {
save: vi.fn(),
};
const mockEventPublisher = {
publish: vi.fn(),
};
describe('CalculateRatingUseCase', () => {
let useCase: CalculateRatingUseCase;
beforeEach(() => {
vi.clearAllMocks();
useCase = new CalculateRatingUseCase({
driverRepository: mockDriverRepository as any,
raceRepository: mockRaceRepository as any,
resultRepository: mockResultRepository as any,
ratingRepository: mockRatingRepository as any,
eventPublisher: mockEventPublisher as any,
});
});
describe('Scenario 1: Driver missing', () => {
it('should return error when driver is not found', async () => {
// Given
mockDriverRepository.findById.mockResolvedValue(null);
// When
const result = await useCase.execute({
driverId: 'driver-123',
raceId: 'race-456',
});
// Then
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().message).toBe('Driver not found');
expect(mockRatingRepository.save).not.toHaveBeenCalled();
});
});
describe('Scenario 2: Race missing', () => {
it('should return error when race is not found', async () => {
// Given
const mockDriver = Driver.create({
id: 'driver-123',
iracingId: 'iracing-123',
name: 'Test Driver',
country: 'US',
});
mockDriverRepository.findById.mockResolvedValue(mockDriver);
mockRaceRepository.findById.mockResolvedValue(null);
// When
const result = await useCase.execute({
driverId: 'driver-123',
raceId: 'race-456',
});
// Then
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().message).toBe('Race not found');
});
});
describe('Scenario 3: No results', () => {
it('should return error when no results found for race', async () => {
// Given
const mockDriver = Driver.create({
id: 'driver-123',
iracingId: 'iracing-123',
name: 'Test Driver',
country: 'US',
});
const mockRace = Race.create({
id: 'race-456',
leagueId: 'league-789',
scheduledAt: new Date(),
track: 'Test Track',
car: 'Test Car',
});
mockDriverRepository.findById.mockResolvedValue(mockDriver);
mockRaceRepository.findById.mockResolvedValue(mockRace);
mockResultRepository.findByRaceId.mockResolvedValue([]);
// When
const result = await useCase.execute({
driverId: 'driver-123',
raceId: 'race-456',
});
// Then
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().message).toBe('No results found for race');
});
});
describe('Scenario 4: Driver not present in results', () => {
it('should return error when driver is not in race results', async () => {
// Given
const mockDriver = Driver.create({
id: 'driver-123',
iracingId: 'iracing-123',
name: 'Test Driver',
country: 'US',
});
const mockRace = Race.create({
id: 'race-456',
leagueId: 'league-789',
scheduledAt: new Date(),
track: 'Test Track',
car: 'Test Car',
});
const otherResult = Result.create({
id: 'result-1',
raceId: 'race-456',
driverId: 'driver-456',
position: 1,
fastestLap: 60000,
incidents: 0,
startPosition: 1,
points: 25,
});
mockDriverRepository.findById.mockResolvedValue(mockDriver);
mockRaceRepository.findById.mockResolvedValue(mockRace);
mockResultRepository.findByRaceId.mockResolvedValue([otherResult]);
// When
const result = await useCase.execute({
driverId: 'driver-123',
raceId: 'race-456',
});
// Then
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().message).toBe('Driver not found in race results');
});
});
describe('Scenario 5: Publishes event after save', () => {
it('should call ratingRepository.save before eventPublisher.publish', async () => {
// Given
const mockDriver = Driver.create({
id: 'driver-123',
iracingId: 'iracing-123',
name: 'Test Driver',
country: 'US',
});
const mockRace = Race.create({
id: 'race-456',
leagueId: 'league-789',
scheduledAt: new Date(),
track: 'Test Track',
car: 'Test Car',
});
const mockResult = Result.create({
id: 'result-1',
raceId: 'race-456',
driverId: 'driver-123',
position: 1,
fastestLap: 60000,
incidents: 0,
startPosition: 1,
points: 25,
});
mockDriverRepository.findById.mockResolvedValue(mockDriver);
mockRaceRepository.findById.mockResolvedValue(mockRace);
mockResultRepository.findByRaceId.mockResolvedValue([mockResult]);
mockRatingRepository.save.mockResolvedValue(undefined);
mockEventPublisher.publish.mockResolvedValue(undefined);
// When
const result = await useCase.execute({
driverId: 'driver-123',
raceId: 'race-456',
});
// Then
expect(result.isOk()).toBe(true);
expect(mockRatingRepository.save).toHaveBeenCalledTimes(1);
expect(mockEventPublisher.publish).toHaveBeenCalledTimes(1);
// Verify call order: save should be called before publish
const saveCallOrder = mockRatingRepository.save.mock.invocationCallOrder[0];
const publishCallOrder = mockEventPublisher.publish.mock.invocationCallOrder[0];
expect(saveCallOrder).toBeLessThan(publishCallOrder);
});
});
describe('Scenario 6: Component boundaries for cleanDriving', () => {
it('should return cleanDriving = 100 when incidents = 0', async () => {
// Given
const mockDriver = Driver.create({
id: 'driver-123',
iracingId: 'iracing-123',
name: 'Test Driver',
country: 'US',
});
const mockRace = Race.create({
id: 'race-456',
leagueId: 'league-789',
scheduledAt: new Date(),
track: 'Test Track',
car: 'Test Car',
});
const mockResult = Result.create({
id: 'result-1',
raceId: 'race-456',
driverId: 'driver-123',
position: 1,
fastestLap: 60000,
incidents: 0,
startPosition: 1,
points: 25,
});
mockDriverRepository.findById.mockResolvedValue(mockDriver);
mockRaceRepository.findById.mockResolvedValue(mockRace);
mockResultRepository.findByRaceId.mockResolvedValue([mockResult]);
mockRatingRepository.save.mockResolvedValue(undefined);
mockEventPublisher.publish.mockResolvedValue(undefined);
// When
const result = await useCase.execute({
driverId: 'driver-123',
raceId: 'race-456',
});
// Then
expect(result.isOk()).toBe(true);
const rating = result.unwrap();
expect(rating.components.cleanDriving).toBe(100);
});
it('should return cleanDriving = 20 when incidents >= 5', async () => {
// Given
const mockDriver = Driver.create({
id: 'driver-123',
iracingId: 'iracing-123',
name: 'Test Driver',
country: 'US',
});
const mockRace = Race.create({
id: 'race-456',
leagueId: 'league-789',
scheduledAt: new Date(),
track: 'Test Track',
car: 'Test Car',
});
const mockResult = Result.create({
id: 'result-1',
raceId: 'race-456',
driverId: 'driver-123',
position: 1,
fastestLap: 60000,
incidents: 5,
startPosition: 1,
points: 25,
});
mockDriverRepository.findById.mockResolvedValue(mockDriver);
mockRaceRepository.findById.mockResolvedValue(mockRace);
mockResultRepository.findByRaceId.mockResolvedValue([mockResult]);
mockRatingRepository.save.mockResolvedValue(undefined);
mockEventPublisher.publish.mockResolvedValue(undefined);
// When
const result = await useCase.execute({
driverId: 'driver-123',
raceId: 'race-456',
});
// Then
expect(result.isOk()).toBe(true);
const rating = result.unwrap();
expect(rating.components.cleanDriving).toBe(20);
});
});
describe('Scenario 7: Time-dependent output', () => {
it('should produce deterministic timestamp when time is frozen', async () => {
// Given
const frozenTime = new Date('2024-01-01T12:00:00.000Z');
vi.useFakeTimers();
vi.setSystemTime(frozenTime);
const mockDriver = Driver.create({
id: 'driver-123',
iracingId: 'iracing-123',
name: 'Test Driver',
country: 'US',
});
const mockRace = Race.create({
id: 'race-456',
leagueId: 'league-789',
scheduledAt: new Date(),
track: 'Test Track',
car: 'Test Car',
});
const mockResult = Result.create({
id: 'result-1',
raceId: 'race-456',
driverId: 'driver-123',
position: 1,
fastestLap: 60000,
incidents: 0,
startPosition: 1,
points: 25,
});
mockDriverRepository.findById.mockResolvedValue(mockDriver);
mockRaceRepository.findById.mockResolvedValue(mockRace);
mockResultRepository.findByRaceId.mockResolvedValue([mockResult]);
mockRatingRepository.save.mockResolvedValue(undefined);
mockEventPublisher.publish.mockResolvedValue(undefined);
// When
const result = await useCase.execute({
driverId: 'driver-123',
raceId: 'race-456',
});
// Then
expect(result.isOk()).toBe(true);
const rating = result.unwrap();
expect(rating.timestamp).toEqual(frozenTime);
vi.useRealTimers();
});
});
});

View File

@@ -0,0 +1,132 @@
/**
* Unit tests for CalculateTeamContributionUseCase
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CalculateTeamContributionUseCase } from './CalculateTeamContributionUseCase';
import { Driver } from '../../../racing/domain/entities/Driver';
import { Race } from '../../../racing/domain/entities/Race';
import { Result } from '../../../racing/domain/entities/result/Result';
import { Rating } from '../../domain/Rating';
const mockRatingRepository = {
findByDriverAndRace: vi.fn(),
save: vi.fn(),
};
const mockDriverRepository = {
findById: vi.fn(),
};
const mockRaceRepository = {
findById: vi.fn(),
};
const mockResultRepository = {
findByRaceId: vi.fn(),
};
describe('CalculateTeamContributionUseCase', () => {
let useCase: CalculateTeamContributionUseCase;
beforeEach(() => {
vi.clearAllMocks();
useCase = new CalculateTeamContributionUseCase({
ratingRepository: mockRatingRepository as any,
driverRepository: mockDriverRepository as any,
raceRepository: mockRaceRepository as any,
resultRepository: mockResultRepository as any,
});
});
describe('Scenario 8: Creates rating when missing', () => {
it('should create and save a new rating when none exists', async () => {
// Given
const driverId = 'driver-1';
const raceId = 'race-1';
const points = 25;
mockDriverRepository.findById.mockResolvedValue(Driver.create({
id: driverId,
iracingId: 'ir-1',
name: 'Driver 1',
country: 'US'
}));
mockRaceRepository.findById.mockResolvedValue(Race.create({
id: raceId,
leagueId: 'l-1',
scheduledAt: new Date(),
track: 'Track',
car: 'Car'
}));
mockResultRepository.findByRaceId.mockResolvedValue([
Result.create({
id: 'res-1',
raceId,
driverId,
position: 1,
points,
incidents: 0,
startPosition: 1,
fastestLap: 0
})
]);
mockRatingRepository.findByDriverAndRace.mockResolvedValue(null);
// When
const result = await useCase.execute({ driverId, raceId });
// Then
expect(mockRatingRepository.save).toHaveBeenCalled();
const savedRating = mockRatingRepository.save.mock.calls[0][0] as Rating;
expect(savedRating.components.teamContribution).toBe(100); // 25/25 * 100
expect(result.teamContribution).toBe(100);
});
});
describe('Scenario 9: Updates existing rating', () => {
it('should preserve other fields and only update teamContribution', async () => {
// Given
const driverId = 'driver-1';
const raceId = 'race-1';
const points = 12.5; // 50% contribution
const existingRating = Rating.create({
driverId: 'driver-1' as any, // Simplified for test
raceId: 'race-1' as any,
rating: 1500,
components: {
resultsStrength: 80,
consistency: 70,
cleanDriving: 90,
racecraft: 75,
reliability: 85,
teamContribution: 10, // Old value
},
timestamp: new Date('2023-01-01')
});
mockDriverRepository.findById.mockResolvedValue({ id: driverId } as any);
mockRaceRepository.findById.mockResolvedValue({ id: raceId } as any);
mockResultRepository.findByRaceId.mockResolvedValue([
{ driverId: { toString: () => driverId }, points } as any
]);
mockRatingRepository.findByDriverAndRace.mockResolvedValue(existingRating);
// When
const result = await useCase.execute({ driverId, raceId });
// Then
expect(mockRatingRepository.save).toHaveBeenCalled();
const savedRating = mockRatingRepository.save.mock.calls[0][0] as Rating;
// Check preserved fields
expect(savedRating.rating).toBe(1500);
expect(savedRating.components.resultsStrength).toBe(80);
// Check updated field
expect(savedRating.components.teamContribution).toBe(50); // 12.5/25 * 100
expect(result.teamContribution).toBe(50);
});
});
});

View File

@@ -0,0 +1,105 @@
/**
* Unit tests for GetRatingLeaderboardUseCase
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GetRatingLeaderboardUseCase } from './GetRatingLeaderboardUseCase';
import { Rating } from '../../domain/Rating';
const mockRatingRepository = {
findByDriver: vi.fn(),
};
const mockDriverRepository = {
findAll: vi.fn(),
findById: vi.fn(),
};
describe('GetRatingLeaderboardUseCase', () => {
let useCase: GetRatingLeaderboardUseCase;
beforeEach(() => {
vi.clearAllMocks();
useCase = new GetRatingLeaderboardUseCase({
ratingRepository: mockRatingRepository as any,
driverRepository: mockDriverRepository as any,
});
});
describe('Scenario 10: Pagination + Sorting', () => {
it('should return latest rating per driver, sorted desc, sliced by limit/offset', async () => {
// Given
const drivers = [
{ id: 'd1', name: { toString: () => 'Driver 1' } },
{ id: 'd2', name: { toString: () => 'Driver 2' } },
{ id: 'd3', name: { toString: () => 'Driver 3' } },
];
const ratingsD1 = [
Rating.create({
driverId: 'd1' as any,
raceId: 'r1' as any,
rating: 1000,
components: {} as any,
timestamp: new Date('2023-01-01')
}),
Rating.create({
driverId: 'd1' as any,
raceId: 'r2' as any,
rating: 1200, // Latest for D1
components: {} as any,
timestamp: new Date('2023-01-02')
})
];
const ratingsD2 = [
Rating.create({
driverId: 'd2' as any,
raceId: 'r1' as any,
rating: 1500, // Latest for D2
components: {} as any,
timestamp: new Date('2023-01-01')
})
];
const ratingsD3 = [
Rating.create({
driverId: 'd3' as any,
raceId: 'r1' as any,
rating: 800, // Latest for D3
components: {} as any,
timestamp: new Date('2023-01-01')
})
];
mockDriverRepository.findAll.mockResolvedValue(drivers);
mockDriverRepository.findById.mockImplementation((id) =>
Promise.resolve(drivers.find(d => d.id === id))
);
mockRatingRepository.findByDriver.mockImplementation((id) => {
if (id === 'd1') return Promise.resolve(ratingsD1);
if (id === 'd2') return Promise.resolve(ratingsD2);
if (id === 'd3') return Promise.resolve(ratingsD3);
return Promise.resolve([]);
});
// When: limit 2, offset 0
const result = await useCase.execute({ limit: 2, offset: 0 });
// Then: Sorted D2 (1500), D1 (1200), D3 (800). Slice(0, 2) -> D2, D1
expect(result).toHaveLength(2);
expect(result[0].driverId).toBe('d2');
expect(result[0].rating).toBe(1500);
expect(result[1].driverId).toBe('d1');
expect(result[1].rating).toBe(1200);
// When: limit 2, offset 1
const resultOffset = await useCase.execute({ limit: 2, offset: 1 });
// Then: Slice(1, 3) -> D1, D3
expect(resultOffset).toHaveLength(2);
expect(resultOffset[0].driverId).toBe('d1');
expect(resultOffset[1].driverId).toBe('d3');
});
});
});

View File

@@ -0,0 +1,48 @@
/**
* Unit tests for SaveRatingUseCase
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SaveRatingUseCase } from './SaveRatingUseCase';
const mockRatingRepository = {
save: vi.fn(),
};
describe('SaveRatingUseCase', () => {
let useCase: SaveRatingUseCase;
beforeEach(() => {
vi.clearAllMocks();
useCase = new SaveRatingUseCase({
ratingRepository: mockRatingRepository as any,
});
});
describe('Scenario 11: Repository error wraps correctly', () => {
it('should wrap repository error with specific prefix', async () => {
// Given
const request = {
driverId: 'd1',
raceId: 'r1',
rating: 1200,
components: {
resultsStrength: 80,
consistency: 70,
cleanDriving: 90,
racecraft: 75,
reliability: 85,
teamContribution: 60,
},
};
const repoError = new Error('Database connection failed');
mockRatingRepository.save.mockRejectedValue(repoError);
// When & Then
await expect(useCase.execute(request)).rejects.toThrow(
'Failed to save rating: Error: Database connection failed'
);
});
});
});

View File

@@ -0,0 +1,69 @@
/**
* Unit tests for Rating domain entity
*/
import { describe, it, expect } from 'vitest';
import { Rating } from './Rating';
import { DriverId } from '../../racing/domain/entities/DriverId';
import { RaceId } from '../../racing/domain/entities/RaceId';
describe('Rating Entity', () => {
it('should create a rating with correct properties', () => {
// Given
const props = {
driverId: DriverId.create('d1'),
raceId: RaceId.create('r1'),
rating: 1200,
components: {
resultsStrength: 80,
consistency: 70,
cleanDriving: 90,
racecraft: 75,
reliability: 85,
teamContribution: 60,
},
timestamp: new Date('2024-01-01T12:00:00Z'),
};
// When
const rating = Rating.create(props);
// Then
expect(rating.driverId).toBe(props.driverId);
expect(rating.raceId).toBe(props.raceId);
expect(rating.rating).toBe(props.rating);
expect(rating.components).toEqual(props.components);
expect(rating.timestamp).toEqual(props.timestamp);
});
it('should convert to JSON correctly', () => {
// Given
const props = {
driverId: DriverId.create('d1'),
raceId: RaceId.create('r1'),
rating: 1200,
components: {
resultsStrength: 80,
consistency: 70,
cleanDriving: 90,
racecraft: 75,
reliability: 85,
teamContribution: 60,
},
timestamp: new Date('2024-01-01T12:00:00Z'),
};
const rating = Rating.create(props);
// When
const json = rating.toJSON();
// Then
expect(json).toEqual({
driverId: 'd1',
raceId: 'r1',
rating: 1200,
components: props.components,
timestamp: '2024-01-01T12:00:00.000Z',
});
});
});