Some checks failed
CI / lint-typecheck (pull_request) Failing after 12s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
/**
|
|
* 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 { DriverRepository } from '../../../racing/domain/repositories/DriverRepository';
|
|
import { RaceRepository } from '../../../racing/domain/repositories/RaceRepository';
|
|
import { ResultRepository } from '../../../racing/domain/repositories/ResultRepository';
|
|
import { RatingRepository } from '../../ports/RatingRepository';
|
|
import { EventPublisher } from '../../../shared/ports/EventPublisher';
|
|
|
|
// 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 unknown as DriverRepository,
|
|
raceRepository: mockRaceRepository as unknown as RaceRepository,
|
|
resultRepository: mockResultRepository as unknown as ResultRepository,
|
|
ratingRepository: mockRatingRepository as unknown as RatingRepository,
|
|
eventPublisher: mockEventPublisher as unknown as EventPublisher,
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|