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

395 lines
18 KiB
TypeScript

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