/** * Integration Test: Dashboard Use Case Orchestration * * Tests the orchestration logic of dashboard-related Use Cases: * - GetDashboardUseCase: Retrieves driver statistics, upcoming races, standings, and activity * - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers) * - Uses In-Memory adapters for fast, deterministic testing * * Focus: Business logic orchestration, NOT UI rendering */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository'; import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository'; import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository'; import { InMemoryActivityRepository } from '../../../adapters/activity/persistence/inmemory/InMemoryActivityRepository'; import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher'; import { GetDashboardUseCase } from '../../../core/dashboard/application/use-cases/GetDashboardUseCase'; import { DashboardQuery } from '../../../core/dashboard/application/ports/DashboardQuery'; describe('Dashboard Use Case Orchestration', () => { let driverRepository: InMemoryDriverRepository; let raceRepository: InMemoryRaceRepository; let leagueRepository: InMemoryLeagueRepository; let activityRepository: InMemoryActivityRepository; let eventPublisher: InMemoryEventPublisher; let getDashboardUseCase: GetDashboardUseCase; beforeAll(() => { driverRepository = new InMemoryDriverRepository(); raceRepository = new InMemoryRaceRepository(); leagueRepository = new InMemoryLeagueRepository(); activityRepository = new InMemoryActivityRepository(); eventPublisher = new InMemoryEventPublisher(); getDashboardUseCase = new GetDashboardUseCase({ driverRepository, raceRepository, leagueRepository, activityRepository, eventPublisher, }); }); beforeEach(() => { driverRepository.clear(); raceRepository.clear(); leagueRepository.clear(); activityRepository.clear(); eventPublisher.clear(); }); describe('GetDashboardUseCase - Success Path', () => { it('should retrieve complete dashboard data for a driver with all data', async () => { // Scenario: Driver with complete data // Given: A driver exists with statistics (rating, rank, starts, wins, podiums) const driverId = 'driver-123'; driverRepository.addDriver({ id: driverId, name: 'John Doe', avatar: 'https://example.com/avatar.jpg', rating: 1500, rank: 123, starts: 10, wins: 3, podiums: 5, leagues: 2, }); // And: The driver has upcoming races scheduled raceRepository.addUpcomingRaces(driverId, [ { id: 'race-1', trackName: 'Monza', carType: 'GT3', scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now }, { id: 'race-2', trackName: 'Spa', carType: 'GT3', scheduledDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // 5 days from now }, { id: 'race-3', trackName: 'Nürburgring', carType: 'GT3', scheduledDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000), // 1 day from now }, { id: 'race-4', trackName: 'Silverstone', carType: 'GT3', scheduledDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now }, { id: 'race-5', trackName: 'Imola', carType: 'GT3', scheduledDate: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now }, ]); // And: The driver is participating in active championships leagueRepository.addLeagueStandings(driverId, [ { leagueId: 'league-1', leagueName: 'GT3 Championship', position: 5, points: 150, totalDrivers: 20, }, { leagueId: 'league-2', leagueName: 'Endurance Series', position: 12, points: 85, totalDrivers: 15, }, ]); // And: The driver has recent activity (race results, events) activityRepository.addRecentActivity(driverId, [ { id: 'activity-1', type: 'race_result', description: 'Finished 3rd at Monza', timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // 1 day ago status: 'success', }, { id: 'activity-2', type: 'league_invitation', description: 'Invited to League XYZ', timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // 2 days ago status: 'info', }, { id: 'activity-3', type: 'achievement', description: 'Reached 1500 rating', timestamp: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), // 3 days ago status: 'success', }, ]); // When: GetDashboardUseCase.execute() is called with driver ID const result = await getDashboardUseCase.execute({ driverId }); // Then: The result should contain all dashboard sections expect(result).toBeDefined(); expect(result.driver.id).toBe(driverId); expect(result.driver.name).toBe('John Doe'); expect(result.driver.avatar).toBe('https://example.com/avatar.jpg'); // And: Driver statistics should be correctly calculated expect(result.statistics.rating).toBe(1500); expect(result.statistics.rank).toBe(123); expect(result.statistics.starts).toBe(10); expect(result.statistics.wins).toBe(3); expect(result.statistics.podiums).toBe(5); expect(result.statistics.leagues).toBe(2); // And: Upcoming races should be limited to 3 expect(result.upcomingRaces).toHaveLength(3); // And: The races should be sorted by scheduled date (earliest first) expect(result.upcomingRaces[0].trackName).toBe('Nürburgring'); // 1 day expect(result.upcomingRaces[1].trackName).toBe('Monza'); // 2 days expect(result.upcomingRaces[2].trackName).toBe('Imola'); // 3 days // And: Championship standings should include league info expect(result.championshipStandings).toHaveLength(2); expect(result.championshipStandings[0].leagueName).toBe('GT3 Championship'); expect(result.championshipStandings[0].position).toBe(5); expect(result.championshipStandings[0].points).toBe(150); expect(result.championshipStandings[0].totalDrivers).toBe(20); // And: Recent activity should be sorted by timestamp (newest first) expect(result.recentActivity).toHaveLength(3); expect(result.recentActivity[0].description).toBe('Finished 3rd at Monza'); expect(result.recentActivity[0].status).toBe('success'); expect(result.recentActivity[1].description).toBe('Invited to League XYZ'); expect(result.recentActivity[2].description).toBe('Reached 1500 rating'); // And: EventPublisher should emit DashboardAccessedEvent expect(eventPublisher.getDashboardAccessedEventCount()).toBe(1); }); it('should retrieve dashboard data for a new driver with no history', async () => { // Scenario: New driver with minimal data // Given: A newly registered driver exists const driverId = 'new-driver-456'; driverRepository.addDriver({ id: driverId, name: 'New Driver', rating: 1000, rank: 1000, starts: 0, wins: 0, podiums: 0, leagues: 0, }); // And: The driver has no race history // And: The driver has no upcoming races // And: The driver is not in any championships // And: The driver has no recent activity // When: GetDashboardUseCase.execute() is called with driver ID const result = await getDashboardUseCase.execute({ driverId }); // Then: The result should contain basic driver statistics expect(result).toBeDefined(); expect(result.driver.id).toBe(driverId); expect(result.driver.name).toBe('New Driver'); expect(result.statistics.rating).toBe(1000); expect(result.statistics.rank).toBe(1000); expect(result.statistics.starts).toBe(0); expect(result.statistics.wins).toBe(0); expect(result.statistics.podiums).toBe(0); expect(result.statistics.leagues).toBe(0); // And: Upcoming races section should be empty expect(result.upcomingRaces).toHaveLength(0); // And: Championship standings section should be empty expect(result.championshipStandings).toHaveLength(0); // And: Recent activity section should be empty expect(result.recentActivity).toHaveLength(0); // And: EventPublisher should emit DashboardAccessedEvent expect(eventPublisher.getDashboardAccessedEventCount()).toBe(1); }); it('should retrieve dashboard data with upcoming races limited to 3', async () => { // Scenario: Driver with many upcoming races // Given: A driver exists const driverId = 'driver-789'; driverRepository.addDriver({ id: driverId, name: 'Race Driver', rating: 1200, rank: 500, starts: 5, wins: 1, podiums: 2, leagues: 1, }); // And: The driver has 5 upcoming races scheduled raceRepository.addUpcomingRaces(driverId, [ { id: 'race-1', trackName: 'Track A', carType: 'GT3', scheduledDate: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days }, { id: 'race-2', trackName: 'Track B', carType: 'GT3', scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days }, { id: 'race-3', trackName: 'Track C', carType: 'GT3', scheduledDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // 5 days }, { id: 'race-4', trackName: 'Track D', carType: 'GT3', scheduledDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000), // 1 day }, { id: 'race-5', trackName: 'Track E', carType: 'GT3', scheduledDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days }, ]); // When: GetDashboardUseCase.execute() is called with driver ID const result = await getDashboardUseCase.execute({ driverId }); // Then: The result should contain only 3 upcoming races expect(result.upcomingRaces).toHaveLength(3); // And: The races should be sorted by scheduled date (earliest first) expect(result.upcomingRaces[0].trackName).toBe('Track D'); // 1 day expect(result.upcomingRaces[1].trackName).toBe('Track B'); // 2 days expect(result.upcomingRaces[2].trackName).toBe('Track C'); // 5 days // And: EventPublisher should emit DashboardAccessedEvent expect(eventPublisher.getDashboardAccessedEventCount()).toBe(1); }); it('should retrieve dashboard data with championship standings for multiple leagues', async () => { // Scenario: Driver in multiple championships // Given: A driver exists const driverId = 'driver-champ'; driverRepository.addDriver({ id: driverId, name: 'Champion Driver', rating: 1800, rank: 50, starts: 20, wins: 8, podiums: 15, leagues: 3, }); // And: The driver is participating in 3 active championships leagueRepository.addLeagueStandings(driverId, [ { leagueId: 'league-1', leagueName: 'Championship A', position: 3, points: 200, totalDrivers: 25, }, { leagueId: 'league-2', leagueName: 'Championship B', position: 8, points: 120, totalDrivers: 18, }, { leagueId: 'league-3', leagueName: 'Championship C', position: 15, points: 60, totalDrivers: 30, }, ]); // When: GetDashboardUseCase.execute() is called with driver ID const result = await getDashboardUseCase.execute({ driverId }); // Then: The result should contain standings for all 3 leagues expect(result.championshipStandings).toHaveLength(3); // And: Each league should show position, points, and total drivers expect(result.championshipStandings[0].leagueName).toBe('Championship A'); expect(result.championshipStandings[0].position).toBe(3); expect(result.championshipStandings[0].points).toBe(200); expect(result.championshipStandings[0].totalDrivers).toBe(25); expect(result.championshipStandings[1].leagueName).toBe('Championship B'); expect(result.championshipStandings[1].position).toBe(8); expect(result.championshipStandings[1].points).toBe(120); expect(result.championshipStandings[1].totalDrivers).toBe(18); expect(result.championshipStandings[2].leagueName).toBe('Championship C'); expect(result.championshipStandings[2].position).toBe(15); expect(result.championshipStandings[2].points).toBe(60); expect(result.championshipStandings[2].totalDrivers).toBe(30); // And: EventPublisher should emit DashboardAccessedEvent expect(eventPublisher.getDashboardAccessedEventCount()).toBe(1); }); it('should retrieve dashboard data with recent activity sorted by timestamp', async () => { // Scenario: Driver with multiple recent activities // Given: A driver exists const driverId = 'driver-activity'; driverRepository.addDriver({ id: driverId, name: 'Active Driver', rating: 1400, rank: 200, starts: 15, wins: 4, podiums: 8, leagues: 1, }); // And: The driver has 5 recent activities (race results, events) activityRepository.addRecentActivity(driverId, [ { id: 'activity-1', type: 'race_result', description: 'Race 1', timestamp: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // 5 days ago status: 'success', }, { id: 'activity-2', type: 'race_result', description: 'Race 2', timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // 1 day ago status: 'success', }, { id: 'activity-3', type: 'achievement', description: 'Achievement 1', timestamp: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), // 3 days ago status: 'success', }, { id: 'activity-4', type: 'league_invitation', description: 'Invitation', timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // 2 days ago status: 'info', }, { id: 'activity-5', type: 'other', description: 'Other event', timestamp: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000), // 4 days ago status: 'info', }, ]); // When: GetDashboardUseCase.execute() is called with driver ID const result = await getDashboardUseCase.execute({ driverId }); // Then: The result should contain all activities expect(result.recentActivity).toHaveLength(5); // And: Activities should be sorted by timestamp (newest first) expect(result.recentActivity[0].description).toBe('Race 2'); // 1 day ago expect(result.recentActivity[1].description).toBe('Invitation'); // 2 days ago expect(result.recentActivity[2].description).toBe('Achievement 1'); // 3 days ago expect(result.recentActivity[3].description).toBe('Other event'); // 4 days ago expect(result.recentActivity[4].description).toBe('Race 1'); // 5 days ago // And: EventPublisher should emit DashboardAccessedEvent expect(eventPublisher.getDashboardAccessedEventCount()).toBe(1); }); }); describe('GetDashboardUseCase - Edge Cases', () => { it('should handle driver with no upcoming races but has completed races', async () => { // Scenario: Driver with completed races but no upcoming races // Given: A driver exists const driverId = 'driver-no-upcoming'; driverRepository.addDriver({ id: driverId, name: 'Past Driver', rating: 1300, rank: 300, starts: 8, wins: 2, podiums: 4, leagues: 1, }); // And: The driver has completed races in the past // And: The driver has no upcoming races scheduled // When: GetDashboardUseCase.execute() is called with driver ID const result = await getDashboardUseCase.execute({ driverId }); // Then: The result should contain driver statistics from completed races expect(result.statistics.starts).toBe(8); expect(result.statistics.wins).toBe(2); expect(result.statistics.podiums).toBe(4); // And: Upcoming races section should be empty expect(result.upcomingRaces).toHaveLength(0); // And: EventPublisher should emit DashboardAccessedEvent expect(eventPublisher.getDashboardAccessedEventCount()).toBe(1); }); it('should handle driver with upcoming races but no completed races', async () => { // Scenario: Driver with upcoming races but no completed races // Given: A driver exists const driverId = 'driver-no-completed'; driverRepository.addDriver({ id: driverId, name: 'New Racer', rating: 1100, rank: 800, starts: 0, wins: 0, podiums: 0, leagues: 0, }); // And: The driver has upcoming races scheduled raceRepository.addUpcomingRaces(driverId, [ { id: 'race-1', trackName: 'Track A', carType: 'GT3', scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), }, ]); // And: The driver has no completed races // When: GetDashboardUseCase.execute() is called with driver ID const result = await getDashboardUseCase.execute({ driverId }); // Then: The result should contain upcoming races expect(result.upcomingRaces).toHaveLength(1); expect(result.upcomingRaces[0].trackName).toBe('Track A'); // And: Driver statistics should show zeros for wins, podiums, etc. expect(result.statistics.starts).toBe(0); expect(result.statistics.wins).toBe(0); expect(result.statistics.podiums).toBe(0); // And: EventPublisher should emit DashboardAccessedEvent expect(eventPublisher.getDashboardAccessedEventCount()).toBe(1); }); it('should handle driver with championship standings but no recent activity', async () => { // Scenario: Driver in championships but no recent activity // Given: A driver exists const driverId = 'driver-champ-only'; driverRepository.addDriver({ id: driverId, name: 'Champ Only', rating: 1600, rank: 100, starts: 12, wins: 5, podiums: 8, leagues: 2, }); // And: The driver is participating in active championships leagueRepository.addLeagueStandings(driverId, [ { leagueId: 'league-1', leagueName: 'Championship A', position: 10, points: 100, totalDrivers: 20, }, ]); // And: The driver has no recent activity // When: GetDashboardUseCase.execute() is called with driver ID const result = await getDashboardUseCase.execute({ driverId }); // Then: The result should contain championship standings expect(result.championshipStandings).toHaveLength(1); expect(result.championshipStandings[0].leagueName).toBe('Championship A'); // And: Recent activity section should be empty expect(result.recentActivity).toHaveLength(0); // And: EventPublisher should emit DashboardAccessedEvent expect(eventPublisher.getDashboardAccessedEventCount()).toBe(1); }); it('should handle driver with recent activity but no championship standings', async () => { // Scenario: Driver with recent activity but not in championships // Given: A driver exists const driverId = 'driver-activity-only'; driverRepository.addDriver({ id: driverId, name: 'Activity Only', rating: 1250, rank: 400, starts: 6, wins: 1, podiums: 2, leagues: 0, }); // And: The driver has recent activity activityRepository.addRecentActivity(driverId, [ { id: 'activity-1', type: 'race_result', description: 'Finished 5th', timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), status: 'success', }, ]); // And: The driver is not participating in any championships // When: GetDashboardUseCase.execute() is called with driver ID const result = await getDashboardUseCase.execute({ driverId }); // Then: The result should contain recent activity expect(result.recentActivity).toHaveLength(1); expect(result.recentActivity[0].description).toBe('Finished 5th'); // And: Championship standings section should be empty expect(result.championshipStandings).toHaveLength(0); // And: EventPublisher should emit DashboardAccessedEvent expect(eventPublisher.getDashboardAccessedEventCount()).toBe(1); }); it('should handle driver with no data at all', async () => { // TODO: Implement test // Scenario: Driver with absolutely no data // Given: A driver exists // And: The driver has no statistics // And: The driver has no upcoming races // And: The driver has no championship standings // And: The driver has no recent activity // When: GetDashboardUseCase.execute() is called with driver ID // Then: The result should contain basic driver info // And: All sections should be empty or show default values // And: EventPublisher should emit DashboardAccessedEvent }); }); describe('GetDashboardUseCase - Error Handling', () => { it('should throw error when driver does not exist', async () => { // TODO: Implement test // Scenario: Non-existent driver // Given: No driver exists with the given ID // When: GetDashboardUseCase.execute() is called with non-existent driver ID // Then: Should throw DriverNotFoundError // And: EventPublisher should NOT emit any events }); it('should throw error when driver ID is invalid', async () => { // TODO: Implement test // Scenario: Invalid driver ID // Given: An invalid driver ID (e.g., empty string, null, undefined) // When: GetDashboardUseCase.execute() is called with invalid driver ID // Then: Should throw ValidationError // And: EventPublisher should NOT emit any events }); it('should handle repository errors gracefully', async () => { // TODO: Implement test // Scenario: Repository throws error // Given: A driver exists // And: DriverRepository throws an error during query // When: GetDashboardUseCase.execute() is called // Then: Should propagate the error appropriately // And: EventPublisher should NOT emit any events }); }); describe('Dashboard Data Orchestration', () => { it('should correctly calculate driver statistics from race results', async () => { // TODO: Implement test // Scenario: Driver statistics calculation // Given: A driver exists // And: The driver has 10 completed races // And: The driver has 3 wins // And: The driver has 5 podiums // When: GetDashboardUseCase.execute() is called // Then: Driver statistics should show: // - Starts: 10 // - Wins: 3 // - Podiums: 5 // - Rating: Calculated based on performance // - Rank: Calculated based on rating }); it('should correctly format upcoming race time information', async () => { // TODO: Implement test // Scenario: Upcoming race time formatting // Given: A driver exists // And: The driver has an upcoming race scheduled in 2 days 4 hours // When: GetDashboardUseCase.execute() is called // Then: The upcoming race should include: // - Track name // - Car type // - Scheduled date and time // - Time until race (formatted as "2 days 4 hours") }); it('should correctly aggregate championship standings across leagues', async () => { // TODO: Implement test // Scenario: Championship standings aggregation // Given: A driver exists // And: The driver is in 2 championships // And: In Championship A: Position 5, 150 points, 20 drivers // And: In Championship B: Position 12, 85 points, 15 drivers // When: GetDashboardUseCase.execute() is called // Then: Championship standings should show: // - League A: Position 5, 150 points, 20 drivers // - League B: Position 12, 85 points, 15 drivers }); it('should correctly format recent activity with proper status', async () => { // TODO: Implement test // Scenario: Recent activity formatting // Given: A driver exists // And: The driver has a race result (finished 3rd) // And: The driver has a league invitation event // When: GetDashboardUseCase.execute() is called // Then: Recent activity should show: // - Race result: Type "race_result", Status "success", Description "Finished 3rd at Monza" // - League invitation: Type "league_invitation", Status "info", Description "Invited to League XYZ" }); }); });