import { describe, it, expect, vi, beforeEach } from 'vitest'; import { RaceResultsService } from './RaceResultsService'; import { RacesApiClient } from '../../api/races/RacesApiClient'; import { RaceResultsDetailPresenter } from '../../presenters/RaceResultsDetailPresenter'; import { RaceWithSOFPresenter } from '../../presenters/RaceWithSOFPresenter'; import { ImportRaceResultsPresenter } from '../../presenters/ImportRaceResultsPresenter'; import type { RaceResultsDetailDto, RaceWithSOFDto, ImportRaceResultsSummaryDto } from '../../dtos'; import type { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel'; import type { RaceWithSOFViewModel } from '../../presenters/RaceWithSOFPresenter'; import type { ImportRaceResultsSummaryViewModel } from '../../presenters/ImportRaceResultsPresenter'; describe('RaceResultsService', () => { let service: RaceResultsService; let mockApiClient: RacesApiClient; let mockResultsDetailPresenter: RaceResultsDetailPresenter; let mockSOFPresenter: RaceWithSOFPresenter; let mockImportPresenter: ImportRaceResultsPresenter; beforeEach(() => { mockApiClient = { getResultsDetail: vi.fn(), getWithSOF: vi.fn(), importResults: vi.fn(), } as unknown as RacesApiClient; mockResultsDetailPresenter = { present: vi.fn(), } as unknown as RaceResultsDetailPresenter; mockSOFPresenter = { present: vi.fn(), } as unknown as RaceWithSOFPresenter; mockImportPresenter = { present: vi.fn(), } as unknown as ImportRaceResultsPresenter; service = new RaceResultsService( mockApiClient, mockResultsDetailPresenter, mockSOFPresenter, mockImportPresenter ); }); describe('getResultsDetail', () => { it('should fetch race results detail and transform via presenter', async () => { // Arrange const raceId = 'race-123'; const currentUserId = 'user-456'; const mockDto: Partial = { raceId, results: [], }; const mockViewModel: Partial = { raceId, results: [], }; vi.mocked(mockApiClient.getResultsDetail).mockResolvedValue(mockDto as RaceResultsDetailDto); vi.mocked(mockResultsDetailPresenter.present).mockReturnValue(mockViewModel as RaceResultsDetailViewModel); // Act const result = await service.getResultsDetail(raceId, currentUserId); // Assert expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId); expect(mockResultsDetailPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId); expect(result).toEqual(mockViewModel); }); it('should fetch race results detail without currentUserId', async () => { // Arrange const raceId = 'race-123'; const mockDto: Partial = { raceId, results: [], }; const mockViewModel: Partial = { raceId, results: [], }; vi.mocked(mockApiClient.getResultsDetail).mockResolvedValue(mockDto as RaceResultsDetailDto); vi.mocked(mockResultsDetailPresenter.present).mockReturnValue(mockViewModel as RaceResultsDetailViewModel); // Act const result = await service.getResultsDetail(raceId); // Assert expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId); expect(mockResultsDetailPresenter.present).toHaveBeenCalledWith(mockDto, undefined); expect(result).toEqual(mockViewModel); }); it('should propagate errors from API client', async () => { // Arrange const raceId = 'race-123'; const error = new Error('API Error'); vi.mocked(mockApiClient.getResultsDetail).mockRejectedValue(error); // Act & Assert await expect(service.getResultsDetail(raceId)).rejects.toThrow('API Error'); expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId); expect(mockResultsDetailPresenter.present).not.toHaveBeenCalled(); }); }); describe('getWithSOF', () => { it('should fetch race with SOF and transform via presenter', async () => { // Arrange const raceId = 'race-123'; const mockDto: RaceWithSOFDto = { id: raceId, track: 'Spa-Francorchamps', strengthOfField: 2500, }; const mockViewModel: RaceWithSOFViewModel = { id: raceId, track: 'Spa-Francorchamps', strengthOfField: 2500, }; vi.mocked(mockApiClient.getWithSOF).mockResolvedValue(mockDto); vi.mocked(mockSOFPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getWithSOF(raceId); // Assert expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId); expect(mockSOFPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should handle null strengthOfField', async () => { // Arrange const raceId = 'race-123'; const mockDto: RaceWithSOFDto = { id: raceId, track: 'Spa-Francorchamps', strengthOfField: null, }; const mockViewModel: RaceWithSOFViewModel = { id: raceId, track: 'Spa-Francorchamps', strengthOfField: null, }; vi.mocked(mockApiClient.getWithSOF).mockResolvedValue(mockDto); vi.mocked(mockSOFPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getWithSOF(raceId); // Assert expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId); expect(mockSOFPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should propagate errors from API client', async () => { // Arrange const raceId = 'race-123'; const error = new Error('SOF calculation failed'); vi.mocked(mockApiClient.getWithSOF).mockRejectedValue(error); // Act & Assert await expect(service.getWithSOF(raceId)).rejects.toThrow('SOF calculation failed'); expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId); expect(mockSOFPresenter.present).not.toHaveBeenCalled(); }); }); describe('importResults', () => { it('should import race results and transform via presenter', async () => { // Arrange const raceId = 'race-123'; const input = { sessionId: 'session-456', results: [ { position: 1, driverId: 'driver-1', finishTime: 120000 }, { position: 2, driverId: 'driver-2', finishTime: 121000 }, ], }; const mockDto: ImportRaceResultsSummaryDto = { success: true, raceId, driversProcessed: 2, resultsRecorded: 2, }; const mockViewModel: ImportRaceResultsSummaryViewModel = { success: true, raceId, driversProcessed: 2, resultsRecorded: 2, }; vi.mocked(mockApiClient.importResults).mockResolvedValue(mockDto); vi.mocked(mockImportPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.importResults(raceId, input); // Assert expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input); expect(mockImportPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should handle import with errors', async () => { // Arrange const raceId = 'race-123'; const input = { sessionId: 'session-456', results: [] }; const mockDto: ImportRaceResultsSummaryDto = { success: false, raceId, driversProcessed: 5, resultsRecorded: 3, errors: ['Driver not found: driver-99', 'Invalid time for driver-88'], }; const mockViewModel: ImportRaceResultsSummaryViewModel = { success: false, raceId, driversProcessed: 5, resultsRecorded: 3, errors: ['Driver not found: driver-99', 'Invalid time for driver-88'], }; vi.mocked(mockApiClient.importResults).mockResolvedValue(mockDto); vi.mocked(mockImportPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.importResults(raceId, input); // Assert expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input); expect(mockImportPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); expect(result.errors).toHaveLength(2); }); it('should propagate errors from API client', async () => { // Arrange const raceId = 'race-123'; const input = { sessionId: 'session-456', results: [] }; const error = new Error('Import failed'); vi.mocked(mockApiClient.importResults).mockRejectedValue(error); // Act & Assert await expect(service.importResults(raceId, input)).rejects.toThrow('Import failed'); expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input); expect(mockImportPresenter.present).not.toHaveBeenCalled(); }); }); });