import { describe, it, expect } from 'vitest'; import { RaceStewardingViewDataBuilder } from './RaceStewardingViewDataBuilder'; describe('RaceStewardingViewDataBuilder', () => { describe('happy paths', () => { it('should transform API DTO to RaceStewardingViewData correctly', () => { const apiDto = { race: { id: 'race-123', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-456', }, pendingProtests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 3', }, filedAt: '2024-01-01T10:00:00Z', status: 'pending', proofVideoUrl: 'video-url', decisionNotes: null, }, ], resolvedProtests: [ { id: 'protest-2', protestingDriverId: 'driver-3', accusedDriverId: 'driver-4', incident: { lap: 10, description: 'Contact at turn 5', }, filedAt: '2024-01-01T10:00:00Z', status: 'resolved', proofVideoUrl: 'video-url', decisionNotes: 'Penalty applied', }, ], penalties: [ { id: 'penalty-1', driverId: 'driver-5', type: 'time_penalty', value: 5, reason: 'Track limits', notes: 'Warning issued', }, ], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, 'driver-3': { id: 'driver-3', name: 'Driver 3' }, 'driver-4': { id: 'driver-4', name: 'Driver 4' }, 'driver-5': { id: 'driver-5', name: 'Driver 5' }, }, pendingCount: 1, resolvedCount: 1, penaltiesCount: 1, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result).toEqual({ race: { id: 'race-123', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-456', }, pendingProtests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 3', }, filedAt: '2024-01-01T10:00:00Z', status: 'pending', proofVideoUrl: 'video-url', decisionNotes: null, }, ], resolvedProtests: [ { id: 'protest-2', protestingDriverId: 'driver-3', accusedDriverId: 'driver-4', incident: { lap: 10, description: 'Contact at turn 5', }, filedAt: '2024-01-01T10:00:00Z', status: 'resolved', proofVideoUrl: 'video-url', decisionNotes: 'Penalty applied', }, ], penalties: [ { id: 'penalty-1', driverId: 'driver-5', type: 'time_penalty', value: 5, reason: 'Track limits', notes: 'Warning issued', }, ], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, 'driver-3': { id: 'driver-3', name: 'Driver 3' }, 'driver-4': { id: 'driver-4', name: 'Driver 4' }, 'driver-5': { id: 'driver-5', name: 'Driver 5' }, }, pendingCount: 1, resolvedCount: 1, penaltiesCount: 1, }); }); it('should handle empty protests and penalties', () => { const apiDto = { race: { id: 'race-456', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-789', }, pendingProtests: [], resolvedProtests: [], penalties: [], driverMap: {}, pendingCount: 0, resolvedCount: 0, penaltiesCount: 0, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.pendingProtests).toHaveLength(0); expect(result.resolvedProtests).toHaveLength(0); expect(result.penalties).toHaveLength(0); expect(result.pendingCount).toBe(0); expect(result.resolvedCount).toBe(0); expect(result.penaltiesCount).toBe(0); }); it('should handle multiple protests and penalties', () => { const apiDto = { race: { id: 'race-789', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-101', }, pendingProtests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 3', }, filedAt: '2024-01-01T10:00:00Z', status: 'pending', proofVideoUrl: 'video-url', decisionNotes: null, }, { id: 'protest-2', protestingDriverId: 'driver-3', accusedDriverId: 'driver-4', incident: { lap: 10, description: 'Contact at turn 5', }, filedAt: '2024-01-01T10:00:00Z', status: 'pending', proofVideoUrl: 'video-url', decisionNotes: null, }, ], resolvedProtests: [ { id: 'protest-3', protestingDriverId: 'driver-5', accusedDriverId: 'driver-6', incident: { lap: 15, description: 'Contact at turn 7', }, filedAt: '2024-01-01T10:00:00Z', status: 'resolved', proofVideoUrl: 'video-url', decisionNotes: 'Penalty applied', }, ], penalties: [ { id: 'penalty-1', driverId: 'driver-7', type: 'time_penalty', value: 5, reason: 'Track limits', notes: 'Warning issued', }, { id: 'penalty-2', driverId: 'driver-8', type: 'grid_penalty', value: 3, reason: 'Qualifying infringement', notes: null, }, ], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, 'driver-3': { id: 'driver-3', name: 'Driver 3' }, 'driver-4': { id: 'driver-4', name: 'Driver 4' }, 'driver-5': { id: 'driver-5', name: 'Driver 5' }, 'driver-6': { id: 'driver-6', name: 'Driver 6' }, 'driver-7': { id: 'driver-7', name: 'Driver 7' }, 'driver-8': { id: 'driver-8', name: 'Driver 8' }, }, pendingCount: 2, resolvedCount: 1, penaltiesCount: 2, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.pendingProtests).toHaveLength(2); expect(result.resolvedProtests).toHaveLength(1); expect(result.penalties).toHaveLength(2); }); }); describe('data transformation', () => { it('should preserve all DTO fields in the output', () => { const apiDto = { race: { id: 'race-102', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-103', }, pendingProtests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 3', }, filedAt: '2024-01-01T10:00:00Z', status: 'pending', proofVideoUrl: 'video-url', decisionNotes: null, }, ], resolvedProtests: [], penalties: [], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, }, pendingCount: 1, resolvedCount: 0, penaltiesCount: 0, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.race?.id).toBe(apiDto.race.id); expect(result.race?.track).toBe(apiDto.race.track); expect(result.race?.scheduledAt).toBe(apiDto.race.scheduledAt); expect(result.league?.id).toBe(apiDto.league.id); expect(result.pendingCount).toBe(apiDto.pendingCount); expect(result.resolvedCount).toBe(apiDto.resolvedCount); expect(result.penaltiesCount).toBe(apiDto.penaltiesCount); }); it('should not modify the input DTO', () => { const apiDto = { race: { id: 'race-104', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-105', }, pendingProtests: [], resolvedProtests: [], penalties: [], driverMap: {}, pendingCount: 0, resolvedCount: 0, penaltiesCount: 0, }; const originalDto = { ...apiDto }; RaceStewardingViewDataBuilder.build(apiDto); expect(apiDto).toEqual(originalDto); }); }); describe('edge cases', () => { it('should handle null API DTO', () => { const result = RaceStewardingViewDataBuilder.build(null); expect(result.race).toBeNull(); expect(result.league).toBeNull(); expect(result.pendingProtests).toHaveLength(0); expect(result.resolvedProtests).toHaveLength(0); expect(result.penalties).toHaveLength(0); expect(result.driverMap).toEqual({}); expect(result.pendingCount).toBe(0); expect(result.resolvedCount).toBe(0); expect(result.penaltiesCount).toBe(0); }); it('should handle undefined API DTO', () => { const result = RaceStewardingViewDataBuilder.build(undefined); expect(result.race).toBeNull(); expect(result.league).toBeNull(); expect(result.pendingProtests).toHaveLength(0); expect(result.resolvedProtests).toHaveLength(0); expect(result.penalties).toHaveLength(0); expect(result.driverMap).toEqual({}); expect(result.pendingCount).toBe(0); expect(result.resolvedCount).toBe(0); expect(result.penaltiesCount).toBe(0); }); it('should handle race without league', () => { const apiDto = { race: { id: 'race-106', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, pendingProtests: [], resolvedProtests: [], penalties: [], driverMap: {}, pendingCount: 0, resolvedCount: 0, penaltiesCount: 0, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.league).toBeNull(); }); it('should handle protests without proof video', () => { const apiDto = { race: { id: 'race-107', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-108', }, pendingProtests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 3', }, filedAt: '2024-01-01T10:00:00Z', status: 'pending', proofVideoUrl: null, decisionNotes: null, }, ], resolvedProtests: [], penalties: [], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, }, pendingCount: 1, resolvedCount: 0, penaltiesCount: 0, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.pendingProtests[0].proofVideoUrl).toBeNull(); }); it('should handle protests without decision notes', () => { const apiDto = { race: { id: 'race-109', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-110', }, pendingProtests: [], resolvedProtests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 3', }, filedAt: '2024-01-01T10:00:00Z', status: 'resolved', proofVideoUrl: 'video-url', decisionNotes: null, }, ], penalties: [], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, }, pendingCount: 0, resolvedCount: 1, penaltiesCount: 0, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.resolvedProtests[0].decisionNotes).toBeNull(); }); it('should handle penalties without notes', () => { const apiDto = { race: { id: 'race-111', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-112', }, pendingProtests: [], resolvedProtests: [], penalties: [ { id: 'penalty-1', driverId: 'driver-1', type: 'time_penalty', value: 5, reason: 'Track limits', notes: null, }, ], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, }, pendingCount: 0, resolvedCount: 0, penaltiesCount: 1, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.penalties[0].notes).toBeNull(); }); it('should handle penalties without value', () => { const apiDto = { race: { id: 'race-113', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-114', }, pendingProtests: [], resolvedProtests: [], penalties: [ { id: 'penalty-1', driverId: 'driver-1', type: 'disqualification', value: null, reason: 'Technical infringement', notes: null, }, ], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, }, pendingCount: 0, resolvedCount: 0, penaltiesCount: 1, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.penalties[0].value).toBe(0); }); it('should handle penalties without reason', () => { const apiDto = { race: { id: 'race-115', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-116', }, pendingProtests: [], resolvedProtests: [], penalties: [ { id: 'penalty-1', driverId: 'driver-1', type: 'warning', value: 0, reason: null, notes: null, }, ], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, }, pendingCount: 0, resolvedCount: 0, penaltiesCount: 1, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.penalties[0].reason).toBe(''); }); it('should handle different protest statuses', () => { const apiDto = { race: { id: 'race-117', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-118', }, pendingProtests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 3', }, filedAt: '2024-01-01T10:00:00Z', status: 'pending', proofVideoUrl: 'video-url', decisionNotes: null, }, ], resolvedProtests: [ { id: 'protest-2', protestingDriverId: 'driver-3', accusedDriverId: 'driver-4', incident: { lap: 10, description: 'Contact at turn 5', }, filedAt: '2024-01-01T10:00:00Z', status: 'resolved', proofVideoUrl: 'video-url', decisionNotes: 'Penalty applied', }, { id: 'protest-3', protestingDriverId: 'driver-5', accusedDriverId: 'driver-6', incident: { lap: 15, description: 'Contact at turn 7', }, filedAt: '2024-01-01T10:00:00Z', status: 'rejected', proofVideoUrl: 'video-url', decisionNotes: 'Insufficient evidence', }, ], penalties: [], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, 'driver-3': { id: 'driver-3', name: 'Driver 3' }, 'driver-4': { id: 'driver-4', name: 'Driver 4' }, 'driver-5': { id: 'driver-5', name: 'Driver 5' }, 'driver-6': { id: 'driver-6', name: 'Driver 6' }, }, pendingCount: 1, resolvedCount: 2, penaltiesCount: 0, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.pendingProtests[0].status).toBe('pending'); expect(result.resolvedProtests[0].status).toBe('resolved'); expect(result.resolvedProtests[1].status).toBe('rejected'); }); it('should handle different penalty types', () => { const apiDto = { race: { id: 'race-119', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-120', }, pendingProtests: [], resolvedProtests: [], penalties: [ { id: 'penalty-1', driverId: 'driver-1', type: 'time_penalty', value: 5, reason: 'Track limits', notes: 'Warning issued', }, { id: 'penalty-2', driverId: 'driver-2', type: 'grid_penalty', value: 3, reason: 'Qualifying infringement', notes: null, }, { id: 'penalty-3', driverId: 'driver-3', type: 'points_deduction', value: 10, reason: 'Dangerous driving', notes: null, }, { id: 'penalty-4', driverId: 'driver-4', type: 'disqualification', value: 0, reason: 'Technical infringement', notes: null, }, { id: 'penalty-5', driverId: 'driver-5', type: 'warning', value: 0, reason: 'Minor infraction', notes: null, }, { id: 'penalty-6', driverId: 'driver-6', type: 'license_points', value: 2, reason: 'Multiple incidents', notes: null, }, ], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, 'driver-3': { id: 'driver-3', name: 'Driver 3' }, 'driver-4': { id: 'driver-4', name: 'Driver 4' }, 'driver-5': { id: 'driver-5', name: 'Driver 5' }, 'driver-6': { id: 'driver-6', name: 'Driver 6' }, }, pendingCount: 0, resolvedCount: 0, penaltiesCount: 6, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.penalties[0].type).toBe('time_penalty'); expect(result.penalties[1].type).toBe('grid_penalty'); expect(result.penalties[2].type).toBe('points_deduction'); expect(result.penalties[3].type).toBe('disqualification'); expect(result.penalties[4].type).toBe('warning'); expect(result.penalties[5].type).toBe('license_points'); }); it('should handle empty driver map', () => { const apiDto = { race: { id: 'race-121', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-122', }, pendingProtests: [], resolvedProtests: [], penalties: [], driverMap: {}, pendingCount: 0, resolvedCount: 0, penaltiesCount: 0, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.driverMap).toEqual({}); }); it('should handle count values from DTO', () => { const apiDto = { race: { id: 'race-123', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-124', }, pendingProtests: [], resolvedProtests: [], penalties: [], driverMap: {}, pendingCount: 5, resolvedCount: 10, penaltiesCount: 3, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.pendingCount).toBe(5); expect(result.resolvedCount).toBe(10); expect(result.penaltiesCount).toBe(3); }); it('should calculate counts from arrays when not provided', () => { const apiDto = { race: { id: 'race-125', track: 'Test Track', scheduledAt: '2024-01-01T10:00:00Z', }, league: { id: 'league-126', }, pendingProtests: [ { id: 'protest-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 5, description: 'Contact at turn 3', }, filedAt: '2024-01-01T10:00:00Z', status: 'pending', proofVideoUrl: 'video-url', decisionNotes: null, }, ], resolvedProtests: [ { id: 'protest-2', protestingDriverId: 'driver-3', accusedDriverId: 'driver-4', incident: { lap: 10, description: 'Contact at turn 5', }, filedAt: '2024-01-01T10:00:00Z', status: 'resolved', proofVideoUrl: 'video-url', decisionNotes: 'Penalty applied', }, ], penalties: [ { id: 'penalty-1', driverId: 'driver-5', type: 'time_penalty', value: 5, reason: 'Track limits', notes: 'Warning issued', }, ], driverMap: { 'driver-1': { id: 'driver-1', name: 'Driver 1' }, 'driver-2': { id: 'driver-2', name: 'Driver 2' }, 'driver-3': { id: 'driver-3', name: 'Driver 3' }, 'driver-4': { id: 'driver-4', name: 'Driver 4' }, 'driver-5': { id: 'driver-5', name: 'Driver 5' }, }, }; const result = RaceStewardingViewDataBuilder.build(apiDto); expect(result.pendingCount).toBe(1); expect(result.resolvedCount).toBe(1); expect(result.penaltiesCount).toBe(1); }); }); });