/** * 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(); }); }); });