import { describe, it, expect, vi, Mocked } from 'vitest'; import { LeagueService } from './LeagueService'; import { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient'; import { LeagueSummaryViewModel, LeagueStandingsViewModel, LeagueStatsViewModel, LeagueScheduleViewModel, LeagueMembershipsViewModel, CreateLeagueViewModel, RemoveMemberViewModel, LeagueMemberViewModel } from '../../view-models'; import type { LeagueWithCapacityDTO, CreateLeagueInputDTO, CreateLeagueOutputDTO, RemoveLeagueMemberOutputDTO, LeagueMemberDTO } from '../../types/generated'; describe('LeagueService', () => { let mockApiClient: Mocked; let service: LeagueService; beforeEach(() => { mockApiClient = { getAllWithCapacity: vi.fn(), getStandings: vi.fn(), getTotal: vi.fn(), getSchedule: vi.fn(), getMemberships: vi.fn(), create: vi.fn(), removeMember: vi.fn(), } as Mocked; service = new LeagueService(mockApiClient); }); describe('getAllLeagues', () => { it('should call apiClient.getAllWithCapacity and return array of LeagueSummaryViewModel', async () => { const mockDto = { leagues: [ { id: 'league-1', name: 'League One' }, { id: 'league-2', name: 'League Two' }, ] as LeagueWithCapacityDTO[], }; mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto); const result = await service.getAllLeagues(); expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled(); expect(result).toHaveLength(2); expect(result[0]).toBeInstanceOf(LeagueSummaryViewModel); expect(result[0].id).toBe('league-1'); expect(result[1]).toBeInstanceOf(LeagueSummaryViewModel); expect(result[1].id).toBe('league-2'); }); it('should handle empty leagues array', async () => { const mockDto = { leagues: [] as LeagueWithCapacityDTO[], }; mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto); const result = await service.getAllLeagues(); expect(result).toHaveLength(0); }); it('should throw error when apiClient.getAllWithCapacity fails', async () => { const error = new Error('API call failed'); mockApiClient.getAllWithCapacity.mockRejectedValue(error); await expect(service.getAllLeagues()).rejects.toThrow('API call failed'); }); }); describe('getLeagueStandings', () => { it('should call apiClient.getStandings and return LeagueStandingsViewModel', async () => { const leagueId = 'league-123'; const currentUserId = 'user-456'; const mockDto = { id: leagueId, name: 'Test League', }; mockApiClient.getStandings.mockResolvedValue(mockDto); const result = await service.getLeagueStandings(leagueId, currentUserId); expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId); expect(result).toBeInstanceOf(LeagueStandingsViewModel); }); it('should throw error when apiClient.getStandings fails', async () => { const leagueId = 'league-123'; const currentUserId = 'user-456'; const error = new Error('API call failed'); mockApiClient.getStandings.mockRejectedValue(error); await expect(service.getLeagueStandings(leagueId, currentUserId)).rejects.toThrow('API call failed'); }); }); describe('getLeagueStats', () => { it('should call apiClient.getTotal and return LeagueStatsViewModel', async () => { const mockDto = { totalLeagues: 42 }; mockApiClient.getTotal.mockResolvedValue(mockDto); const result = await service.getLeagueStats(); expect(mockApiClient.getTotal).toHaveBeenCalled(); expect(result).toBeInstanceOf(LeagueStatsViewModel); expect(result.totalLeagues).toBe(42); }); it('should throw error when apiClient.getTotal fails', async () => { const error = new Error('API call failed'); mockApiClient.getTotal.mockRejectedValue(error); await expect(service.getLeagueStats()).rejects.toThrow('API call failed'); }); }); describe('getLeagueSchedule', () => { it('should call apiClient.getSchedule and return LeagueScheduleViewModel', async () => { const leagueId = 'league-123'; const mockDto = { races: [{ id: 'race-1' }, { id: 'race-2' }] }; mockApiClient.getSchedule.mockResolvedValue(mockDto); const result = await service.getLeagueSchedule(leagueId); expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId); expect(result).toBeInstanceOf(LeagueScheduleViewModel); expect(result.races).toEqual(mockDto.races); }); it('should handle empty races array', async () => { const leagueId = 'league-123'; const mockDto = { races: [] }; mockApiClient.getSchedule.mockResolvedValue(mockDto); const result = await service.getLeagueSchedule(leagueId); expect(result.races).toEqual([]); expect(result.hasRaces).toBe(false); }); it('should throw error when apiClient.getSchedule fails', async () => { const leagueId = 'league-123'; const error = new Error('API call failed'); mockApiClient.getSchedule.mockRejectedValue(error); await expect(service.getLeagueSchedule(leagueId)).rejects.toThrow('API call failed'); }); }); describe('getLeagueMemberships', () => { it('should call apiClient.getMemberships and return LeagueMembershipsViewModel', async () => { const leagueId = 'league-123'; const currentUserId = 'user-456'; const mockDto = { memberships: [ { driverId: 'driver-1' }, { driverId: 'driver-2' }, ] as LeagueMemberDTO[], }; mockApiClient.getMemberships.mockResolvedValue(mockDto); const result = await service.getLeagueMemberships(leagueId, currentUserId); expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId); expect(result).toBeInstanceOf(LeagueMembershipsViewModel); expect(result.memberships).toHaveLength(2); expect(result.memberships[0]).toBeInstanceOf(LeagueMemberViewModel); expect(result.memberships[0].driverId).toBe('driver-1'); }); it('should handle empty memberships array', async () => { const leagueId = 'league-123'; const currentUserId = 'user-456'; const mockDto = { memberships: [] as LeagueMemberDTO[], }; mockApiClient.getMemberships.mockResolvedValue(mockDto); const result = await service.getLeagueMemberships(leagueId, currentUserId); expect(result.memberships).toHaveLength(0); expect(result.hasMembers).toBe(false); }); it('should throw error when apiClient.getMemberships fails', async () => { const leagueId = 'league-123'; const currentUserId = 'user-456'; const error = new Error('API call failed'); mockApiClient.getMemberships.mockRejectedValue(error); await expect(service.getLeagueMemberships(leagueId, currentUserId)).rejects.toThrow('API call failed'); }); }); describe('createLeague', () => { it('should call apiClient.create', async () => { const input: CreateLeagueInputDTO = { name: 'New League', description: 'A new league', }; const mockDto: CreateLeagueOutputDTO = { leagueId: 'new-league-id', success: true, }; mockApiClient.create.mockResolvedValue(mockDto); await service.createLeague(input); expect(mockApiClient.create).toHaveBeenCalledWith(input); }); it('should throw error when apiClient.create fails', async () => { const input: CreateLeagueInputDTO = { name: 'New League', description: 'A new league', }; const error = new Error('API call failed'); mockApiClient.create.mockRejectedValue(error); await expect(service.createLeague(input)).rejects.toThrow('API call failed'); }); it('should not call apiClient.create when submitBlocker is blocked', async () => { const input: CreateLeagueInputDTO = { name: 'New League', description: 'A new league', }; // First call should succeed const mockDto: CreateLeagueOutputDTO = { leagueId: 'new-league-id', success: true, }; mockApiClient.create.mockResolvedValue(mockDto); await service.createLeague(input); // This should block the submitBlocker // Reset mock to check calls mockApiClient.create.mockClear(); // Second call should not call API await service.createLeague(input); expect(mockApiClient.create).not.toHaveBeenCalled(); }); it('should not call apiClient.create when throttle is active', async () => { const input: CreateLeagueInputDTO = { name: 'New League', description: 'A new league', }; // First call const mockDto: CreateLeagueOutputDTO = { leagueId: 'new-league-id', success: true, }; mockApiClient.create.mockResolvedValue(mockDto); await service.createLeague(input); // This blocks throttle for 500ms // Reset mock mockApiClient.create.mockClear(); // Immediate second call should not call API due to throttle await service.createLeague(input); expect(mockApiClient.create).not.toHaveBeenCalled(); }); }); describe('removeMember', () => { it('should call apiClient.removeMember and return RemoveMemberViewModel', async () => { const leagueId = 'league-123'; const performerDriverId = 'performer-456'; const targetDriverId = 'target-789'; const mockDto: RemoveLeagueMemberOutputDTO = { success: true }; mockApiClient.removeMember.mockResolvedValue(mockDto); const result = await service.removeMember(leagueId, performerDriverId, targetDriverId); expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId); expect(result).toBeInstanceOf(RemoveMemberViewModel); expect(result.success).toBe(true); }); it('should handle unsuccessful removal', async () => { const leagueId = 'league-123'; const performerDriverId = 'performer-456'; const targetDriverId = 'target-789'; const mockDto: RemoveLeagueMemberOutputDTO = { success: false }; mockApiClient.removeMember.mockResolvedValue(mockDto); const result = await service.removeMember(leagueId, performerDriverId, targetDriverId); expect(result.success).toBe(false); expect(result.successMessage).toBe('Failed to remove member.'); }); it('should throw error when apiClient.removeMember fails', async () => { const leagueId = 'league-123'; const performerDriverId = 'performer-456'; const targetDriverId = 'target-789'; const error = new Error('API call failed'); mockApiClient.removeMember.mockRejectedValue(error); await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('API call failed'); }); }); });