/** * Integration Test: League Standings Data Flow * * Tests the complete data flow from database to API response for league standings: * 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 Standings - 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 standings DTO structure from API', async () => { // Setup: Create test data const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Test League' }); const season = await factory.createSeason(league.id.toString()); // Create drivers const driver1 = await factory.createDriver({ name: 'John Doe', country: 'US' }); const driver2 = await factory.createDriver({ name: 'Jane Smith', country: 'UK' }); const driver3 = await factory.createDriver({ name: 'Bob Johnson', country: 'CA' }); // Create races with results const race1 = await factory.createRace({ leagueId: league.id.toString(), track: 'Laguna Seca', car: 'Formula Ford', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // Past race status: 'completed' }); const race2 = await factory.createRace({ leagueId: league.id.toString(), track: 'Road Atlanta', car: 'Formula Ford', scheduledAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), // Past race status: 'completed' }); // Create results for race 1 await factory.createResult(race1.id.toString(), driver1.id.toString(), { position: 1, fastestLap: 95000, incidents: 0, startPosition: 2 }); await factory.createResult(race1.id.toString(), driver2.id.toString(), { position: 2, fastestLap: 95500, incidents: 1, startPosition: 1 }); await factory.createResult(race1.id.toString(), driver3.id.toString(), { position: 3, fastestLap: 96000, incidents: 2, startPosition: 3 }); // Create results for race 2 await factory.createResult(race2.id.toString(), driver1.id.toString(), { position: 2, fastestLap: 94500, incidents: 1, startPosition: 1 }); await factory.createResult(race2.id.toString(), driver2.id.toString(), { position: 1, fastestLap: 94000, incidents: 0, startPosition: 3 }); await factory.createResult(race2.id.toString(), driver3.id.toString(), { position: 3, fastestLap: 95000, incidents: 1, startPosition: 2 }); // Execute: Call API endpoint const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/standings`); // Verify: API response structure expect(response).toBeDefined(); expect(response.standings).toBeDefined(); expect(Array.isArray(response.standings)).toBe(true); // Verify: Each standing has correct DTO structure for (const standing of response.standings) { expect(standing).toHaveProperty('driverId'); expect(standing).toHaveProperty('driver'); expect(standing).toHaveProperty('points'); expect(standing).toHaveProperty('position'); expect(standing).toHaveProperty('wins'); expect(standing).toHaveProperty('podiums'); expect(standing).toHaveProperty('races'); expect(standing).toHaveProperty('positionChange'); expect(standing).toHaveProperty('lastRacePoints'); expect(standing).toHaveProperty('droppedRaceIds'); // Verify driver DTO structure expect(standing.driver).toHaveProperty('id'); expect(standing.driver).toHaveProperty('iracingId'); expect(standing.driver).toHaveProperty('name'); expect(standing.driver).toHaveProperty('country'); expect(standing.driver).toHaveProperty('joinedAt'); } }); it('should return empty standings for league with no results', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Empty League' }); const season = await factory.createSeason(league.id.toString()); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/standings`); expect(response.standings).toEqual([]); }); it('should handle standings with single driver', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Single Driver 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: 'Laguna Seca', car: 'Formula Ford', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), status: 'completed' }); await factory.createResult(race.id.toString(), driver.id.toString(), { position: 1, fastestLap: 95000, incidents: 0, startPosition: 1 }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/standings`); expect(response.standings).toHaveLength(1); expect(response.standings[0].driver.name).toBe('Solo Driver'); expect(response.standings[0].position).toBe(1); }); }); describe('End-to-End Data Flow', () => { it('should correctly calculate standings from race results', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Calculation Test League' }); const season = await factory.createSeason(league.id.toString()); // Create 3 drivers 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 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' }), ]); // Create results with specific points to verify calculation // Standard scoring: 1st=25, 2nd=18, 3rd=15 // Race 1: A=1st, B=2nd, C=3rd 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 }); // Race 2: B=1st, C=2nd, A=3rd await factory.createResult(races[1].id.toString(), drivers[1].id.toString(), { position: 1, fastestLap: 89500, incidents: 0, startPosition: 3 }); await factory.createResult(races[1].id.toString(), drivers[2].id.toString(), { position: 2, fastestLap: 90000, incidents: 1, startPosition: 1 }); await factory.createResult(races[1].id.toString(), drivers[0].id.toString(), { position: 3, fastestLap: 90500, incidents: 2, startPosition: 2 }); // Race 3: C=1st, A=2nd, B=3rd await factory.createResult(races[2].id.toString(), drivers[2].id.toString(), { position: 1, fastestLap: 89000, incidents: 0, startPosition: 2 }); await factory.createResult(races[2].id.toString(), drivers[0].id.toString(), { position: 2, fastestLap: 89500, incidents: 1, startPosition: 3 }); await factory.createResult(races[2].id.toString(), drivers[1].id.toString(), { position: 3, fastestLap: 90000, incidents: 2, startPosition: 1 }); // Race 4: A=1st, B=2nd, C=3rd await factory.createResult(races[3].id.toString(), drivers[0].id.toString(), { position: 1, fastestLap: 88500, incidents: 0, startPosition: 1 }); await factory.createResult(races[3].id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 89000, incidents: 1, startPosition: 2 }); await factory.createResult(races[3].id.toString(), drivers[2].id.toString(), { position: 3, fastestLap: 89500, incidents: 2, startPosition: 3 }); // Race 5: B=1st, C=2nd, A=3rd await factory.createResult(races[4].id.toString(), drivers[1].id.toString(), { position: 1, fastestLap: 88000, incidents: 0, startPosition: 3 }); await factory.createResult(races[4].id.toString(), drivers[2].id.toString(), { position: 2, fastestLap: 88500, incidents: 1, startPosition: 1 }); await factory.createResult(races[4].id.toString(), drivers[0].id.toString(), { position: 3, fastestLap: 89000, incidents: 2, startPosition: 2 }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/standings`); // Expected points: // Driver A: 25 + 15 + 18 + 25 + 15 = 98 // Driver B: 18 + 25 + 15 + 18 + 25 = 101 // Driver C: 15 + 18 + 25 + 15 + 18 = 91 expect(response.standings).toHaveLength(3); // Find drivers in response const standingA = response.standings.find(s => s.driver.name === 'Driver A'); const standingB = response.standings.find(s => s.driver.name === 'Driver B'); const standingC = response.standings.find(s => s.driver.name === 'Driver C'); expect(standingA).toBeDefined(); expect(standingB).toBeDefined(); expect(standingC).toBeDefined(); // Verify positions (B should be 1st, A 2nd, C 3rd) expect(standingB?.position).toBe(1); expect(standingA?.position).toBe(2); expect(standingC?.position).toBe(3); // Verify race counts expect(standingA?.races).toBe(5); expect(standingB?.races).toBe(5); expect(standingC?.races).toBe(5); // Verify win counts expect(standingA?.wins).toBe(2); // Races 1 and 4 expect(standingB?.wins).toBe(2); // Races 2 and 5 expect(standingC?.wins).toBe(1); // Race 3 // Verify podium counts expect(standingA?.podiums).toBe(5); // All races expect(standingB?.podiums).toBe(5); // All races expect(standingC?.podiums).toBe(5); // All races }); it('should handle standings with tied points correctly', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Tie Test League' }); const season = await factory.createSeason(league.id.toString()); const drivers = await Promise.all([ factory.createDriver({ name: 'Driver X', iracingId: '2001' }), factory.createDriver({ name: 'Driver Y', iracingId: '2002' }), ]); const race1 = await factory.createRace({ leagueId: league.id.toString(), track: 'Track A', car: 'Car A', scheduledAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000), status: 'completed' }); const race2 = await factory.createRace({ leagueId: league.id.toString(), track: 'Track B', car: 'Car A', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), status: 'completed' }); // Both drivers get same points: 25 + 18 = 43 await factory.createResult(race1.id.toString(), drivers[0].id.toString(), { position: 1, fastestLap: 90000, incidents: 0, startPosition: 1 }); await factory.createResult(race1.id.toString(), drivers[1].id.toString(), { position: 2, fastestLap: 90500, incidents: 1, startPosition: 2 }); await factory.createResult(race2.id.toString(), drivers[0].id.toString(), { position: 2, fastestLap: 89500, incidents: 1, startPosition: 2 }); await factory.createResult(race2.id.toString(), drivers[1].id.toString(), { position: 1, fastestLap: 89000, incidents: 0, startPosition: 1 }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/standings`); expect(response.standings).toHaveLength(2); // Both should have same points expect(response.standings[0].points).toBe(43); expect(response.standings[1].points).toBe(43); // Positions should be 1 and 2 (tie-breaker logic may vary) const positions = response.standings.map(s => s.position).sort(); expect(positions).toEqual([1, 2]); }); }); describe('Data Consistency', () => { it('should maintain data consistency across multiple API calls', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Consistency Test 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}/standings`); const response2 = await api.get(`/leagues/${league.id}/standings`); const response3 = await api.get(`/leagues/${league.id}/standings`); // All responses should be identical expect(response1).toEqual(response2); expect(response2).toEqual(response3); // Verify data integrity expect(response1.standings).toHaveLength(1); expect(response1.standings[0].driver.name).toBe('Consistent Driver'); expect(response1.standings[0].points).toBeGreaterThan(0); }); it('should handle edge case: league with many drivers and races', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Large 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 (random but consistent positions) 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}/standings`); // Should have all 10 drivers expect(response.standings).toHaveLength(10); // All drivers should have 10 races for (const standing of response.standings) { expect(standing.races).toBe(10); expect(standing.driver).toBeDefined(); expect(standing.driver.id).toBeDefined(); expect(standing.driver.name).toBeDefined(); } // Positions should be unique 1-10 const positions = response.standings.map(s => s.position).sort((a, b) => a - b); expect(positions).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); }); it('should handle missing fields gracefully', async () => { const factory = harness.getFactory(); const league = await factory.createLeague({ name: 'Edge Case League' }); const season = await factory.createSeason(league.id.toString()); // Create driver without bio (should be optional) const driver = await factory.createDriver({ name: 'Test Driver', country: 'US' }); const race = await factory.createRace({ leagueId: league.id.toString(), track: 'Test Track', car: 'Test Car', scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), status: 'completed' }); await factory.createResult(race.id.toString(), driver.id.toString(), { position: 1, fastestLap: 90000, incidents: 0, startPosition: 1 }); const api = harness.getApi(); const response = await api.get(`/leagues/${league.id}/standings`); expect(response.standings).toHaveLength(1); expect(response.standings[0].driver.bio).toBeUndefined(); // Optional field expect(response.standings[0].driver.name).toBe('Test Driver'); expect(response.standings[0].driver.country).toBe('US'); }); }); });