import { describe, it, expect, vi, beforeEach } from 'vitest'; import { JoinLeagueUseCase } from './JoinLeagueUseCase'; import type { LeagueRepository } from '../ports/LeagueRepository'; import type { DriverRepository } from '../../../racing/domain/repositories/DriverRepository'; import type { EventPublisher } from '../../../shared/ports/EventPublisher'; import type { JoinLeagueCommand } from '../ports/JoinLeagueCommand'; const mockLeagueRepository = { findById: vi.fn(), addPendingRequests: vi.fn(), addLeagueMembers: vi.fn(), }; const mockDriverRepository = { findDriverById: vi.fn(), }; const mockEventPublisher = { publish: vi.fn(), }; describe('JoinLeagueUseCase', () => { let useCase: JoinLeagueUseCase; beforeEach(() => { // Reset mocks vi.clearAllMocks(); useCase = new JoinLeagueUseCase( mockLeagueRepository as any, mockDriverRepository as any, mockEventPublisher as any ); }); 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.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.mockImplementation(() => Promise.resolve(mockLeague)); mockDriverRepository.findDriverById.mockImplementation(() => Promise.resolve(null)); // When & Then await expect(useCase.execute(command)).rejects.toThrow('Driver not found'); expect(mockLeagueRepository.findById).toHaveBeenCalledWith('league-123'); expect(mockDriverRepository.findDriverById).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.mockResolvedValue(mockLeague); mockDriverRepository.findDriverById.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.mockResolvedValue(mockLeague); mockDriverRepository.findDriverById.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(); }); }); });