import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { JoinLeagueUseCase } from './JoinLeagueUseCase'; import type { JoinLeagueCommand } from '../ports/JoinLeagueCommand'; import { LeagueRepository } from '../ports/LeagueRepository'; import { DriverRepository } from '../../../racing/domain/repositories/DriverRepository'; import { EventPublisher } from '../../../shared/ports/EventPublisher'; const mockLeagueRepository: LeagueRepository = { create: vi.fn() as unknown as Mock, findById: vi.fn() as unknown as Mock, findByName: vi.fn() as unknown as Mock, findByOwner: vi.fn() as unknown as Mock, search: vi.fn() as unknown as Mock, update: vi.fn() as unknown as Mock, delete: vi.fn() as unknown as Mock, getStats: vi.fn() as unknown as Mock, updateStats: vi.fn() as unknown as Mock, getFinancials: vi.fn() as unknown as Mock, updateFinancials: vi.fn() as unknown as Mock, getStewardingMetrics: vi.fn() as unknown as Mock, updateStewardingMetrics: vi.fn() as unknown as Mock, getPerformanceMetrics: vi.fn() as unknown as Mock, updatePerformanceMetrics: vi.fn() as unknown as Mock, getRatingMetrics: vi.fn() as unknown as Mock, updateRatingMetrics: vi.fn() as unknown as Mock, getTrendMetrics: vi.fn() as unknown as Mock, updateTrendMetrics: vi.fn() as unknown as Mock, getSuccessRateMetrics: vi.fn() as unknown as Mock, updateSuccessRateMetrics: vi.fn() as unknown as Mock, getResolutionTimeMetrics: vi.fn() as unknown as Mock, updateResolutionTimeMetrics: vi.fn() as unknown as Mock, getComplexSuccessRateMetrics: vi.fn() as unknown as Mock, updateComplexSuccessRateMetrics: vi.fn() as unknown as Mock, getComplexResolutionTimeMetrics: vi.fn() as unknown as Mock, updateComplexResolutionTimeMetrics: vi.fn() as unknown as Mock, getLeagueMembers: vi.fn() as unknown as Mock, getPendingRequests: vi.fn() as unknown as Mock, addLeagueMembers: vi.fn() as unknown as Mock, updateLeagueMember: vi.fn() as unknown as Mock, removeLeagueMember: vi.fn() as unknown as Mock, addPendingRequests: vi.fn() as unknown as Mock, removePendingRequest: vi.fn() as unknown as Mock, }; const mockDriverRepository: DriverRepository = { findById: vi.fn() as unknown as Mock, findByIRacingId: vi.fn() as unknown as Mock, findAll: vi.fn() as unknown as Mock, create: vi.fn() as unknown as Mock, update: vi.fn() as unknown as Mock, delete: vi.fn() as unknown as Mock, exists: vi.fn() as unknown as Mock, existsByIRacingId: vi.fn() as unknown as Mock, }; const mockEventPublisher: EventPublisher = { publish: vi.fn() as unknown as Mock, }; describe('JoinLeagueUseCase', () => { let useCase: JoinLeagueUseCase; beforeEach(() => { // Reset mocks vi.clearAllMocks(); useCase = new JoinLeagueUseCase( mockLeagueRepository, mockDriverRepository, mockEventPublisher ); }); describe('Scenario 1: League missing', () => { it('should throw "League not found" when league does not exist', async () => { // Given const command: JoinLeagueCommand = { leagueId: 'league-123', driverId: 'driver-456', }; (mockLeagueRepository.findById as Mock).mockImplementation(() => Promise.resolve(null)); // When & Then await expect(useCase.execute(command)).rejects.toThrow('League not found'); expect(mockLeagueRepository.findById).toHaveBeenCalledWith('league-123'); }); }); describe('Scenario 2: Driver missing', () => { it('should throw "Driver not found" when driver does not exist', async () => { // Given const command: JoinLeagueCommand = { leagueId: 'league-123', driverId: 'driver-456', }; const mockLeague = { id: 'league-123', name: 'Test League', description: null, visibility: 'public' as const, ownerId: 'owner-789', status: 'active' as const, createdAt: new Date(), updatedAt: new Date(), maxDrivers: null, approvalRequired: true, lateJoinAllowed: true, raceFrequency: null, raceDay: null, raceTime: null, tracks: null, scoringSystem: null, bonusPointsEnabled: false, penaltiesEnabled: false, protestsEnabled: false, appealsEnabled: false, stewardTeam: null, gameType: null, skillLevel: null, category: null, tags: null, }; (mockLeagueRepository.findById as Mock).mockImplementation(() => Promise.resolve(mockLeague)); (mockDriverRepository.findById as Mock).mockImplementation(() => Promise.resolve(null)); // When & Then await expect(useCase.execute(command)).rejects.toThrow('Driver not found'); expect(mockLeagueRepository.findById).toHaveBeenCalledWith('league-123'); expect(mockDriverRepository.findById).toHaveBeenCalledWith('driver-456'); }); }); describe('Scenario 3: approvalRequired path uses pending requests + time determinism', () => { it('should add pending request with deterministic time when approvalRequired is true', async () => { // Given const command: JoinLeagueCommand = { leagueId: 'league-123', driverId: 'driver-456', }; const mockLeague = { id: 'league-123', name: 'Test League', description: null, visibility: 'public' as const, ownerId: 'owner-789', status: 'active' as const, createdAt: new Date(), updatedAt: new Date(), maxDrivers: null, approvalRequired: true, lateJoinAllowed: true, raceFrequency: null, raceDay: null, raceTime: null, tracks: null, scoringSystem: null, bonusPointsEnabled: false, penaltiesEnabled: false, protestsEnabled: false, appealsEnabled: false, stewardTeam: null, gameType: null, skillLevel: null, category: null, tags: null, }; const mockDriver = { id: 'driver-456', name: 'Test Driver', iracingId: 'iracing-123', avatarUrl: null, createdAt: new Date(), updatedAt: new Date(), }; // Freeze time for deterministic testing const frozenTime = new Date('2024-01-01T00:00:00.000Z'); vi.setSystemTime(frozenTime); (mockLeagueRepository.findById as Mock).mockResolvedValue(mockLeague); (mockDriverRepository.findById as Mock).mockResolvedValue(mockDriver); // When await useCase.execute(command); // Then expect(mockLeagueRepository.addPendingRequests).toHaveBeenCalledWith( 'league-123', expect.arrayContaining([ expect.objectContaining({ id: expect.any(String), driverId: 'driver-456', name: 'Test Driver', requestDate: frozenTime, }), ]) ); // Verify no members were added expect(mockLeagueRepository.addLeagueMembers).not.toHaveBeenCalled(); // Reset system time vi.useRealTimers(); }); }); describe('Scenario 4: no-approval path adds member', () => { it('should add member when approvalRequired is false', async () => { // Given const command: JoinLeagueCommand = { leagueId: 'league-123', driverId: 'driver-456', }; const mockLeague = { id: 'league-123', name: 'Test League', description: null, visibility: 'public' as const, ownerId: 'owner-789', status: 'active' as const, createdAt: new Date(), updatedAt: new Date(), maxDrivers: null, approvalRequired: false, lateJoinAllowed: true, raceFrequency: null, raceDay: null, raceTime: null, tracks: null, scoringSystem: null, bonusPointsEnabled: false, penaltiesEnabled: false, protestsEnabled: false, appealsEnabled: false, stewardTeam: null, gameType: null, skillLevel: null, category: null, tags: null, }; const mockDriver = { id: 'driver-456', name: 'Test Driver', iracingId: 'iracing-123', avatarUrl: null, createdAt: new Date(), updatedAt: new Date(), }; (mockLeagueRepository.findById as Mock).mockResolvedValue(mockLeague); (mockDriverRepository.findById as Mock).mockResolvedValue(mockDriver); // When await useCase.execute(command); // Then expect(mockLeagueRepository.addLeagueMembers).toHaveBeenCalledWith( 'league-123', expect.arrayContaining([ expect.objectContaining({ driverId: 'driver-456', name: 'Test Driver', role: 'member', joinDate: expect.any(Date), }), ]) ); // Verify no pending requests were added expect(mockLeagueRepository.addPendingRequests).not.toHaveBeenCalled(); }); }); });