import { describe, it, expect, vi, beforeEach } from 'vitest'; import { RaceService } from './RaceService'; import { RacesApiClient } from '../../api/races/RacesApiClient'; import { RaceDetailPresenter } from '../../presenters/RaceDetailPresenter'; import type { RaceDetailDto, RacesPageDataDto, RaceStatsDto } from '../../dtos'; import type { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel'; describe('RaceService', () => { let mockApiClient: RacesApiClient; let mockPresenter: RaceDetailPresenter; let service: RaceService; beforeEach(() => { mockApiClient = { getDetail: vi.fn(), getPageData: vi.fn(), getTotal: vi.fn(), } as unknown as RacesApiClient; mockPresenter = { present: vi.fn(), } as unknown as RaceDetailPresenter; service = new RaceService(mockApiClient, mockPresenter); }); describe('getRaceDetail', () => { it('should fetch race detail from API and transform via presenter', async () => { // Arrange const mockDto: RaceDetailDto = { race: { id: 'race-1', name: 'Test Race', scheduledTime: '2025-12-17T20:00:00Z', status: 'upcoming', trackName: 'Spa-Francorchamps', carClasses: ['GT3'], }, league: null, entryList: [], registration: { isRegistered: false, canRegister: true, }, userResult: null, }; const mockViewModel: RaceDetailViewModel = { race: mockDto.race, league: mockDto.league, entryList: mockDto.entryList, registration: mockDto.registration, userResult: mockDto.userResult, isRegistered: false, canRegister: true, raceStatusDisplay: 'Upcoming', formattedScheduledTime: expect.any(String), entryCount: 0, hasResults: false, registrationStatusMessage: 'You can register for this race', } as unknown as RaceDetailViewModel; vi.mocked(mockApiClient.getDetail).mockResolvedValue(mockDto); vi.mocked(mockPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getRaceDetail('race-1', 'driver-1'); // Assert expect(mockApiClient.getDetail).toHaveBeenCalledWith('race-1', 'driver-1'); expect(mockApiClient.getDetail).toHaveBeenCalledTimes(1); expect(mockPresenter.present).toHaveBeenCalledWith(mockDto); expect(mockPresenter.present).toHaveBeenCalledTimes(1); expect(result).toBe(mockViewModel); }); it('should propagate API client errors', async () => { // Arrange const error = new Error('API Error: Race not found'); vi.mocked(mockApiClient.getDetail).mockRejectedValue(error); // Act & Assert await expect( service.getRaceDetail('invalid-race', 'driver-1') ).rejects.toThrow('API Error: Race not found'); expect(mockApiClient.getDetail).toHaveBeenCalledWith('invalid-race', 'driver-1'); expect(mockPresenter.present).not.toHaveBeenCalled(); }); it('should propagate presenter errors', async () => { // Arrange const mockDto: RaceDetailDto = { race: null, league: null, entryList: [], registration: { isRegistered: false, canRegister: false, }, userResult: null, }; const error = new Error('Presenter Error: Invalid DTO structure'); vi.mocked(mockApiClient.getDetail).mockResolvedValue(mockDto); vi.mocked(mockPresenter.present).mockImplementation(() => { throw error; }); // Act & Assert await expect( service.getRaceDetail('race-1', 'driver-1') ).rejects.toThrow('Presenter Error: Invalid DTO structure'); expect(mockApiClient.getDetail).toHaveBeenCalledWith('race-1', 'driver-1'); expect(mockPresenter.present).toHaveBeenCalledWith(mockDto); }); }); describe('getRacesPageData', () => { it('should fetch races page data from API', async () => { // Arrange const mockPageData: RacesPageDataDto = { races: [ { id: 'race-1', name: 'Test Race 1', scheduledTime: '2025-12-17T20:00:00Z', trackName: 'Spa-Francorchamps', }, { id: 'race-2', name: 'Test Race 2', scheduledTime: '2025-12-18T20:00:00Z', trackName: 'Monza', }, ], totalCount: 2, }; vi.mocked(mockApiClient.getPageData).mockResolvedValue(mockPageData); // Act const result = await service.getRacesPageData(); // Assert expect(mockApiClient.getPageData).toHaveBeenCalledTimes(1); expect(result).toBe(mockPageData); }); it('should propagate API client errors', async () => { // Arrange const error = new Error('API Error: Failed to fetch page data'); vi.mocked(mockApiClient.getPageData).mockRejectedValue(error); // Act & Assert await expect(service.getRacesPageData()).rejects.toThrow( 'API Error: Failed to fetch page data' ); expect(mockApiClient.getPageData).toHaveBeenCalledTimes(1); }); }); describe('getRacesTotal', () => { it('should fetch race statistics from API', async () => { // Arrange const mockStats: RaceStatsDto = { total: 42, upcoming: 10, live: 2, finished: 30, }; vi.mocked(mockApiClient.getTotal).mockResolvedValue(mockStats); // Act const result = await service.getRacesTotal(); // Assert expect(mockApiClient.getTotal).toHaveBeenCalledTimes(1); expect(result).toBe(mockStats); }); it('should propagate API client errors', async () => { // Arrange const error = new Error('API Error: Failed to fetch statistics'); vi.mocked(mockApiClient.getTotal).mockRejectedValue(error); // Act & Assert await expect(service.getRacesTotal()).rejects.toThrow( 'API Error: Failed to fetch statistics' ); expect(mockApiClient.getTotal).toHaveBeenCalledTimes(1); }); }); describe('Constructor Dependency Injection', () => { it('should require apiClient and raceDetailPresenter', () => { // This test verifies the constructor signature expect(() => { new RaceService(mockApiClient, mockPresenter); }).not.toThrow(); }); it('should use injected dependencies', async () => { // Arrange const customApiClient = { getDetail: vi.fn().mockResolvedValue({ race: null, league: null, entryList: [], registration: { isRegistered: false, canRegister: false }, userResult: null, }), getPageData: vi.fn(), getTotal: vi.fn(), } as unknown as RacesApiClient; const customPresenter = { present: vi.fn().mockReturnValue({} as RaceDetailViewModel), } as unknown as RaceDetailPresenter; const customService = new RaceService(customApiClient, customPresenter); // Act await customService.getRaceDetail('race-1', 'driver-1'); // Assert expect(customApiClient.getDetail).toHaveBeenCalledWith('race-1', 'driver-1'); expect(customPresenter.present).toHaveBeenCalled(); }); }); });