import { describe, it, expect } from 'vitest'; import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase'; import { Driver } from '@core/racing/domain/entities/Driver'; import { Race } from '@core/racing/domain/entities/Race'; import { Result } from '@core/racing/domain/entities/Result'; import { League } from '@core/racing/domain/entities/League'; import { Standing } from '@core/racing/domain/entities/Standing'; import { LeagueMembership, JoinRequest } from '@core/racing/domain/entities/LeagueMembership'; import type { FeedItem } from '@core/social/domain/types/FeedItem'; interface TestImageService { getDriverAvatar(driverId: string): string; getTeamLogo(teamId: string): string; getLeagueCover(leagueId: string): string; getLeagueLogo(leagueId: string): string; } function createTestImageService(): TestImageService { return { getDriverAvatar: (driverId: string) => `avatar-${driverId}`, getTeamLogo: (teamId: string) => `team-logo-${teamId}`, getLeagueCover: (leagueId: string) => `league-cover-${leagueId}`, getLeagueLogo: (leagueId: string) => `league-logo-${leagueId}`, }; } describe('DashboardOverviewUseCase', () => { it('partitions upcoming races into myUpcomingRaces and otherUpcomingRaces and selects nextRace from myUpcomingRaces', async () => { // Given a driver with memberships in two leagues and future races with mixed registration const driverId = 'driver-1'; const driver = Driver.create({ id: driverId, iracingId: '12345', name: 'Alice Racer', country: 'US' }); const leagues = [ League.create({ id: 'league-1', name: 'Alpha League', description: 'First league', ownerId: 'owner-1' }), League.create({ id: 'league-2', name: 'Beta League', description: 'Second league', ownerId: 'owner-2' }), ]; const now = Date.now(); const races = [ Race.create({ id: 'race-1', leagueId: 'league-1', track: 'Monza', car: 'GT3', scheduledAt: new Date(now + 60 * 60 * 1000), status: 'scheduled', }), Race.create({ id: 'race-2', leagueId: 'league-1', track: 'Spa', car: 'GT3', scheduledAt: new Date(now + 2 * 60 * 60 * 1000), status: 'scheduled', }), Race.create({ id: 'race-3', leagueId: 'league-2', track: 'Silverstone', car: 'GT4', scheduledAt: new Date(now + 3 * 60 * 60 * 1000), status: 'scheduled', }), Race.create({ id: 'race-4', leagueId: 'league-2', track: 'Imola', car: 'GT4', scheduledAt: new Date(now + 4 * 60 * 60 * 1000), status: 'scheduled', }), ]; const results: Result[] = []; const memberships = [ LeagueMembership.create({ leagueId: 'league-1', driverId, role: 'member', status: 'active', }), LeagueMembership.create({ leagueId: 'league-2', driverId, role: 'member', status: 'active', }), ]; const registeredRaceIds = new Set(['race-1', 'race-3']); const feedItems: FeedItem[] = []; const friends: Driver[] = []; const driverRepository = { findById: async (id: string): Promise => (id === driver.id ? driver : null), findByIRacingId: async (): Promise => null, findAll: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByIRacingId: async (): Promise => false, }; const raceRepository = { findById: async (): Promise => null, findAll: async (): Promise => races, findByLeagueId: async (): Promise => [], findUpcomingByLeagueId: async (): Promise => [], findCompletedByLeagueId: async (): Promise => [], findByStatus: async (): Promise => [], findByDateRange: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const resultRepository = { findById: async (): Promise => null, findAll: async (): Promise => results, findByRaceId: async (): Promise => [], findByDriverId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, createMany: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByRaceId: async (): Promise => false, }; const leagueRepository = { findById: async (): Promise => null, findAll: async (): Promise => leagues, findByOwnerId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, searchByName: async (): Promise => [], }; const standingRepository = { findByLeagueId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => null, findAll: async (): Promise => [], save: async (): Promise => { throw new Error('Not implemented'); }, saveMany: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, recalculate: async (): Promise => [], }; const leagueMembershipRepository = { getMembership: async (leagueId: string, driverIdParam: string): Promise => { return ( memberships.find( (m) => m.leagueId === leagueId && m.driverId === driverIdParam, ) ?? null ); }, getLeagueMembers: async (): Promise => [], getJoinRequests: async (): Promise => [], saveMembership: async (): Promise => { throw new Error('Not implemented'); }, removeMembership: async (): Promise => { throw new Error('Not implemented'); }, saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, removeJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, }; const raceRegistrationRepository = { isRegistered: async (raceId: string, driverIdParam: string): Promise => { if (driverIdParam !== driverId) return false; return registeredRaceIds.has(raceId); }, getRegisteredDrivers: async (): Promise => [], getRegistrationCount: async (): Promise => 0, register: async (): Promise => { throw new Error('Not implemented'); }, withdraw: async (): Promise => { throw new Error('Not implemented'); }, getDriverRegistrations: async (): Promise => [], clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, }; const feedRepository = { getFeedForDriver: async (): Promise => feedItems, getGlobalFeed: async (): Promise => [], }; const socialRepository = { getFriends: async (): Promise => friends, getFriendIds: async (): Promise => [], getSuggestedFriends: async (): Promise => [], }; const imageService = createTestImageService(); const getDriverStats = (id: string) => id === driverId ? { rating: 1600, wins: 5, podiums: 12, totalRaces: 40, overallRank: 42, consistency: 88, } : null; const useCase = new DashboardOverviewUseCase( driverRepository, raceRepository, resultRepository, leagueRepository, standingRepository, leagueMembershipRepository, raceRegistrationRepository, feedRepository, socialRepository, imageService, getDriverStats, ); // When const result = await useCase.execute({ driverId }); expect(result.isOk()).toBe(true); const vm = result.unwrap(); // Then myUpcomingRaces only contains registered races from the driver's leagues expect(vm.myUpcomingRaces.map((r) => r.id)).toEqual(['race-1', 'race-3']); // And otherUpcomingRaces contains the other upcoming races in those leagues expect(vm.otherUpcomingRaces.map((r) => r.id)).toEqual(['race-2', 'race-4']); // And nextRace is the earliest upcoming race from myUpcomingRaces expect(vm.nextRace).not.toBeNull(); expect(vm.nextRace!.id).toBe('race-1'); }); it('builds recentResults sorted by date descending and leagueStandingsSummaries from standings', async () => { // Given completed races with results and standings const driverId = 'driver-2'; const driver = Driver.create({ id: driverId, iracingId: '67890', name: 'Result Driver', country: 'DE' }); const leagues = [ League.create({ id: 'league-A', name: 'Results League A', description: 'League A', ownerId: 'owner-A' }), League.create({ id: 'league-B', name: 'Results League B', description: 'League B', ownerId: 'owner-B' }), ]; const raceOld = Race.create({ id: 'race-old', leagueId: 'league-A', track: 'Old Circuit', car: 'GT3', scheduledAt: new Date('2024-01-01T10:00:00Z'), status: 'completed', }); const raceNew = Race.create({ id: 'race-new', leagueId: 'league-B', track: 'New Circuit', car: 'GT4', scheduledAt: new Date('2024-02-01T10:00:00Z'), status: 'completed', }); const races = [raceOld, raceNew]; const results = [ Result.create({ id: 'result-old', raceId: raceOld.id, driverId, position: 5, fastestLap: 120, incidents: 3, startPosition: 5, }), Result.create({ id: 'result-new', raceId: raceNew.id, driverId, position: 2, fastestLap: 115, incidents: 1, startPosition: 2, }), ]; const memberships = [ LeagueMembership.create({ leagueId: 'league-A', driverId, role: 'member', status: 'active', }), LeagueMembership.create({ leagueId: 'league-B', driverId, role: 'member', status: 'active', }), ]; const standingsByLeague = new Map< string, Standing[] >(); standingsByLeague.set('league-A', [ Standing.create({ leagueId: 'league-A', driverId, position: 3, points: 50 }), Standing.create({ leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 }), ]); standingsByLeague.set('league-B', [ Standing.create({ leagueId: 'league-B', driverId, position: 1, points: 100 }), Standing.create({ leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 }), ]); const driverRepository = { findById: async (id: string): Promise => (id === driver.id ? driver : null), findByIRacingId: async (): Promise => null, findAll: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByIRacingId: async (): Promise => false, }; const raceRepository = { findById: async (): Promise => null, findAll: async (): Promise => races, findByLeagueId: async (): Promise => [], findUpcomingByLeagueId: async (): Promise => [], findCompletedByLeagueId: async (): Promise => [], findByStatus: async (): Promise => [], findByDateRange: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const resultRepository = { findById: async (): Promise => null, findAll: async (): Promise => results, findByRaceId: async (): Promise => [], findByDriverId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, createMany: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByRaceId: async (): Promise => false, }; const leagueRepository = { findById: async (): Promise => null, findAll: async (): Promise => leagues, findByOwnerId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, searchByName: async (): Promise => [], }; const standingRepository = { findByLeagueId: async (leagueId: string): Promise => standingsByLeague.get(leagueId) ?? [], findByDriverIdAndLeagueId: async (): Promise => null, findAll: async (): Promise => [], save: async (): Promise => { throw new Error('Not implemented'); }, saveMany: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, recalculate: async (): Promise => [], }; const leagueMembershipRepository = { getMembership: async (leagueId: string, driverIdParam: string): Promise => { return ( memberships.find( (m) => m.leagueId === leagueId && m.driverId === driverIdParam, ) ?? null ); }, getLeagueMembers: async (): Promise => [], getJoinRequests: async (): Promise => [], saveMembership: async (): Promise => { throw new Error('Not implemented'); }, removeMembership: async (): Promise => { throw new Error('Not implemented'); }, saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, removeJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, }; const raceRegistrationRepository = { isRegistered: async (): Promise => false, getRegisteredDrivers: async (): Promise => [], getRegistrationCount: async (): Promise => 0, register: async (): Promise => { throw new Error('Not implemented'); }, withdraw: async (): Promise => { throw new Error('Not implemented'); }, getDriverRegistrations: async (): Promise => [], clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, }; const feedRepository = { getFeedForDriver: async (): Promise => [], getGlobalFeed: async (): Promise => [], }; const socialRepository = { getFriends: async (): Promise => [], getFriendIds: async (): Promise => [], getSuggestedFriends: async (): Promise => [], }; const imageService = createTestImageService(); const getDriverStats = (id: string) => id === driverId ? { rating: 1800, wins: 3, podiums: 7, totalRaces: 20, overallRank: 10, consistency: 92, } : null; const useCase = new DashboardOverviewUseCase( driverRepository, raceRepository, resultRepository, leagueRepository, standingRepository, leagueMembershipRepository, raceRegistrationRepository, feedRepository, socialRepository, imageService, getDriverStats, ); // When const result = await useCase.execute({ driverId }); expect(result.isOk()).toBe(true); const vm = result.unwrap(); // Then recentResults are sorted by finishedAt descending (newest first) expect(vm.recentResults.length).toBe(2); expect(vm.recentResults[0]!.raceId).toBe('race-new'); expect(vm.recentResults[1]!.raceId).toBe('race-old'); // And leagueStandingsSummaries reflect the driver's position and points per league const summariesByLeague = new Map( vm.leagueStandingsSummaries.map((s) => [s.leagueId, s]), ); const summaryA = summariesByLeague.get('league-A'); const summaryB = summariesByLeague.get('league-B'); expect(summaryA).toBeDefined(); expect(summaryA!.position).toBe(3); expect(summaryA!.points).toBe(50); expect(summaryA!.totalDrivers).toBe(2); expect(summaryB).toBeDefined(); expect(summaryB!.position).toBe(1); expect(summaryB!.points).toBe(100); expect(summaryB!.totalDrivers).toBe(2); }); it('returns empty collections and safe defaults when driver has no races or standings', async () => { // Given a driver with no related data const driverId = 'driver-empty'; const driver = Driver.create({ id: driverId, iracingId: '11111', name: 'New Racer', country: 'FR' }); const driverRepository = { findById: async (id: string): Promise => (id === driver.id ? driver : null), findByIRacingId: async (): Promise => null, findAll: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByIRacingId: async (): Promise => false, }; const raceRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByLeagueId: async (): Promise => [], findUpcomingByLeagueId: async (): Promise => [], findCompletedByLeagueId: async (): Promise => [], findByStatus: async (): Promise => [], findByDateRange: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, }; const resultRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByRaceId: async (): Promise => [], findByDriverId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, createMany: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, existsByRaceId: async (): Promise => false, }; const leagueRepository = { findById: async (): Promise => null, findAll: async (): Promise => [], findByOwnerId: async (): Promise => [], create: async (): Promise => { throw new Error('Not implemented'); }, update: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, searchByName: async (): Promise => [], }; const standingRepository = { findByLeagueId: async (): Promise => [], findByDriverIdAndLeagueId: async (): Promise => null, findAll: async (): Promise => [], save: async (): Promise => { throw new Error('Not implemented'); }, saveMany: async (): Promise => { throw new Error('Not implemented'); }, delete: async (): Promise => { throw new Error('Not implemented'); }, deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, exists: async (): Promise => false, recalculate: async (): Promise => [], }; const leagueMembershipRepository = { getMembership: async (): Promise => null, getLeagueMembers: async (): Promise => [], getJoinRequests: async (): Promise => [], saveMembership: async (): Promise => { throw new Error('Not implemented'); }, removeMembership: async (): Promise => { throw new Error('Not implemented'); }, saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, removeJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, }; const raceRegistrationRepository = { isRegistered: async (): Promise => false, getRegisteredDrivers: async (): Promise => [], getRegistrationCount: async (): Promise => 0, register: async (): Promise => { throw new Error('Not implemented'); }, withdraw: async (): Promise => { throw new Error('Not implemented'); }, getDriverRegistrations: async (): Promise => [], clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, }; const feedRepository = { getFeedForDriver: async (): Promise => [], getGlobalFeed: async (): Promise => [], }; const socialRepository = { getFriends: async (): Promise => [], getFriendIds: async (): Promise => [], getSuggestedFriends: async (): Promise => [], }; const imageService = createTestImageService(); const getDriverStats = () => null; const useCase = new DashboardOverviewUseCase( driverRepository, raceRepository, resultRepository, leagueRepository, standingRepository, leagueMembershipRepository, raceRegistrationRepository, feedRepository, socialRepository, imageService, getDriverStats, ); // When const result = await useCase.execute({ driverId }); expect(result.isOk()).toBe(true); const vm = result.unwrap(); // Then collections are empty and no errors are thrown expect(vm.myUpcomingRaces).toEqual([]); expect(vm.otherUpcomingRaces).toEqual([]); expect(vm.nextRace).toBeNull(); expect(vm.recentResults).toEqual([]); expect(vm.leagueStandingsSummaries).toEqual([]); expect(vm.friends).toEqual([]); expect(vm.feedSummary.notificationCount).toBe(0); expect(vm.feedSummary.items).toEqual([]); }); });