/** * Integration Test: Dashboard Data Flow * * Tests the complete data flow for dashboard functionality: * 1. Repository queries return correct data * 2. Use case processes and orchestrates data correctly * 3. Presenter transforms data to DTOs * 4. API returns correct response structure * * Focus: Data transformation and flow, 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 { DashboardPresenter } from '../../../core/dashboard/application/presenters/DashboardPresenter'; import { DashboardDTO } from '../../../core/dashboard/application/dto/DashboardDTO'; describe('Dashboard Data Flow Integration', () => { let driverRepository: InMemoryDriverRepository; let raceRepository: InMemoryRaceRepository; let leagueRepository: InMemoryLeagueRepository; let activityRepository: InMemoryActivityRepository; let eventPublisher: InMemoryEventPublisher; let getDashboardUseCase: GetDashboardUseCase; let dashboardPresenter: DashboardPresenter; beforeAll(() => { driverRepository = new InMemoryDriverRepository(); raceRepository = new InMemoryRaceRepository(); leagueRepository = new InMemoryLeagueRepository(); activityRepository = new InMemoryActivityRepository(); eventPublisher = new InMemoryEventPublisher(); getDashboardUseCase = new GetDashboardUseCase({ driverRepository, raceRepository, leagueRepository, activityRepository, eventPublisher, }); dashboardPresenter = new DashboardPresenter(); }); beforeEach(() => { driverRepository.clear(); raceRepository.clear(); leagueRepository.clear(); activityRepository.clear(); eventPublisher.clear(); }); describe('Repository to Use Case Data Flow', () => { it('should correctly flow driver data from repository to use case', async () => { // Scenario: Driver data flow // Given: A driver exists in the repository with specific statistics const driverId = 'driver-flow'; driverRepository.addDriver({ id: driverId, name: 'Flow Driver', rating: 1500, rank: 123, starts: 10, wins: 3, podiums: 5, leagues: 1, }); // And: The driver has rating 1500, rank 123, 10 starts, 3 wins, 5 podiums // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // Then: The use case should retrieve driver data from repository expect(result.driver.id).toBe(driverId); expect(result.driver.name).toBe('Flow Driver'); // And: The use case should calculate derived statistics 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); // And: The result should contain all driver statistics expect(result.statistics.leagues).toBe(1); }); it('should correctly flow race data from repository to use case', async () => { // Scenario: Race data flow // Given: Multiple races exist in the repository const driverId = 'driver-race-flow'; driverRepository.addDriver({ id: driverId, name: 'Race Flow Driver', rating: 1200, rank: 500, starts: 5, wins: 1, podiums: 2, leagues: 1, }); // And: Some races are scheduled for the future raceRepository.addUpcomingRaces(driverId, [ { id: 'race-1', trackName: 'Track A', carType: 'GT3', scheduledDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), }, { id: 'race-2', trackName: 'Track B', carType: 'GT3', scheduledDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000), }, { id: 'race-3', trackName: 'Track C', carType: 'GT3', scheduledDate: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), }, { id: 'race-4', trackName: 'Track D', carType: 'GT3', scheduledDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }, ]); // And: Some races are completed // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // Then: The use case should retrieve upcoming races from repository expect(result.upcomingRaces).toBeDefined(); // And: The use case should limit results to 3 races expect(result.upcomingRaces).toHaveLength(3); // And: The use case should sort races by scheduled date expect(result.upcomingRaces[0].trackName).toBe('Track B'); // 1 day expect(result.upcomingRaces[1].trackName).toBe('Track C'); // 3 days expect(result.upcomingRaces[2].trackName).toBe('Track A'); // 5 days }); it('should correctly flow league data from repository to use case', async () => { // Scenario: League data flow // Given: Multiple leagues exist in the repository const driverId = 'driver-league-flow'; driverRepository.addDriver({ id: driverId, name: 'League Flow Driver', rating: 1400, rank: 200, starts: 12, wins: 4, podiums: 7, leagues: 2, }); // And: The driver is participating in some leagues leagueRepository.addLeagueStandings(driverId, [ { leagueId: 'league-1', leagueName: 'League A', position: 8, points: 120, totalDrivers: 25, }, { leagueId: 'league-2', leagueName: 'League B', position: 3, points: 180, totalDrivers: 15, }, ]); // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // Then: The use case should retrieve league memberships from repository expect(result.championshipStandings).toBeDefined(); // And: The use case should calculate standings for each league expect(result.championshipStandings).toHaveLength(2); // And: The result should contain league name, position, points, and driver count expect(result.championshipStandings[0].leagueName).toBe('League A'); expect(result.championshipStandings[0].position).toBe(8); expect(result.championshipStandings[0].points).toBe(120); expect(result.championshipStandings[0].totalDrivers).toBe(25); }); it('should correctly flow activity data from repository to use case', async () => { // Scenario: Activity data flow // Given: Multiple activities exist in the repository const driverId = 'driver-activity-flow'; driverRepository.addDriver({ id: driverId, name: 'Activity Flow Driver', rating: 1300, rank: 300, starts: 8, wins: 2, podiums: 4, leagues: 1, }); // And: Activities include race results and other events activityRepository.addRecentActivity(driverId, [ { id: 'activity-1', type: 'race_result', description: 'Race result 1', timestamp: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), status: 'success', }, { id: 'activity-2', type: 'achievement', description: 'Achievement 1', timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), status: 'success', }, { id: 'activity-3', type: 'league_invitation', description: 'Invitation', timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), status: 'info', }, ]); // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // Then: The use case should retrieve recent activities from repository expect(result.recentActivity).toBeDefined(); // And: The use case should sort activities by timestamp (newest first) expect(result.recentActivity).toHaveLength(3); expect(result.recentActivity[0].description).toBe('Achievement 1'); // 1 day ago expect(result.recentActivity[1].description).toBe('Invitation'); // 2 days ago expect(result.recentActivity[2].description).toBe('Race result 1'); // 3 days ago // And: The result should contain activity type, description, and timestamp expect(result.recentActivity[0].type).toBe('achievement'); expect(result.recentActivity[0].timestamp).toBeDefined(); }); }); describe('Complete Data Flow: Repository -> Use Case -> Presenter', () => { it('should complete full data flow for driver with all data', async () => { // Scenario: Complete data flow // Given: A driver exists with complete data in repositories const driverId = 'driver-complete-flow'; driverRepository.addDriver({ id: driverId, name: 'Complete Flow Driver', avatar: 'https://example.com/avatar.jpg', rating: 1600, rank: 85, starts: 25, wins: 8, podiums: 15, leagues: 2, }); raceRepository.addUpcomingRaces(driverId, [ { id: 'race-1', trackName: 'Monza', carType: 'GT3', scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), }, { id: 'race-2', trackName: 'Spa', carType: 'GT3', scheduledDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), }, ]); leagueRepository.addLeagueStandings(driverId, [ { leagueId: 'league-1', leagueName: 'Championship A', position: 5, points: 200, totalDrivers: 30, }, ]); activityRepository.addRecentActivity(driverId, [ { id: 'activity-1', type: 'race_result', description: 'Finished 2nd at Monza', timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), status: 'success', }, ]); // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // And: DashboardPresenter.present() is called with the result const dto = dashboardPresenter.present(result); // Then: The final DTO should contain: expect(dto.driver.id).toBe(driverId); expect(dto.driver.name).toBe('Complete Flow Driver'); expect(dto.driver.avatar).toBe('https://example.com/avatar.jpg'); // - Driver statistics (rating, rank, starts, wins, podiums, leagues) expect(dto.statistics.rating).toBe(1600); expect(dto.statistics.rank).toBe(85); expect(dto.statistics.starts).toBe(25); expect(dto.statistics.wins).toBe(8); expect(dto.statistics.podiums).toBe(15); expect(dto.statistics.leagues).toBe(2); // - Upcoming races (up to 3, sorted by date) expect(dto.upcomingRaces).toHaveLength(2); expect(dto.upcomingRaces[0].trackName).toBe('Monza'); // - Championship standings (league name, position, points, driver count) expect(dto.championshipStandings).toHaveLength(1); expect(dto.championshipStandings[0].leagueName).toBe('Championship A'); expect(dto.championshipStandings[0].position).toBe(5); expect(dto.championshipStandings[0].points).toBe(200); expect(dto.championshipStandings[0].totalDrivers).toBe(30); // - Recent activity (type, description, timestamp, status) expect(dto.recentActivity).toHaveLength(1); expect(dto.recentActivity[0].type).toBe('race_result'); expect(dto.recentActivity[0].description).toBe('Finished 2nd at Monza'); expect(dto.recentActivity[0].status).toBe('success'); // And: All data should be correctly transformed and formatted expect(dto.upcomingRaces[0].scheduledDate).toBeDefined(); expect(dto.recentActivity[0].timestamp).toBeDefined(); }); it('should complete full data flow for new driver with no data', async () => { // Scenario: Complete data flow for new driver // Given: A newly registered driver exists with no data const driverId = 'driver-new-flow'; driverRepository.addDriver({ id: driverId, name: 'New Flow Driver', rating: 1000, rank: 1000, starts: 0, wins: 0, podiums: 0, leagues: 0, }); // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // And: DashboardPresenter.present() is called with the result const dto = dashboardPresenter.present(result); // Then: The final DTO should contain: expect(dto.driver.id).toBe(driverId); expect(dto.driver.name).toBe('New Flow Driver'); // - Basic driver statistics (rating, rank, starts, wins, podiums, leagues) expect(dto.statistics.rating).toBe(1000); expect(dto.statistics.rank).toBe(1000); expect(dto.statistics.starts).toBe(0); expect(dto.statistics.wins).toBe(0); expect(dto.statistics.podiums).toBe(0); expect(dto.statistics.leagues).toBe(0); // - Empty upcoming races array expect(dto.upcomingRaces).toHaveLength(0); // - Empty championship standings array expect(dto.championshipStandings).toHaveLength(0); // - Empty recent activity array expect(dto.recentActivity).toHaveLength(0); // And: All fields should have appropriate default values // (already verified by the above checks) }); it('should maintain data consistency across multiple data flows', async () => { // Scenario: Data consistency // Given: A driver exists with data const driverId = 'driver-consistency'; driverRepository.addDriver({ id: driverId, name: 'Consistency Driver', rating: 1350, rank: 250, starts: 10, wins: 3, podiums: 5, leagues: 1, }); raceRepository.addUpcomingRaces(driverId, [ { id: 'race-1', trackName: 'Track A', carType: 'GT3', scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), }, ]); // When: GetDashboardUseCase.execute() is called multiple times const result1 = await getDashboardUseCase.execute({ driverId }); const result2 = await getDashboardUseCase.execute({ driverId }); const result3 = await getDashboardUseCase.execute({ driverId }); // And: DashboardPresenter.present() is called for each result const dto1 = dashboardPresenter.present(result1); const dto2 = dashboardPresenter.present(result2); const dto3 = dashboardPresenter.present(result3); // Then: All DTOs should be identical expect(dto1).toEqual(dto2); expect(dto2).toEqual(dto3); // And: Data should remain consistent across calls expect(dto1.driver.name).toBe('Consistency Driver'); expect(dto1.statistics.rating).toBe(1350); expect(dto1.upcomingRaces).toHaveLength(1); }); }); describe('Data Transformation Edge Cases', () => { it('should handle driver with maximum upcoming races', async () => { // Scenario: Maximum upcoming races // Given: A driver exists const driverId = 'driver-max-races'; driverRepository.addDriver({ id: driverId, name: 'Max Races Driver', rating: 1200, rank: 500, starts: 5, wins: 1, podiums: 2, leagues: 1, }); // And: The driver has 10 upcoming races scheduled raceRepository.addUpcomingRaces(driverId, [ { id: 'race-1', trackName: 'Track A', carType: 'GT3', scheduledDate: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000) }, { id: 'race-2', trackName: 'Track B', carType: 'GT3', scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000) }, { id: 'race-3', trackName: 'Track C', carType: 'GT3', scheduledDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000) }, { id: 'race-4', trackName: 'Track D', carType: 'GT3', scheduledDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000) }, { id: 'race-5', trackName: 'Track E', carType: 'GT3', scheduledDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) }, { id: 'race-6', trackName: 'Track F', carType: 'GT3', scheduledDate: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000) }, { id: 'race-7', trackName: 'Track G', carType: 'GT3', scheduledDate: new Date(Date.now() + 8 * 24 * 60 * 60 * 1000) }, { id: 'race-8', trackName: 'Track H', carType: 'GT3', scheduledDate: new Date(Date.now() + 4 * 24 * 60 * 60 * 1000) }, { id: 'race-9', trackName: 'Track I', carType: 'GT3', scheduledDate: new Date(Date.now() + 6 * 24 * 60 * 60 * 1000) }, { id: 'race-10', trackName: 'Track J', carType: 'GT3', scheduledDate: new Date(Date.now() + 9 * 24 * 60 * 60 * 1000) }, ]); // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // And: DashboardPresenter.present() is called const dto = dashboardPresenter.present(result); // Then: The DTO should contain exactly 3 upcoming races expect(dto.upcomingRaces).toHaveLength(3); // And: The races should be the 3 earliest scheduled races expect(dto.upcomingRaces[0].trackName).toBe('Track D'); // 1 day expect(dto.upcomingRaces[1].trackName).toBe('Track B'); // 2 days expect(dto.upcomingRaces[2].trackName).toBe('Track F'); // 3 days }); it('should handle driver with many championship standings', async () => { // Scenario: Many championship standings // Given: A driver exists const driverId = 'driver-many-standings'; driverRepository.addDriver({ id: driverId, name: 'Many Standings Driver', rating: 1400, rank: 200, starts: 12, wins: 4, podiums: 7, leagues: 5, }); // And: The driver is participating in 5 championships leagueRepository.addLeagueStandings(driverId, [ { leagueId: 'league-1', leagueName: 'Championship A', position: 8, points: 120, totalDrivers: 25, }, { leagueId: 'league-2', leagueName: 'Championship B', position: 3, points: 180, totalDrivers: 15, }, { leagueId: 'league-3', leagueName: 'Championship C', position: 12, points: 95, totalDrivers: 30, }, { leagueId: 'league-4', leagueName: 'Championship D', position: 1, points: 250, totalDrivers: 20, }, { leagueId: 'league-5', leagueName: 'Championship E', position: 5, points: 160, totalDrivers: 18, }, ]); // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // And: DashboardPresenter.present() is called const dto = dashboardPresenter.present(result); // Then: The DTO should contain standings for all 5 championships expect(dto.championshipStandings).toHaveLength(5); // And: Each standing should have correct data expect(dto.championshipStandings[0].leagueName).toBe('Championship A'); expect(dto.championshipStandings[0].position).toBe(8); expect(dto.championshipStandings[0].points).toBe(120); expect(dto.championshipStandings[0].totalDrivers).toBe(25); expect(dto.championshipStandings[1].leagueName).toBe('Championship B'); expect(dto.championshipStandings[1].position).toBe(3); expect(dto.championshipStandings[1].points).toBe(180); expect(dto.championshipStandings[1].totalDrivers).toBe(15); expect(dto.championshipStandings[2].leagueName).toBe('Championship C'); expect(dto.championshipStandings[2].position).toBe(12); expect(dto.championshipStandings[2].points).toBe(95); expect(dto.championshipStandings[2].totalDrivers).toBe(30); expect(dto.championshipStandings[3].leagueName).toBe('Championship D'); expect(dto.championshipStandings[3].position).toBe(1); expect(dto.championshipStandings[3].points).toBe(250); expect(dto.championshipStandings[3].totalDrivers).toBe(20); expect(dto.championshipStandings[4].leagueName).toBe('Championship E'); expect(dto.championshipStandings[4].position).toBe(5); expect(dto.championshipStandings[4].points).toBe(160); expect(dto.championshipStandings[4].totalDrivers).toBe(18); }); it('should handle driver with many recent activities', async () => { // Scenario: Many recent activities // Given: A driver exists const driverId = 'driver-many-activities'; driverRepository.addDriver({ id: driverId, name: 'Many Activities Driver', rating: 1300, rank: 300, starts: 8, wins: 2, podiums: 4, leagues: 1, }); // And: The driver has 20 recent activities const activities = []; for (let i = 0; i < 20; i++) { activities.push({ id: `activity-${i}`, type: i % 2 === 0 ? 'race_result' : 'achievement', description: `Activity ${i}`, timestamp: new Date(Date.now() - i * 60 * 60 * 1000), // each activity 1 hour apart status: i % 3 === 0 ? 'success' : i % 3 === 1 ? 'info' : 'warning', }); } activityRepository.addRecentActivity(driverId, activities); // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // And: DashboardPresenter.present() is called const dto = dashboardPresenter.present(result); // Then: The DTO should contain all 20 activities expect(dto.recentActivity).toHaveLength(20); // And: Activities should be sorted by timestamp (newest first) for (let i = 0; i < 20; i++) { expect(dto.recentActivity[i].description).toBe(`Activity ${i}`); expect(dto.recentActivity[i].timestamp).toBeDefined(); } }); it('should handle driver with mixed race statuses', async () => { // Scenario: Mixed race statuses // Given: A driver exists with statistics reflecting completed races const driverId = 'driver-mixed-statuses'; driverRepository.addDriver({ id: driverId, name: 'Mixed Statuses Driver', rating: 1500, rank: 100, starts: 5, // only completed races count wins: 2, podiums: 3, leagues: 1, }); // And: The driver has scheduled races (upcoming) raceRepository.addUpcomingRaces(driverId, [ { id: 'race-scheduled-1', trackName: 'Track A', carType: 'GT3', scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), }, { id: 'race-scheduled-2', trackName: 'Track B', carType: 'GT3', scheduledDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), }, ]); // Note: Cancelled races are not stored in the repository, so they won't appear // When: GetDashboardUseCase.execute() is called const result = await getDashboardUseCase.execute({ driverId }); // And: DashboardPresenter.present() is called const dto = dashboardPresenter.present(result); // Then: Driver statistics should only count completed races expect(dto.statistics.starts).toBe(5); expect(dto.statistics.wins).toBe(2); expect(dto.statistics.podiums).toBe(3); // And: Upcoming races should only include scheduled races expect(dto.upcomingRaces).toHaveLength(2); expect(dto.upcomingRaces[0].trackName).toBe('Track A'); expect(dto.upcomingRaces[1].trackName).toBe('Track B'); // And: Cancelled races should not appear in any section // (they are not in upcoming races, and we didn't add them to activities) expect(dto.upcomingRaces.some(r => r.trackName.includes('Cancelled'))).toBe(false); }); }); });