/** * Integration Test: League Stats Data Flow * * Tests the complete data flow from database to API response for league stats: * 1. Database query returns correct data * 2. Use case processes the data correctly * 3. Presenter transforms data to DTOs * 4. API returns correct response */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { IntegrationTestHarness, createTestHarness } from '../harness'; describe('League Stats - Data Flow Integration', () => { let harness: IntegrationTestHarness; beforeAll(async () => { harness = createTestHarness(); await harness.beforeAll(); }, 120000); afterAll(async () => { await harness.afterAll(); }, 30000); beforeEach(async () => { await harness.beforeEach(); }); describe('API to View Data Flow', () => { it('should return correct stats DTO structure from API', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Stats Test League' }); const season = await factory.createSeason(league.id.toString()); // Create drivers const drivers = await Promise.all([ factory.createDriver({ name: 'Driver 1', country: 'US' }), factory.createDriver({ name: 'Driver 2', country: 'UK' }), factory.createDriver({ name: 'Driver 3', country: 'CA' }), ]); // Create races const races = await Promise.all([ factory.createRace({ leagueId: league.id.toString(), track: 'Track 1', car: 'Car 1', scheduledAt: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Track 2', car: 'Car 1', scheduledAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Track 3', car: 'Car 1', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), status: 'completed' }), ]); // Create results await factory.createResult(races[0].id.toString(), drivers[0].id.toString(), { position: 1, fastestLap: 90000, incidents: 0, startPosition: 1 }); await factory.createResult(races[0].id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 90500, incidents: 1, startPosition: 2 }); await factory.createResult(races[0].id.toString(), drivers[2].id.toString(), { position: 3, fastestLap: 91000, incidents: 2, startPosition: 3 }); await factory.createResult(races[1].id.toString(), drivers[0].id.toString(), { position: 2, fastestLap: 89500, incidents: 1, startPosition: 2 }); await factory.createResult(races[1].id.toString(), drivers[1].id.toString(), { position: 1, fastestLap: 89000, incidents: 0, startPosition: 3 }); await factory.createResult(races[1].id.toString(), drivers[2].id.toString(), { position: 3, fastestLap: 89500, incidents: 1, startPosition: 1 }); await factory.createResult(races[2].id.toString(), drivers[0].id.toString(), { position: 3, fastestLap: 88500, incidents: 2, startPosition: 3 }); await factory.createResult(races[2].id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 88000, incidents: 1, startPosition: 1 }); await factory.createResult(races[2].id.toString(), drivers[2].id.toString(), { position: 1, fastestLap: 87500, incidents: 0, startPosition: 2 }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/stats`); // Verify: API response structure expect(response).toBeDefined(); expect(response).toHaveProperty('totalRaces'); expect(response).toHaveProperty('totalDrivers'); expect(response).toHaveProperty('totalResults'); expect(response).toHaveProperty('averageIncidentsPerRace'); expect(response).toHaveProperty('mostCommonTrack'); expect(response).toHaveProperty('mostCommonCar'); expect(response).toHaveProperty('topPerformers'); expect(Array.isArray(response.topPerformers)).toBe(true); // Verify: Top performers structure for (const performer of response.topPerformers) { expect(performer).toHaveProperty('driverId'); expect(performer).toHaveProperty('driver'); expect(performer).toHaveProperty('points'); expect(performer).toHaveProperty('wins'); expect(performer).toHaveProperty('podiums'); expect(performer).toHaveProperty('races'); } }); it('should return empty stats for league with no data', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Empty Stats League' }); const season = await factory.createSeason(league.id.toString()); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/stats`); expect(response.totalRaces).toBe(0); expect(response.totalDrivers).toBe(0); expect(response.totalResults).toBe(0); expect(response.topPerformers).toEqual([]); }); it('should handle stats with single race', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Single Race Stats League' }); const season = await factory.createSeason(league.id.toString()); const driver = await factory.createDriver({ name: 'Solo Driver', country: 'US' }); const race = await factory.createRace({ leagueId: league.id.toString(), track: 'Monza', car: 'GT3', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), status: 'completed' }); await factory.createResult(race.id.toString(), driver.id.toString(), { position: 1, fastestLap: 85000, incidents: 0, startPosition: 1 }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/stats`); expect(response.totalRaces).toBe(1); expect(response.totalDrivers).toBe(1); expect(response.totalResults).toBe(1); expect(response.topPerformers).toHaveLength(1); expect(response.topPerformers[0].driver.name).toBe('Solo Driver'); }); }); describe('End-to-End Data Flow', () => { it('should correctly calculate stats from race results', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Calculation Stats League' }); const season = await factory.createSeason(league.id.toString()); const drivers = await Promise.all([ factory.createDriver({ name: 'Driver A', iracingId: '1001' }), factory.createDriver({ name: 'Driver B', iracingId: '1002' }), factory.createDriver({ name: 'Driver C', iracingId: '1003' }), ]); // Create 5 races with different tracks and cars const races = await Promise.all([ factory.createRace({ leagueId: league.id.toString(), track: 'Laguna Seca', car: 'Formula Ford', scheduledAt: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Road Atlanta', car: 'Formula Ford', scheduledAt: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Laguna Seca', car: 'Formula Ford', scheduledAt: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Nürburgring', car: 'GT3', scheduledAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Road Atlanta', car: 'GT3', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), status: 'completed' }), ]); // Create results with specific incidents // Race 1: Laguna Seca, Formula Ford await factory.createResult(races[0].id.toString(), drivers[0].id.toString(), { position: 1, fastestLap: 95000, incidents: 0, startPosition: 1 }); await factory.createResult(races[0].id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 95500, incidents: 1, startPosition: 2 }); await factory.createResult(races[0].id.toString(), drivers[2].id.toString(), { position: 3, fastestLap: 96000, incidents: 2, startPosition: 3 }); // Race 2: Road Atlanta, Formula Ford await factory.createResult(races[1].id.toString(), drivers[0].id.toString(), { position: 2, fastestLap: 94500, incidents: 1, startPosition: 2 }); await factory.createResult(races[1].id.toString(), drivers[1].id.toString(), { position: 1, fastestLap: 94000, incidents: 0, startPosition: 3 }); await factory.createResult(races[1].id.toString(), drivers[2].id.toString(), { position: 3, fastestLap: 95000, incidents: 1, startPosition: 1 }); // Race 3: Laguna Seca, Formula Ford await factory.createResult(races[2].id.toString(), drivers[0].id.toString(), { position: 1, fastestLap: 93500, incidents: 0, startPosition: 1 }); await factory.createResult(races[2].id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 94000, incidents: 1, startPosition: 2 }); await factory.createResult(races[2].id.toString(), drivers[2].id.toString(), { position: 3, fastestLap: 94500, incidents: 2, startPosition: 3 }); // Race 4: Nürburgring, GT3 await factory.createResult(races[3].id.toString(), drivers[0].id.toString(), { position: 3, fastestLap: 92500, incidents: 2, startPosition: 3 }); await factory.createResult(races[3].id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 92000, incidents: 1, startPosition: 1 }); await factory.createResult(races[3].id.toString(), drivers[2].id.toString(), { position: 1, fastestLap: 91500, incidents: 0, startPosition: 2 }); // Race 5: Road Atlanta, GT3 await factory.createResult(races[4].id.toString(), drivers[0].id.toString(), { position: 2, fastestLap: 91000, incidents: 1, startPosition: 2 }); await factory.createResult(races[4].id.toString(), drivers[1].id.toString(), { position: 1, fastestLap: 90500, incidents: 0, startPosition: 3 }); await factory.createResult(races[4].id.toString(), drivers[2].id.toString(), { position: 3, fastestLap: 91500, incidents: 1, startPosition: 1 }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/stats`); // Verify calculated stats expect(response.totalRaces).toBe(5); expect(response.totalDrivers).toBe(3); expect(response.totalResults).toBe(15); // Verify average incidents per race // Total incidents: 0+1+2 + 1+0+1 + 0+1+2 + 2+1+0 + 1+0+1 = 15 // Average: 15 / 5 = 3 expect(response.averageIncidentsPerRace).toBe(3); // Verify most common track (Laguna Seca appears 2 times, Road Atlanta 2 times, Nürburgring 1 time) // Should return one of the most common tracks expect(['Laguna Seca', 'Road Atlanta']).toContain(response.mostCommonTrack); // Verify most common car (Formula Ford appears 3 times, GT3 appears 2 times) expect(response.mostCommonCar).toBe('Formula Ford'); // Verify top performers expect(response.topPerformers).toHaveLength(3); // Find drivers in response const performerA = response.topPerformers.find(p => p.driver.name === 'Driver A'); const performerB = response.topPerformers.find(p => p.driver.name === 'Driver B'); const performerC = response.topPerformers.find(p => p.driver.name === 'Driver C'); expect(performerA).toBeDefined(); expect(performerB).toBeDefined(); expect(performerC).toBeDefined(); // Verify race counts expect(performerA?.races).toBe(5); expect(performerB?.races).toBe(5); expect(performerC?.races).toBe(5); // Verify win counts expect(performerA?.wins).toBe(2); // Races 1 and 3 expect(performerB?.wins).toBe(2); // Races 2 and 5 expect(performerC?.wins).toBe(1); // Race 4 // Verify podium counts expect(performerA?.podiums).toBe(5); // All races expect(performerB?.podiums).toBe(5); // All races expect(performerC?.podiums).toBe(5); // All races }); it('should handle stats with varying race counts per driver', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Varying Races League' }); const season = await factory.createSeason(league.id.toString()); const drivers = await Promise.all([ factory.createDriver({ name: 'Full Timer', iracingId: '2001' }), factory.createDriver({ name: 'Part Timer', iracingId: '2002' }), factory.createDriver({ name: 'One Race', iracingId: '2003' }), ]); // Create 5 races const races = await Promise.all([ factory.createRace({ leagueId: league.id.toString(), track: 'Track 1', car: 'Car 1', scheduledAt: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Track 2', car: 'Car 1', scheduledAt: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Track 3', car: 'Car 1', scheduledAt: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Track 4', car: 'Car 1', scheduledAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000), status: 'completed' }), factory.createRace({ leagueId: league.id.toString(), track: 'Track 5', car: 'Car 1', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), status: 'completed' }), ]); // Full Timer: all 5 races for (let i = 0; i < 5; i++) { await factory.createResult(races[i].id.toString(), drivers[0].id.toString(), { position: 1, fastestLap: 90000 + i * 100, incidents: i % 2, startPosition: 1 }); } // Part Timer: 3 races (1, 2, 4) await factory.createResult(races[0].id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 90500, incidents: 1, startPosition: 2 }); await factory.createResult(races[1].id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 90600, incidents: 1, startPosition: 2 }); await factory.createResult(races[3].id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 90800, incidents: 1, startPosition: 2 }); // One Race: only race 5 await factory.createResult(races[4].id.toString(), drivers[2].id.toString(), { position: 2, fastestLap: 90900, incidents: 0, startPosition: 2 }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/stats`); expect(response.totalRaces).toBe(5); expect(response.totalDrivers).toBe(3); expect(response.totalResults).toBe(9); // 5 + 3 + 1 // Verify top performers have correct race counts const fullTimer = response.topPerformers.find(p => p.driver.name === 'Full Timer'); const partTimer = response.topPerformers.find(p => p.driver.name === 'Part Timer'); const oneRace = response.topPerformers.find(p => p.driver.name === 'One Race'); expect(fullTimer?.races).toBe(5); expect(partTimer?.races).toBe(3); expect(oneRace?.races).toBe(1); }); }); describe('Data Consistency', () => { it('should maintain data consistency across multiple API calls', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Consistency Stats League' }); const season = await factory.createSeason(league.id.toString()); const driver = await factory.createDriver({ name: 'Consistent Driver', country: 'DE' }); const race = await factory.createRace({ leagueId: league.id.toString(), track: 'Nürburgring', car: 'GT3', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), status: 'completed' }); await factory.createResult(race.id.toString(), driver.id.toString(), { position: 1, fastestLap: 85000, incidents: 0, startPosition: 1 }); const api = harness.getApi(); // Make multiple calls const response1 = await api.get(`/leagues/${league.id}/stats`); const response2 = await api.get(`/leagues/${league.id}/stats`); const response3 = await api.get(`/leagues/${league.id}/stats`); // All responses should be identical expect(response1).toEqual(response2); expect(response2).toEqual(response3); // Verify data integrity expect(response1.totalRaces).toBe(1); expect(response1.totalDrivers).toBe(1); expect(response1.totalResults).toBe(1); expect(response1.topPerformers).toHaveLength(1); expect(response1.topPerformers[0].driver.name).toBe('Consistent Driver'); }); it('should handle edge case: league with many races and drivers', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Large Stats League' }); const season = await factory.createSeason(league.id.toString()); // Create 10 drivers const drivers = await Promise.all( Array.from({ length: 10 }, (_, i) => factory.createDriver({ name: `Driver ${i + 1}`, iracingId: `${3000 + i}` }) ) ); // Create 10 races const races = await Promise.all( Array.from({ length: 10 }, (_, i) => factory.createRace({ leagueId: league.id.toString(), track: `Track ${i + 1}`, car: 'GT3', scheduledAt: new Date(Date.now() - (10 - i) * 24 * 60 * 60 * 1000), status: 'completed' }) ) ); // Create results for each race (all drivers participate) for (let raceIndex = 0; raceIndex < 10; raceIndex++) { for (let driverIndex = 0; driverIndex < 10; driverIndex++) { const position = ((driverIndex + raceIndex) % 10) + 1; await factory.createResult( races[raceIndex].id.toString(), drivers[driverIndex].id.toString(), { position, fastestLap: 85000 + (position * 100), incidents: position % 3, startPosition: position } ); } } const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/stats`); // Should have correct totals expect(response.totalRaces).toBe(10); expect(response.totalDrivers).toBe(10); expect(response.totalResults).toBe(100); // 10 races * 10 drivers // Should have 10 top performers (one per driver) expect(response.topPerformers).toHaveLength(10); // All top performers should have 10 races for (const performer of response.topPerformers) { expect(performer.races).toBe(10); expect(performer.driver).toBeDefined(); expect(performer.driver.id).toBeDefined(); expect(performer.driver.name).toBeDefined(); } }); it('should handle edge case: league with no completed races', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'No Completed Races League' }); const season = await factory.createSeason(league.id.toString()); const driver = await factory.createDriver({ name: 'Waiting Driver', country: 'US' }); // Create only scheduled races (no completed races) const race = await factory.createRace({ leagueId: league.id.toString(), track: 'Future Track', car: 'Future Car', scheduledAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), status: 'scheduled' }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/stats`); // Should have 0 stats since no completed races expect(response.totalRaces).toBe(0); expect(response.totalDrivers).toBe(0); expect(response.totalResults).toBe(0); expect(response.topPerformers).toEqual([]); }); it('should handle edge case: league with mixed race statuses', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Mixed Status League' }); const season = await factory.createSeason(league.id.toString()); const driver = await factory.createDriver({ name: 'Mixed Driver', country: 'US' }); // Create races with different statuses const completedRace = await factory.createRace({ leagueId: league.id.toString(), track: 'Completed Track', car: 'Completed Car', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), status: 'completed' }); const scheduledRace = await factory.createRace({ leagueId: league.id.toString(), track: 'Scheduled Track', car: 'Scheduled Car', scheduledAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), status: 'scheduled' }); const inProgressRace = await factory.createRace({ leagueId: league.id.toString(), track: 'In Progress Track', car: 'In Progress Car', scheduledAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), status: 'in_progress' }); // Add result only to completed race await factory.createResult(completedRace.id.toString(), driver.id.toString(), { position: 1, fastestLap: 85000, incidents: 0, startPosition: 1 }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/stats`); // Should only count completed races expect(response.totalRaces).toBe(1); expect(response.totalDrivers).toBe(1); expect(response.totalResults).toBe(1); expect(response.topPerformers).toHaveLength(1); expect(response.topPerformers[0].driver.name).toBe('Mixed Driver'); }); }); });