import type { Logger } from '@core/shared/application/Logger'; import { Result } from '@core/shared/domain/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; import type { NotificationService } from '../../../notifications/application/ports/NotificationService'; import type { LeagueMembershipRepository } from '../../domain/repositories/LeagueMembershipRepository'; import type { LeagueRepository } from '../../domain/repositories/LeagueRepository'; import type { RaceEventRepository } from '../../domain/repositories/RaceEventRepository'; import type { ResultRepository } from '../../domain/repositories/ResultRepository'; import { SendFinalResultsUseCase, type SendFinalResultsErrorCode, type SendFinalResultsInput, } from './SendFinalResultsUseCase'; const unwrapError = ( result: Result>, ): ApplicationErrorCode => { expect(result.isErr()).toBe(true); return result.unwrapErr(); }; describe('SendFinalResultsUseCase', () => { let notificationService: { sendNotification: Mock }; let raceEventRepository: { findById: Mock }; let resultRepository: { findByRaceId: Mock }; let leagueRepository: { findById: Mock }; let membershipRepository: { getMembership: Mock }; let logger: Logger; let useCase: SendFinalResultsUseCase; beforeEach(() => { notificationService = { sendNotification: vi.fn() }; raceEventRepository = { findById: vi.fn() }; resultRepository = { findByRaceId: vi.fn() }; leagueRepository = { findById: vi.fn() }; membershipRepository = { getMembership: vi.fn() }; logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; useCase = new SendFinalResultsUseCase(notificationService as unknown as NotificationService, raceEventRepository as unknown as RaceEventRepository, resultRepository as unknown as ResultRepository, leagueRepository as unknown as LeagueRepository, membershipRepository as unknown as LeagueMembershipRepository, logger); }); const createInput = (overrides: Partial = {}): SendFinalResultsInput => ({ leagueId: 'league-1', raceId: 'race-1', triggeredById: 'user-1', ...overrides, }); it('sends final results notifications to all participating drivers and presents result', async () => { const mockRaceEvent = { id: 'race-1', leagueId: 'league-1', name: 'Test Race', status: 'closed', getMainRaceSession: vi.fn().mockReturnValue({ id: 'session-1' }), }; const mockLeague = { id: 'league-1' }; const mockMembership = { role: { toString: () => 'steward' } }; leagueRepository.findById.mockResolvedValue(mockLeague); raceEventRepository.findById.mockResolvedValue(mockRaceEvent); membershipRepository.getMembership.mockResolvedValue(mockMembership); const mockResults = [ { driverId: { toString: () => 'driver-1' }, position: { toNumber: () => 1 }, incidents: { toNumber: () => 0 }, getPositionChange: vi.fn().mockReturnValue(2), }, { driverId: { toString: () => 'driver-2' }, position: { toNumber: () => 2 }, incidents: { toNumber: () => 1 }, getPositionChange: vi.fn().mockReturnValue(-1), }, ]; resultRepository.findByRaceId.mockResolvedValue(mockResults); const input = createInput(); const result = await useCase.execute(input); expect(result.isOk()).toBe(true); const presented = result.unwrap(); expect(leagueRepository.findById).toHaveBeenCalledWith('league-1'); expect(raceEventRepository.findById).toHaveBeenCalledWith('race-1'); expect(resultRepository.findByRaceId).toHaveBeenCalledWith('session-1'); expect(notificationService.sendNotification).toHaveBeenCalledTimes(2); expect(presented).toEqual({ leagueId: 'league-1', raceId: 'race-1', notificationsSent: 2, }); }); it('returns LEAGUE_NOT_FOUND when league does not exist', async () => { leagueRepository.findById.mockResolvedValue(null); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('LEAGUE_NOT_FOUND'); expect(error.details?.message).toBe('League not found'); }); it('returns RACE_NOT_FOUND when race event does not exist', async () => { leagueRepository.findById.mockResolvedValue({ id: 'league-1' }); raceEventRepository.findById.mockResolvedValue(null); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('RACE_NOT_FOUND'); expect(error.details?.message).toBe('Race event not found'); }); it('returns INSUFFICIENT_PERMISSIONS when user is not steward or higher', async () => { leagueRepository.findById.mockResolvedValue({ id: 'league-1' }); raceEventRepository.findById.mockResolvedValue({ id: 'race-1', leagueId: 'league-1', status: 'closed' }); membershipRepository.getMembership.mockResolvedValue(null); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('INSUFFICIENT_PERMISSIONS'); expect(error.details?.message).toBe('Insufficient permissions to send final results'); }); it('returns RESULTS_NOT_FINAL when race is not closed', async () => { leagueRepository.findById.mockResolvedValue({ id: 'league-1' }); raceEventRepository.findById.mockResolvedValue({ id: 'race-1', leagueId: 'league-1', name: 'Test Race', status: 'in_progress', getMainRaceSession: vi.fn(), }); membershipRepository.getMembership.mockResolvedValue({ role: { toString: () => 'steward' } }); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('RESULTS_NOT_FINAL'); expect(error.details?.message).toBe('Race results are not in a final state'); }); it('returns RESULTS_NOT_FINAL when main race session is missing', async () => { leagueRepository.findById.mockResolvedValue({ id: 'league-1' }); raceEventRepository.findById.mockResolvedValue({ id: 'race-1', leagueId: 'league-1', name: 'Test Race', status: 'closed', getMainRaceSession: vi.fn().mockReturnValue(undefined), }); membershipRepository.getMembership.mockResolvedValue({ role: { toString: () => 'steward' } }); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('RESULTS_NOT_FINAL'); expect(error.details?.message).toBe('Main race session not found for race event'); }); it('wraps repository errors into REPOSITORY_ERROR and does not present output', async () => { const mockError = new Error('Repository failure'); leagueRepository.findById.mockRejectedValue(mockError); const result = await useCase.execute(createInput()); const error = unwrapError(result); expect(error.code).toBe('REPOSITORY_ERROR'); expect(error.details?.message).toBe('Repository failure'); expect(logger.error).toHaveBeenCalled(); }); });