import { beforeEach, describe, expect, it } from 'vitest'; import { RatingEvent } from '../../domain/entities/RatingEvent'; import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository'; import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository'; import { RatingEventId } from '../../domain/value-objects/RatingEventId'; import { UserRating } from '../../domain/value-objects/UserRating'; import { RaceResultsData, RaceResultsProvider } from '../ports/RaceResultsProvider'; import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase'; import { RecordRaceRatingEventsUseCase } from './RecordRaceRatingEventsUseCase'; // Mock implementations class MockRaceResultsProvider implements RaceResultsProvider { private results: RaceResultsData | null = null; setResults(results: RaceResultsData | null) { this.results = results; } async getRaceResults(): Promise { return this.results; } async hasRaceResults(): Promise { return this.results !== null; } } class MockRatingEventRepository implements RatingEventRepository { private events: RatingEvent[] = []; async save(event: RatingEvent): Promise { this.events.push(event); return event; } async findByUserId(userId: string): Promise { return this.events.filter(e => e.userId === userId); } async findByIds(ids: RatingEventId[]): Promise { return this.events.filter(e => ids.some(id => id.equals(e.id))); } async getAllByUserId(userId: string): Promise { return this.events.filter(e => e.userId === userId); } async findEventsPaginated(userId: string, options?: import('@core/identity/domain/repositories/RatingEventRepository').PaginatedQueryOptions): Promise> { const allEvents = await this.findByUserId(userId); // Apply filters let filtered = allEvents; if (options?.filter) { const filter = options.filter; if (filter.dimensions) { filtered = filtered.filter(e => filter.dimensions!.includes(e.dimension.value)); } if (filter.sourceTypes) { filtered = filtered.filter(e => filter.sourceTypes!.includes(e.source.type)); } if (filter.from) { filtered = filtered.filter(e => e.occurredAt >= filter.from!); } if (filter.to) { filtered = filtered.filter(e => e.occurredAt <= filter.to!); } if (filter.reasonCodes) { filtered = filtered.filter(e => filter.reasonCodes!.includes(e.reason.code)); } if (filter.visibility) { filtered = filtered.filter(e => e.visibility.public === (filter.visibility === 'public')); } } const total = filtered.length; const limit = options?.limit ?? 10; const offset = options?.offset ?? 0; const items = filtered.slice(offset, offset + limit); const hasMore = offset + limit < total; const nextOffset = hasMore ? offset + limit : undefined; const result: import('@core/identity/domain/repositories/RatingEventRepository').PaginatedResult = { items, total, limit, offset, hasMore }; if (nextOffset !== undefined) { result.nextOffset = nextOffset; } return result; } } class MockUserRatingRepository implements UserRatingRepository { private ratings: Map = new Map(); async findByUserId(userId: string): Promise { return this.ratings.get(userId) || null; } async save(userRating: UserRating): Promise { this.ratings.set(userRating.userId, userRating); return userRating; } // Helper for tests setRating(userId: string, rating: UserRating) { this.ratings.set(userId, rating); } } describe('RecordRaceRatingEventsUseCase', () => { let useCase: RecordRaceRatingEventsUseCase; let mockRaceResultsProvider: MockRaceResultsProvider; let mockRatingEventRepository: MockRatingEventRepository; let mockUserRatingRepository: MockUserRatingRepository; let appendRatingEventsUseCase: AppendRatingEventsUseCase; beforeEach(() => { mockRaceResultsProvider = new MockRaceResultsProvider(); mockRatingEventRepository = new MockRatingEventRepository(); mockUserRatingRepository = new MockUserRatingRepository(); appendRatingEventsUseCase = new AppendRatingEventsUseCase( mockRatingEventRepository, mockUserRatingRepository ); useCase = new RecordRaceRatingEventsUseCase( mockRaceResultsProvider, mockUserRatingRepository, appendRatingEventsUseCase ); }); describe('execute', () => { it('should return error when race results not found', async () => { mockRaceResultsProvider.setResults(null); const result = await useCase.execute({ raceId: 'race-123' }); expect(result.success).toBe(false); expect(result.raceId).toBe('race-123'); expect(result.eventsCreated).toBe(0); expect(result.driversUpdated).toEqual([]); expect(result.errors).toContain('Race results not found'); }); it('should return error when no results in race', async () => { mockRaceResultsProvider.setResults({ raceId: 'race-123', results: [], }); const result = await useCase.execute({ raceId: 'race-123' }); expect(result.success).toBe(false); expect(result.eventsCreated).toBe(0); expect(result.errors).toContain('No results found for race'); }); it('should process single driver with good performance', async () => { mockRaceResultsProvider.setResults({ raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', sof: 2500, }, ], }); // Set initial rating for user const initialRating = UserRating.create('user-123'); mockUserRatingRepository.setRating('user-123', initialRating); const result = await useCase.execute({ raceId: 'race-123' }); expect(result.success).toBe(true); expect(result.raceId).toBe('race-123'); expect(result.eventsCreated).toBeGreaterThan(0); expect(result.driversUpdated).toContain('user-123'); expect(result.errors).toEqual([]); }); it('should process multiple drivers with mixed results', async () => { mockRaceResultsProvider.setResults({ raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', sof: 2500, }, { userId: 'user-456', startPos: 3, finishPos: 8, incidents: 2, status: 'finished', sof: 2500, }, { userId: 'user-789', startPos: 5, finishPos: 5, incidents: 0, status: 'dns', sof: 2500, }, ], }); // Set initial ratings mockUserRatingRepository.setRating('user-123', UserRating.create('user-123')); mockUserRatingRepository.setRating('user-456', UserRating.create('user-456')); mockUserRatingRepository.setRating('user-789', UserRating.create('user-789')); const result = await useCase.execute({ raceId: 'race-123' }); expect(result.success).toBe(true); expect(result.eventsCreated).toBeGreaterThan(0); expect(result.driversUpdated.length).toBe(3); expect(result.driversUpdated).toContain('user-123'); expect(result.driversUpdated).toContain('user-456'); expect(result.driversUpdated).toContain('user-789'); expect(result.errors).toEqual([]); }); it('should compute SoF if not provided', async () => { mockRaceResultsProvider.setResults({ raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', // No sof }, { userId: 'user-456', startPos: 3, finishPos: 8, incidents: 0, status: 'finished', // No sof }, ], }); // Set ratings for SoF calculation const rating1 = UserRating.create('user-123'); const rating2 = UserRating.create('user-456'); // Update driver ratings to specific values mockUserRatingRepository.setRating('user-123', rating1.updateDriverRating(60)); mockUserRatingRepository.setRating('user-456', rating2.updateDriverRating(40)); const result = await useCase.execute({ raceId: 'race-123' }); expect(result.success).toBe(true); expect(result.eventsCreated).toBeGreaterThan(0); expect(result.driversUpdated.length).toBe(2); }); it('should handle errors for individual drivers gracefully', async () => { mockRaceResultsProvider.setResults({ raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', sof: 2500, }, { userId: 'user-456', startPos: 3, finishPos: 8, incidents: 0, status: 'finished', sof: 2500, }, ], }); // Set ratings for both users mockUserRatingRepository.setRating('user-123', UserRating.create('user-123')); mockUserRatingRepository.setRating('user-456', UserRating.create('user-456')); // Make the repository throw an error for user-456 const originalSave = mockRatingEventRepository.save; let user456CallCount = 0; mockRatingEventRepository.save = async (event: RatingEvent) => { if (event.userId === 'user-456') { user456CallCount++; if (user456CallCount === 1) { // Fail on first save attempt throw new Error('Database constraint violation for user-456'); } } return event; }; const result = await useCase.execute({ raceId: 'race-123' }); // Should still succeed overall but with errors expect(result.raceId).toBe('race-123'); expect(result.driversUpdated).toContain('user-123'); expect(result.errors).toBeDefined(); expect(result.errors!.length).toBeGreaterThan(0); expect(result.errors![0]).toContain('user-456'); // Restore mockRatingEventRepository.save = originalSave; }); it('should return success with no events when no valid events created', async () => { // This would require a scenario where factory creates no events // For now, we'll test with empty results mockRaceResultsProvider.setResults({ raceId: 'race-123', results: [], }); const result = await useCase.execute({ raceId: 'race-123' }); expect(result.success).toBe(false); // No results }); it('should handle repository errors', async () => { mockRaceResultsProvider.setResults({ raceId: 'race-123', results: [ { userId: 'user-123', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', sof: 2500, }, ], }); mockUserRatingRepository.setRating('user-123', UserRating.create('user-123')); // Mock repository to throw error const originalSave = mockRatingEventRepository.save; mockRatingEventRepository.save = async () => { throw new Error('Repository error'); }; const result = await useCase.execute({ raceId: 'race-123' }); expect(result.success).toBe(false); expect(result.errors).toBeDefined(); expect(result.errors!.length).toBeGreaterThan(0); // Restore mockRatingEventRepository.save = originalSave; }); }); });