import { describe, it, expect, vi, Mocked } from 'vitest'; import { ProtestService } from './ProtestService'; import { ProtestsApiClient } from '../../api/protests/ProtestsApiClient'; import { ProtestViewModel } from '../../view-models/ProtestViewModel'; import { RaceViewModel } from '../../view-models/RaceViewModel'; import { ProtestDriverViewModel } from '../../view-models/ProtestDriverViewModel'; describe('ProtestService', () => { let mockApiClient: Mocked; let service: ProtestService; beforeEach(() => { mockApiClient = { getLeagueProtests: vi.fn(), getLeagueProtest: vi.fn(), applyPenalty: vi.fn(), requestDefense: vi.fn(), reviewProtest: vi.fn(), getRaceProtests: vi.fn(), } as unknown as Mocked; service = new ProtestService(mockApiClient); }); describe('getLeagueProtests', () => { it('should call apiClient.getLeagueProtests and return transformed data', async () => { const leagueId = 'league-123'; const mockDto = { protests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 1' }, filedAt: '2023-10-01T10:00:00Z', status: 'pending', }, ], racesById: { 'race-1': { id: 'race-1', track: 'Monza' } }, driversById: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, }, }; mockApiClient.getLeagueProtests.mockResolvedValue(mockDto as any); const result = await service.getLeagueProtests(leagueId); expect(mockApiClient.getLeagueProtests).toHaveBeenCalledWith(leagueId); expect(result.protests).toHaveLength(1); expect(result.protests[0]).toBeInstanceOf(ProtestViewModel); expect(result.racesById).toEqual(mockDto.racesById); expect(result.driversById).toEqual(mockDto.driversById); }); it('should throw error when apiClient.getLeagueProtests fails', async () => { const leagueId = 'league-123'; const error = new Error('API call failed'); mockApiClient.getLeagueProtests.mockRejectedValue(error); await expect(service.getLeagueProtests(leagueId)).rejects.toThrow('API call failed'); }); }); describe('getProtestById', () => { it('should call apiClient.getLeagueProtest and return transformed data', async () => { const leagueId = 'league-123'; const protestId = 'protest-1'; const mockDto = { protests: [ { id: protestId, protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 1' }, filedAt: '2023-10-01T10:00:00Z', status: 'pending', }, ], racesById: { 'race-1': { id: 'race-1', track: 'Monza', scheduledAt: '2023-10-01T10:00:00Z' } }, driversById: { 'driver-1': { id: 'driver-1', name: 'Driver 1', avatarUrl: 'https://example.com/avatar1.jpg' }, 'driver-2': { id: 'driver-2', name: 'Driver 2', avatarUrl: 'https://example.com/avatar2.jpg' }, }, }; mockApiClient.getLeagueProtest.mockResolvedValue(mockDto as any); const result = await service.getProtestById(leagueId, protestId); expect(mockApiClient.getLeagueProtest).toHaveBeenCalledWith(leagueId, protestId); expect(result).not.toBeNull(); expect(result?.protest).toBeInstanceOf(ProtestViewModel); expect(result?.race).toBeInstanceOf(RaceViewModel); expect(result?.protestingDriver).toBeInstanceOf(ProtestDriverViewModel); expect(result?.accusedDriver).toBeInstanceOf(ProtestDriverViewModel); }); it('should return null when protest not found', async () => { const leagueId = 'league-123'; const protestId = 'protest-999'; const mockDto = { protests: [], racesById: {}, driversById: {}, }; mockApiClient.getLeagueProtest.mockResolvedValue(mockDto); const result = await service.getProtestById(leagueId, protestId); expect(result).toBeNull(); }); it('should throw error when apiClient.getLeagueProtest fails', async () => { const leagueId = 'league-123'; const protestId = 'protest-1'; const error = new Error('API call failed'); mockApiClient.getLeagueProtest.mockRejectedValue(error); await expect(service.getProtestById(leagueId, protestId)).rejects.toThrow('API call failed'); }); }); describe('applyPenalty', () => { it('should call apiClient.applyPenalty', async () => { const input = { protestId: 'protest-123', driverId: 'driver-456', type: 'time', value: 10, reason: 'Test reason', }; mockApiClient.applyPenalty.mockResolvedValue(undefined); await service.applyPenalty(input as any); expect(mockApiClient.applyPenalty).toHaveBeenCalledWith(input); }); }); describe('requestDefense', () => { it('should call apiClient.requestDefense', async () => { const input = { protestId: 'protest-123', driverId: 'driver-456', message: 'Please provide defense', }; mockApiClient.requestDefense.mockResolvedValue(undefined); await service.requestDefense(input as any); expect(mockApiClient.requestDefense).toHaveBeenCalledWith(input); }); }); describe('reviewProtest', () => { it('should call apiClient.reviewProtest', async () => { const input = { protestId: 'protest-123', stewardId: 'steward-456', decision: 'upheld', decisionNotes: 'Test notes', }; mockApiClient.reviewProtest.mockResolvedValue(undefined); await service.reviewProtest(input); expect(mockApiClient.reviewProtest).toHaveBeenCalledWith({ ...input, enum: 'uphold', }); }); }); describe('findByRaceId', () => { it('should call apiClient.getRaceProtests and return protests array', async () => { const raceId = 'race-123'; const mockDto = { protests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', status: 'pending' }, { id: 'protest-2', protestingDriverId: 'driver-3', accusedDriverId: 'driver-4', status: 'resolved' }, ], driverMap: {}, }; mockApiClient.getRaceProtests.mockResolvedValue(mockDto as any); const result = await service.findByRaceId(raceId); expect(mockApiClient.getRaceProtests).toHaveBeenCalledWith(raceId); expect(result).toEqual(mockDto.protests); expect(result).toHaveLength(2); }); it('should throw error when apiClient.getRaceProtests fails', async () => { const raceId = 'race-123'; const error = new Error('API call failed'); mockApiClient.getRaceProtests.mockRejectedValue(error); await expect(service.findByRaceId(raceId)).rejects.toThrow('API call failed'); }); }); });