Files
gridpilot.gg/tests/integration/league/stats-data-flow.integration.test.ts
2026-01-21 22:36:01 +01:00

493 lines
22 KiB
TypeScript

/**
* 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');
});
});
});