import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { CompleteRaceUseCaseWithRatings, type CompleteRaceWithRatingsInput, type CompleteRaceWithRatingsResult, } from './CompleteRaceUseCaseWithRatings'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository'; import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { IStandingRepository } from '../../domain/repositories/IStandingRepository'; import { RatingUpdateService } from '@core/identity/domain/services/RatingUpdateService'; import type { IRaceResultsProvider } from '@core/identity/application/ports/IRaceResultsProvider'; describe('CompleteRaceUseCaseWithRatings', () => { let useCase: CompleteRaceUseCaseWithRatings; let raceRepository: { findById: Mock; update: Mock; }; let raceRegistrationRepository: { getRegisteredDrivers: Mock; }; let resultRepository: { create: Mock; }; let standingRepository: { findByDriverIdAndLeagueId: Mock; save: Mock; }; let driverRatingProvider: { getRatings: Mock; }; let ratingUpdateService: { updateDriverRatingsAfterRace: Mock; recordRaceRatingEvents: Mock; }; let raceResultsProvider: { getRaceResults: Mock; hasRaceResults: Mock; }; let output: { present: Mock }; beforeEach(() => { raceRepository = { findById: vi.fn(), update: vi.fn(), }; raceRegistrationRepository = { getRegisteredDrivers: vi.fn(), }; resultRepository = { create: vi.fn(), }; standingRepository = { findByDriverIdAndLeagueId: vi.fn(), save: vi.fn(), }; driverRatingProvider = { getRatings: vi.fn(), }; ratingUpdateService = { updateDriverRatingsAfterRace: vi.fn(), recordRaceRatingEvents: vi.fn(), }; raceResultsProvider = { getRaceResults: vi.fn(), hasRaceResults: vi.fn(), }; // Test without raceResultsProvider (backward compatible mode) useCase = new CompleteRaceUseCaseWithRatings( raceRepository as unknown as IRaceRepository, raceRegistrationRepository as unknown as IRaceRegistrationRepository, resultRepository as unknown as IResultRepository, standingRepository as unknown as IStandingRepository, driverRatingProvider, ratingUpdateService as unknown as RatingUpdateService, ); }); it('completes race with ratings when race exists and has registered drivers', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; const mockRace = { id: 'race-1', leagueId: 'league-1', status: 'scheduled', complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }), }; raceRepository.findById.mockResolvedValue(mockRace); raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1', 'driver-2']); driverRatingProvider.getRatings.mockReturnValue( new Map([ ['driver-1', 1600], ['driver-2', 1500], ]), ); resultRepository.create.mockResolvedValue(undefined); standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null); standingRepository.save.mockResolvedValue(undefined); ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined); raceRepository.update.mockResolvedValue(undefined); const result = await useCase.execute(command); expect(result.isOk()).toBe(true); const value = result.unwrap(); expect(value).toEqual({ raceId: 'race-1', ratingsUpdatedForDriverIds: ['driver-1', 'driver-2'], }); expect(raceRepository.findById).toHaveBeenCalledWith('race-1'); expect(raceRegistrationRepository.getRegisteredDrivers).toHaveBeenCalledWith('race-1'); expect(driverRatingProvider.getRatings).toHaveBeenCalledWith(['driver-1', 'driver-2']); expect(resultRepository.create).toHaveBeenCalledTimes(2); expect(standingRepository.save).toHaveBeenCalledTimes(2); expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled(); expect(mockRace.complete).toHaveBeenCalled(); expect(raceRepository.update).toHaveBeenCalledWith({ id: 'race-1', status: 'completed' }); }); it('returns error when race does not exist', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; raceRepository.findById.mockResolvedValue(null); const result = await useCase.execute(command); expect(result.isErr()).toBe(true); expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND'); }); it('returns error when race is already completed', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; const mockRace = { id: 'race-1', leagueId: 'league-1', status: 'completed', complete: vi.fn(), }; raceRepository.findById.mockResolvedValue(mockRace); const result = await useCase.execute(command); expect(result.isErr()).toBe(true); expect(result.unwrapErr().code).toBe('ALREADY_COMPLETED'); expect(raceRegistrationRepository.getRegisteredDrivers).not.toHaveBeenCalled(); }); it('returns error when no registered drivers', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; const mockRace = { id: 'race-1', leagueId: 'league-1', status: 'scheduled', complete: vi.fn(), }; raceRepository.findById.mockResolvedValue(mockRace); raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue([]); const result = await useCase.execute(command); expect(result.isErr()).toBe(true); expect(result.unwrapErr().code).toBe('NO_REGISTERED_DRIVERS'); }); it('returns rating update error when rating service throws', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; const mockRace = { id: 'race-1', leagueId: 'league-1', status: 'scheduled', complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }), }; raceRepository.findById.mockResolvedValue(mockRace); raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']); driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]])); resultRepository.create.mockResolvedValue(undefined); standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null); standingRepository.save.mockResolvedValue(undefined); ratingUpdateService.updateDriverRatingsAfterRace.mockRejectedValue(new Error('Rating error')); const result = await useCase.execute(command); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('RATING_UPDATE_FAILED'); expect(error.details?.message).toBe('Rating error'); }); it('returns repository error when persistence fails', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; const mockRace = { id: 'race-1', leagueId: 'league-1', status: 'scheduled', complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }), }; raceRepository.findById.mockResolvedValue(mockRace); raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']); driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]])); resultRepository.create.mockRejectedValue(new Error('DB error')); const result = await useCase.execute(command); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('REPOSITORY_ERROR'); expect(error.details?.message).toBe('DB error'); }); // SLICE 7: New tests for ledger-based approach describe('Ledger-based rating updates (Slice 7)', () => { let useCaseWithLedger: CompleteRaceUseCaseWithRatings; beforeEach(() => { // Create use case with raceResultsProvider for ledger mode useCaseWithLedger = new CompleteRaceUseCaseWithRatings( raceRepository as unknown as IRaceRepository, raceRegistrationRepository as unknown as IRaceRegistrationRepository, resultRepository as unknown as IResultRepository, standingRepository as unknown as IStandingRepository, driverRatingProvider, ratingUpdateService as unknown as RatingUpdateService, raceResultsProvider as unknown as IRaceResultsProvider, ); }); it('completes race with ledger-based rating updates when raceResultsProvider is available', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; const mockRace = { id: 'race-1', leagueId: 'league-1', status: 'scheduled', complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }), }; raceRepository.findById.mockResolvedValue(mockRace); raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1', 'driver-2']); driverRatingProvider.getRatings.mockReturnValue( new Map([ ['driver-1', 1600], ['driver-2', 1500], ]), ); resultRepository.create.mockResolvedValue(undefined); standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null); standingRepository.save.mockResolvedValue(undefined); // Mock ledger-based rating update ratingUpdateService.recordRaceRatingEvents.mockResolvedValue({ success: true, eventsCreated: 4, driversUpdated: ['driver-1', 'driver-2'], }); raceRepository.update.mockResolvedValue(undefined); const result = await useCaseWithLedger.execute(command); expect(result.isOk()).toBe(true); const value = result.unwrap(); expect(value).toEqual({ raceId: 'race-1', ratingsUpdatedForDriverIds: ['driver-1', 'driver-2'], }); // Verify ledger-based approach was used expect(ratingUpdateService.recordRaceRatingEvents).toHaveBeenCalledWith( 'race-1', expect.arrayContaining([ expect.objectContaining({ userId: 'driver-1', startPos: expect.any(Number), finishPos: expect.any(Number), incidents: expect.any(Number), status: 'finished', }), ]), ); // Verify legacy method was NOT called expect(ratingUpdateService.updateDriverRatingsAfterRace).not.toHaveBeenCalled(); expect(raceRepository.update).toHaveBeenCalledWith({ id: 'race-1', status: 'completed' }); }); it('falls back to legacy approach when ledger update fails', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; const mockRace = { id: 'race-1', leagueId: 'league-1', status: 'scheduled', complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }), }; raceRepository.findById.mockResolvedValue(mockRace); raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']); driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]])); resultRepository.create.mockResolvedValue(undefined); standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null); standingRepository.save.mockResolvedValue(undefined); // Mock ledger-based rating update failure ratingUpdateService.recordRaceRatingEvents.mockResolvedValue({ success: false, eventsCreated: 0, driversUpdated: [], }); // Legacy method should be called as fallback ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined); raceRepository.update.mockResolvedValue(undefined); const result = await useCaseWithLedger.execute(command); expect(result.isOk()).toBe(true); // Verify both methods were called (ledger attempted, then fallback) expect(ratingUpdateService.recordRaceRatingEvents).toHaveBeenCalled(); expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled(); }); it('handles ledger update errors gracefully', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; const mockRace = { id: 'race-1', leagueId: 'league-1', status: 'scheduled', complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }), }; raceRepository.findById.mockResolvedValue(mockRace); raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']); driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]])); resultRepository.create.mockResolvedValue(undefined); standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null); standingRepository.save.mockResolvedValue(undefined); // Mock ledger-based rating update throwing error ratingUpdateService.recordRaceRatingEvents.mockRejectedValue(new Error('Ledger error')); // Legacy method should be called as fallback ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined); raceRepository.update.mockResolvedValue(undefined); const result = await useCaseWithLedger.execute(command); expect(result.isOk()).toBe(true); // Verify fallback was used expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled(); }); it('still works without raceResultsProvider (backward compatibility)', async () => { const command: CompleteRaceWithRatingsInput = { raceId: 'race-1', }; const mockRace = { id: 'race-1', leagueId: 'league-1', status: 'scheduled', complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }), }; raceRepository.findById.mockResolvedValue(mockRace); raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']); driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]])); resultRepository.create.mockResolvedValue(undefined); standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null); standingRepository.save.mockResolvedValue(undefined); ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined); raceRepository.update.mockResolvedValue(undefined); // Use original useCase without raceResultsProvider const result = await useCase.execute(command); expect(result.isOk()).toBe(true); // Verify only legacy method was called expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled(); expect(ratingUpdateService.recordRaceRatingEvents).not.toHaveBeenCalled(); }); }); });