import { describe, it, expect, vi, beforeEach } from 'vitest'; import { DriverService } from './DriverService'; import type { DriversApiClient } from '../../api/drivers/DriversApiClient'; import type { DriversLeaderboardPresenter } from '../../presenters/DriversLeaderboardPresenter'; import type { DriverPresenter } from '../../presenters/DriverPresenter'; import type { CompleteOnboardingPresenter } from '../../presenters/CompleteOnboardingPresenter'; import type { DriversLeaderboardDto, CompleteOnboardingOutputDto, DriverDto, CompleteOnboardingInputDto } from '../../dtos'; import type { DriverLeaderboardViewModel, DriverViewModel, CompleteOnboardingViewModel } from '../../view-models'; describe('DriverService', () => { let service: DriverService; let mockApiClient: DriversApiClient; let mockLeaderboardPresenter: DriversLeaderboardPresenter; let mockDriverPresenter: DriverPresenter; let mockOnboardingPresenter: CompleteOnboardingPresenter; beforeEach(() => { mockApiClient = { getLeaderboard: vi.fn(), completeOnboarding: vi.fn(), getCurrent: vi.fn(), } as unknown as DriversApiClient; mockLeaderboardPresenter = { present: vi.fn(), } as unknown as DriversLeaderboardPresenter; mockDriverPresenter = { present: vi.fn(), } as unknown as DriverPresenter; mockOnboardingPresenter = { present: vi.fn(), } as unknown as CompleteOnboardingPresenter; service = new DriverService( mockApiClient, mockLeaderboardPresenter, mockDriverPresenter, mockOnboardingPresenter ); }); describe('constructor', () => { it('should create instance with injected dependencies', () => { expect(service).toBeInstanceOf(DriverService); }); }); describe('getDriverLeaderboard', () => { it('should fetch leaderboard from API and transform via presenter', async () => { // Arrange const mockDto: DriversLeaderboardDto = { drivers: [ { id: 'driver-1', name: 'John Doe', rating: 2500, races: 50, wins: 10, isActive: true, }, { id: 'driver-2', name: 'Jane Smith', rating: 2300, races: 40, wins: 8, isActive: true, }, ], }; const mockViewModel = { drivers: [ { id: 'driver-1', name: 'John Doe', rating: 2500, races: 50, wins: 10, isActive: true, }, { id: 'driver-2', name: 'Jane Smith', rating: 2300, races: 40, wins: 8, isActive: true, }, ], } as DriverLeaderboardViewModel; vi.mocked(mockApiClient.getLeaderboard).mockResolvedValue(mockDto); vi.mocked(mockLeaderboardPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getDriverLeaderboard(); // Assert expect(mockApiClient.getLeaderboard).toHaveBeenCalled(); expect(mockLeaderboardPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should handle empty leaderboard', async () => { // Arrange const mockDto: DriversLeaderboardDto = { drivers: [], }; const mockViewModel = { drivers: [], } as DriverLeaderboardViewModel; vi.mocked(mockApiClient.getLeaderboard).mockResolvedValue(mockDto); vi.mocked(mockLeaderboardPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getDriverLeaderboard(); // Assert expect(mockApiClient.getLeaderboard).toHaveBeenCalled(); expect(mockLeaderboardPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should propagate errors from API client', async () => { // Arrange const error = new Error('Leaderboard fetch failed'); vi.mocked(mockApiClient.getLeaderboard).mockRejectedValue(error); // Act & Assert await expect(service.getDriverLeaderboard()).rejects.toThrow('Leaderboard fetch failed'); expect(mockApiClient.getLeaderboard).toHaveBeenCalled(); expect(mockLeaderboardPresenter.present).not.toHaveBeenCalled(); }); }); describe('completeDriverOnboarding', () => { it('should complete onboarding and transform via presenter', async () => { // Arrange const input: CompleteOnboardingInputDto = { iracingId: '123456', displayName: 'John Doe', }; const mockDto: CompleteOnboardingOutputDto = { driverId: 'driver-123', success: true, }; const mockViewModel: CompleteOnboardingViewModel = { driverId: 'driver-123', success: true, }; vi.mocked(mockApiClient.completeOnboarding).mockResolvedValue(mockDto); vi.mocked(mockOnboardingPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.completeDriverOnboarding(input); // Assert expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input); expect(mockOnboardingPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should handle onboarding failure', async () => { // Arrange const input: CompleteOnboardingInputDto = { iracingId: '123456', displayName: 'John Doe', }; const mockDto: CompleteOnboardingOutputDto = { driverId: '', success: false, }; const mockViewModel: CompleteOnboardingViewModel = { driverId: '', success: false, }; vi.mocked(mockApiClient.completeOnboarding).mockResolvedValue(mockDto); vi.mocked(mockOnboardingPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.completeDriverOnboarding(input); // Assert expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input); expect(mockOnboardingPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); expect(result.success).toBe(false); }); it('should propagate errors from API client', async () => { // Arrange const input: CompleteOnboardingInputDto = { iracingId: '123456', displayName: 'John Doe', }; const error = new Error('Onboarding failed'); vi.mocked(mockApiClient.completeOnboarding).mockRejectedValue(error); // Act & Assert await expect(service.completeDriverOnboarding(input)).rejects.toThrow('Onboarding failed'); expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input); expect(mockOnboardingPresenter.present).not.toHaveBeenCalled(); }); }); describe('getCurrentDriver', () => { it('should fetch current driver and transform via presenter', async () => { // Arrange const mockDto: DriverDto = { id: 'driver-123', name: 'John Doe', avatarUrl: 'https://example.com/avatar.jpg', iracingId: '123456', rating: 2500, }; const mockViewModel: DriverViewModel = { id: 'driver-123', name: 'John Doe', avatarUrl: 'https://example.com/avatar.jpg', iracingId: '123456', rating: 2500, }; vi.mocked(mockApiClient.getCurrent).mockResolvedValue(mockDto); vi.mocked(mockDriverPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getCurrentDriver(); // Assert expect(mockApiClient.getCurrent).toHaveBeenCalled(); expect(mockDriverPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should return null when no current driver', async () => { // Arrange vi.mocked(mockApiClient.getCurrent).mockResolvedValue(null); // Act const result = await service.getCurrentDriver(); // Assert expect(mockApiClient.getCurrent).toHaveBeenCalled(); expect(mockDriverPresenter.present).not.toHaveBeenCalled(); expect(result).toBeNull(); }); it('should handle driver without optional fields', async () => { // Arrange const mockDto: DriverDto = { id: 'driver-123', name: 'John Doe', }; const mockViewModel: DriverViewModel = { id: 'driver-123', name: 'John Doe', }; vi.mocked(mockApiClient.getCurrent).mockResolvedValue(mockDto); vi.mocked(mockDriverPresenter.present).mockReturnValue(mockViewModel); // Act const result = await service.getCurrentDriver(); // Assert expect(mockApiClient.getCurrent).toHaveBeenCalled(); expect(mockDriverPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should propagate errors from API client', async () => { // Arrange const error = new Error('Failed to fetch current driver'); vi.mocked(mockApiClient.getCurrent).mockRejectedValue(error); // Act & Assert await expect(service.getCurrentDriver()).rejects.toThrow('Failed to fetch current driver'); expect(mockApiClient.getCurrent).toHaveBeenCalled(); expect(mockDriverPresenter.present).not.toHaveBeenCalled(); }); }); });