diff --git a/tests/integration/league/members-data-flow.integration.test.ts b/tests/integration/league/members-data-flow.integration.test.ts deleted file mode 100644 index 5cfcff62c..000000000 --- a/tests/integration/league/members-data-flow.integration.test.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * Integration Test: League Members Data Flow - * - * Tests the complete data flow from database to API response for league members: - * 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 Members - 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 members DTO structure from API', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Members Test League' }); - const season = await factory.createSeason(league.id.toString()); - - // Create drivers - const drivers = await Promise.all([ - factory.createDriver({ name: 'Owner Driver', country: 'US' }), - factory.createDriver({ name: 'Admin Driver', country: 'UK' }), - factory.createDriver({ name: 'Member Driver', country: 'CA' }), - ]); - - // Create league memberships (simulated via database) - // Note: In real implementation, memberships would be created through the domain - // For this test, we'll verify the API response structure - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/memberships`); - - // Verify: API response structure - expect(response).toBeDefined(); - expect(response.memberships).toBeDefined(); - expect(Array.isArray(response.memberships)).toBe(true); - - // Verify: Each membership has correct DTO structure - for (const membership of response.memberships) { - expect(membership).toHaveProperty('driverId'); - expect(membership).toHaveProperty('driver'); - expect(membership).toHaveProperty('role'); - expect(membership).toHaveProperty('status'); - expect(membership).toHaveProperty('joinedAt'); - - // Verify driver DTO structure - expect(membership.driver).toHaveProperty('id'); - expect(membership.driver).toHaveProperty('iracingId'); - expect(membership.driver).toHaveProperty('name'); - expect(membership.driver).toHaveProperty('country'); - expect(membership.driver).toHaveProperty('joinedAt'); - } - }); - - it('should return empty members for league with no members', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Empty Members League' }); - const season = await factory.createSeason(league.id.toString()); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/memberships`); - - expect(response.memberships).toEqual([]); - }); - - it('should handle league with single member', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Single Member League' }); - const season = await factory.createSeason(league.id.toString()); - - const driver = await factory.createDriver({ name: 'Solo Member', country: 'US' }); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/memberships`); - - // Should have at least the owner - expect(response.memberships.length).toBeGreaterThan(0); - - const soloMember = response.memberships.find(m => m.driver.name === 'Solo Member'); - expect(soloMember).toBeDefined(); - expect(soloMember?.role).toBeDefined(); - expect(soloMember?.status).toBeDefined(); - }); - }); - - describe('End-to-End Data Flow', () => { - it('should correctly transform member data to DTO', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Transformation Members League' }); - const season = await factory.createSeason(league.id.toString()); - - const drivers = await Promise.all([ - factory.createDriver({ name: 'Owner', country: 'US', iracingId: '1001' }), - factory.createDriver({ name: 'Admin', country: 'UK', iracingId: '1002' }), - factory.createDriver({ name: 'Member', country: 'CA', iracingId: '1003' }), - ]); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/memberships`); - - // Verify all drivers are in the response - expect(response.memberships.length).toBeGreaterThanOrEqual(3); - - // Verify each driver has correct data - for (const driver of drivers) { - const membership = response.memberships.find(m => m.driver.name === driver.name.toString()); - expect(membership).toBeDefined(); - expect(membership?.driver.id).toBe(driver.id.toString()); - expect(membership?.driver.iracingId).toBe(driver.iracingId); - expect(membership?.driver.country).toBe(driver.country); - } - }); - - it('should handle league with many members', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Many Members League' }); - const season = await factory.createSeason(league.id.toString()); - - // Create 15 drivers - const drivers = await Promise.all( - Array.from({ length: 15 }, (_, i) => - factory.createDriver({ name: `Member ${i + 1}`, iracingId: `${2000 + i}` }) - ) - ); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/memberships`); - - // Should have all drivers - expect(response.memberships.length).toBeGreaterThanOrEqual(15); - - // All memberships should have correct structure - for (const membership of response.memberships) { - expect(membership).toHaveProperty('driverId'); - expect(membership).toHaveProperty('driver'); - expect(membership).toHaveProperty('role'); - expect(membership).toHaveProperty('status'); - expect(membership).toHaveProperty('joinedAt'); - } - }); - - it('should handle members with different roles', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Roles League' }); - const season = await factory.createSeason(league.id.toString()); - - const drivers = await Promise.all([ - factory.createDriver({ name: 'Owner', country: 'US' }), - factory.createDriver({ name: 'Admin', country: 'UK' }), - factory.createDriver({ name: 'Member', country: 'CA' }), - ]); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/memberships`); - - // Should have members with different roles - const roles = response.memberships.map(m => m.role); - expect(roles.length).toBeGreaterThan(0); - - // Verify roles are present - const hasOwner = roles.some(r => r === 'owner' || r === 'OWNER'); - const hasAdmin = roles.some(r => r === 'admin' || r === 'ADMIN'); - const hasMember = roles.some(r => r === 'member' || r === 'MEMBER'); - - // At least owner should exist - expect(hasOwner || hasAdmin || hasMember).toBe(true); - }); - }); - - describe('Data Consistency', () => { - it('should maintain data consistency across multiple API calls', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Consistency Members League' }); - const season = await factory.createSeason(league.id.toString()); - - const driver = await factory.createDriver({ name: 'Consistent Member', country: 'DE' }); - - const api = harness.getApi(); - - // Make multiple calls - const response1 = await api.get(`/leagues/${league.id}/memberships`); - const response2 = await api.get(`/leagues/${league.id}/memberships`); - const response3 = await api.get(`/leagues/${league.id}/memberships`); - - // All responses should be identical - expect(response1).toEqual(response2); - expect(response2).toEqual(response3); - - // Verify data integrity - expect(response1.memberships.length).toBeGreaterThan(0); - - const consistentMember = response1.memberships.find(m => m.driver.name === 'Consistent Member'); - expect(consistentMember).toBeDefined(); - expect(consistentMember?.driver.country).toBe('DE'); - }); - - it('should handle edge case: league with many members and complex data', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Complex Members League' }); - const season = await factory.createSeason(league.id.toString()); - - // Create 20 drivers - const drivers = await Promise.all( - Array.from({ length: 20 }, (_, i) => - factory.createDriver({ - name: `Complex Member ${i + 1}`, - iracingId: `${3000 + i}`, - country: ['US', 'UK', 'CA', 'DE', 'FR'][i % 5] - }) - ) - ); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/memberships`); - - // Should have all drivers - expect(response.memberships.length).toBeGreaterThanOrEqual(20); - - // All memberships should have correct structure - for (const membership of response.memberships) { - expect(membership).toHaveProperty('driverId'); - expect(membership).toHaveProperty('driver'); - expect(membership).toHaveProperty('role'); - expect(membership).toHaveProperty('status'); - expect(membership).toHaveProperty('joinedAt'); - - // Verify driver has all required fields - expect(membership.driver).toHaveProperty('id'); - expect(membership.driver).toHaveProperty('iracingId'); - expect(membership.driver).toHaveProperty('name'); - expect(membership.driver).toHaveProperty('country'); - expect(membership.driver).toHaveProperty('joinedAt'); - } - - // Verify all drivers are present - const driverNames = response.memberships.map(m => m.driver.name); - for (const driver of drivers) { - expect(driverNames).toContain(driver.name.toString()); - } - }); - - it('should handle edge case: members with optional fields', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Optional Fields League' }); - const season = await factory.createSeason(league.id.toString()); - - // Create driver without bio (should be optional) - const driver = await factory.createDriver({ name: 'Test Member', country: 'US' }); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/memberships`); - - expect(response.memberships.length).toBeGreaterThan(0); - - const testMember = response.memberships.find(m => m.driver.name === 'Test Member'); - expect(testMember).toBeDefined(); - expect(testMember?.driver.bio).toBeUndefined(); // Optional field - expect(testMember?.driver.name).toBe('Test Member'); - expect(testMember?.driver.country).toBe('US'); - }); - - it('should handle edge case: league with no completed races but has members', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'No Races Members League' }); - const season = await factory.createSeason(league.id.toString()); - - const driver = await factory.createDriver({ name: 'Waiting Member', 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}/memberships`); - - // Should still have members even with no completed races - expect(response.memberships.length).toBeGreaterThan(0); - - const waitingMember = response.memberships.find(m => m.driver.name === 'Waiting Member'); - expect(waitingMember).toBeDefined(); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/league/schedule-data-flow.integration.test.ts b/tests/integration/league/schedule-data-flow.integration.test.ts deleted file mode 100644 index faa7feeba..000000000 --- a/tests/integration/league/schedule-data-flow.integration.test.ts +++ /dev/null @@ -1,386 +0,0 @@ -/** - * Integration Test: League Schedule Data Flow - * - * Tests the complete data flow from database to API response for league schedule: - * 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 Schedule - 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 schedule DTO structure from API', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Schedule Test League' }); - const season = await factory.createSeason(league.id.toString()); - - // Create races with different statuses - 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), // Future race - status: 'scheduled' - }); - - const race2 = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Road Atlanta', - car: 'Formula Ford', - scheduledAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // Future race - status: 'scheduled' - }); - - const race3 = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Nürburgring', - car: 'GT3', - scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // Past race - status: 'completed' - }); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/schedule`); - - // Verify: API response structure - expect(response).toBeDefined(); - expect(response.races).toBeDefined(); - expect(Array.isArray(response.races)).toBe(true); - - // Verify: Each race has correct DTO structure - for (const race of response.races) { - expect(race).toHaveProperty('id'); - expect(race).toHaveProperty('track'); - expect(race).toHaveProperty('car'); - expect(race).toHaveProperty('scheduledAt'); - expect(race).toHaveProperty('status'); - expect(race).toHaveProperty('results'); - expect(Array.isArray(race.results)).toBe(true); - } - - // Verify: Race data matches what we created - const scheduledRaces = response.races.filter(r => r.status === 'scheduled'); - const completedRaces = response.races.filter(r => r.status === 'completed'); - - expect(scheduledRaces).toHaveLength(2); - expect(completedRaces).toHaveLength(1); - }); - - it('should return empty schedule for league with no races', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Empty Schedule League' }); - const season = await factory.createSeason(league.id.toString()); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/schedule`); - - expect(response.races).toEqual([]); - }); - - it('should handle schedule with single race', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Single Race League' }); - const season = await factory.createSeason(league.id.toString()); - - const race = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Monza', - car: 'GT3', - scheduledAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), - status: 'scheduled' - }); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/schedule`); - - expect(response.races).toHaveLength(1); - expect(response.races[0].track).toBe('Monza'); - expect(response.races[0].car).toBe('GT3'); - expect(response.races[0].status).toBe('scheduled'); - }); - }); - - describe('End-to-End Data Flow', () => { - it('should correctly transform race data to schedule DTO', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Transformation Test League' }); - const season = await factory.createSeason(league.id.toString()); - - const driver = await factory.createDriver({ name: 'Test Driver', country: 'US' }); - - // Create a completed race with results - const race = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Suzuka', - car: 'Formula 1', - scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), - status: 'completed' - }); - - await factory.createResult(race.id.toString(), driver.id.toString(), { - position: 1, - fastestLap: 92000, - incidents: 0, - startPosition: 2 - }); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/schedule`); - - expect(response.races).toHaveLength(1); - - const raceData = response.races[0]; - expect(raceData.track).toBe('Suzuka'); - expect(raceData.car).toBe('Formula 1'); - expect(raceData.status).toBe('completed'); - expect(raceData.results).toHaveLength(1); - expect(raceData.results[0].position).toBe(1); - expect(raceData.results[0].driverId).toBe(driver.id.toString()); - }); - - it('should handle schedule with multiple races and results', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Multi Race League' }); - const season = await factory.createSeason(league.id.toString()); - - const drivers = await Promise.all([ - factory.createDriver({ name: 'Driver 1', country: 'US' }), - factory.createDriver({ name: 'Driver 2', country: 'UK' }), - ]); - - // Create 3 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: 'scheduled' - }), - ]); - - // Add results to first two races - 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[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: 1 }); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/schedule`); - - expect(response.races).toHaveLength(3); - - // Verify completed races have results - const completedRaces = response.races.filter(r => r.status === 'completed'); - expect(completedRaces).toHaveLength(2); - - for (const race of completedRaces) { - expect(race.results).toHaveLength(2); - expect(race.results[0].position).toBeDefined(); - expect(race.results[0].driverId).toBeDefined(); - } - - // Verify scheduled race has no results - const scheduledRace = response.races.find(r => r.status === 'scheduled'); - expect(scheduledRace).toBeDefined(); - expect(scheduledRace?.results).toEqual([]); - }); - - it('should handle schedule with published/unpublished races', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Publish Test League' }); - const season = await factory.createSeason(league.id.toString()); - - // Create races with different publish states - const race1 = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Track A', - car: 'Car A', - scheduledAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), - status: 'scheduled' - }); - - const race2 = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Track B', - car: 'Car B', - scheduledAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), - status: 'scheduled' - }); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/schedule`); - - expect(response.races).toHaveLength(2); - - // Both races should be in the schedule - const trackNames = response.races.map(r => r.track); - expect(trackNames).toContain('Track A'); - expect(trackNames).toContain('Track B'); - }); - }); - - describe('Data Consistency', () => { - it('should maintain data consistency across multiple API calls', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Consistency Schedule League' }); - const season = await factory.createSeason(league.id.toString()); - - const race = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Consistency Track', - car: 'Consistency Car', - scheduledAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), - status: 'scheduled' - }); - - const api = harness.getApi(); - - // Make multiple calls - const response1 = await api.get(`/leagues/${league.id}/schedule`); - const response2 = await api.get(`/leagues/${league.id}/schedule`); - const response3 = await api.get(`/leagues/${league.id}/schedule`); - - // All responses should be identical - expect(response1).toEqual(response2); - expect(response2).toEqual(response3); - - // Verify data integrity - expect(response1.races).toHaveLength(1); - expect(response1.races[0].track).toBe('Consistency Track'); - expect(response1.races[0].car).toBe('Consistency Car'); - }); - - it('should handle edge case: league with many races', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Large Schedule League' }); - const season = await factory.createSeason(league.id.toString()); - - // Create 20 races - const races = await Promise.all( - Array.from({ length: 20 }, (_, i) => - factory.createRace({ - leagueId: league.id.toString(), - track: `Track ${i + 1}`, - car: 'GT3', - scheduledAt: new Date(Date.now() + (i + 1) * 24 * 60 * 60 * 1000), - status: 'scheduled' - }) - ) - ); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/schedule`); - - // Should have all 20 races - expect(response.races).toHaveLength(20); - - // All races should have correct structure - for (const race of response.races) { - expect(race).toHaveProperty('id'); - expect(race).toHaveProperty('track'); - expect(race).toHaveProperty('car'); - expect(race).toHaveProperty('scheduledAt'); - expect(race).toHaveProperty('status'); - expect(race).toHaveProperty('results'); - expect(Array.isArray(race.results)).toBe(true); - } - - // All races should be scheduled - const allScheduled = response.races.every(r => r.status === 'scheduled'); - expect(allScheduled).toBe(true); - }); - - it('should handle edge case: league with races spanning multiple seasons', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'Multi Season League' }); - - // Create two seasons - const season1 = await factory.createSeason(league.id.toString(), { name: 'Season 1', year: 2024 }); - const season2 = await factory.createSeason(league.id.toString(), { name: 'Season 2', year: 2025 }); - - // Create races in both seasons - const race1 = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Season 1 Track', - car: 'Car 1', - scheduledAt: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), // Last year - status: 'completed' - }); - - const race2 = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Season 2 Track', - car: 'Car 2', - scheduledAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // This year - status: 'scheduled' - }); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/schedule`); - - // Should have both races (schedule endpoint returns all races for league) - expect(response.races).toHaveLength(2); - - const trackNames = response.races.map(r => r.track); - expect(trackNames).toContain('Season 1 Track'); - expect(trackNames).toContain('Season 2 Track'); - }); - - it('should handle edge case: race with no results', async () => { - const factory = harness.getFactory(); - const league = await factory.createLeague({ name: 'No Results League' }); - const season = await factory.createSeason(league.id.toString()); - - // Create a completed race with no results - const race = await factory.createRace({ - leagueId: league.id.toString(), - track: 'Empty Results Track', - car: 'Empty Car', - scheduledAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), - status: 'completed' - }); - - const api = harness.getApi(); - const response = await api.get(`/leagues/${league.id}/schedule`); - - expect(response.races).toHaveLength(1); - expect(response.races[0].results).toEqual([]); - expect(response.races[0].status).toBe('completed'); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/league/schedule-lifecycle.integration.test.ts b/tests/integration/league/schedule-lifecycle.integration.test.ts deleted file mode 100644 index 5468ac366..000000000 --- a/tests/integration/league/schedule-lifecycle.integration.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Integration Test: League Schedule Lifecycle API - * - * Tests publish/unpublish/republish lifecycle endpoints. - */ - -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { ApiClient } from '../harness/api-client'; -import { DockerManager } from '../harness/docker-manager'; - -describe('League Schedule Lifecycle - API Integration', () => { - let api: ApiClient; - let docker: DockerManager; - - beforeAll(async () => { - docker = DockerManager.getInstance(); - await docker.start(); - - api = new ApiClient({ baseUrl: 'http://localhost:3101', timeout: 60000 }); - await api.waitForReady(); - }, 120000); - - afterAll(async () => { - docker.stop(); - }, 30000); - - it('should handle publish endpoint for non-existent league', async () => { - const nonExistentLeagueId = 'non-existent-league'; - const nonExistentSeasonId = 'non-existent-season'; - - await expect( - api.post(`/leagues/${nonExistentLeagueId}/seasons/${nonExistentSeasonId}/publish`, {}) - ).rejects.toThrow(); - }); - - it('should handle unpublish endpoint for non-existent league', async () => { - const nonExistentLeagueId = 'non-existent-league'; - const nonExistentSeasonId = 'non-existent-season'; - - await expect( - api.post(`/leagues/${nonExistentLeagueId}/seasons/${nonExistentSeasonId}/unpublish`, {}) - ).rejects.toThrow(); - }); - - it('should handle create schedule race endpoint for non-existent league', async () => { - const nonExistentLeagueId = 'non-existent-league'; - const nonExistentSeasonId = 'non-existent-season'; - - await expect( - api.post(`/leagues/${nonExistentLeagueId}/seasons/${nonExistentSeasonId}/schedule/races`, { - track: 'Laguna Seca', - car: 'Formula Ford', - scheduledAtIso: new Date().toISOString(), - }) - ).rejects.toThrow(); - }); - - it('should reject invalid date format', async () => { - const leagueId = 'test-league'; - const seasonId = 'test-season'; - - await expect( - api.post(`/leagues/${leagueId}/seasons/${seasonId}/schedule/races`, { - track: 'Laguna Seca', - car: 'Formula Ford', - scheduledAtIso: 'invalid-date', - }) - ).rejects.toThrow(); - }); - - it('should reject missing required fields for race creation', async () => { - const leagueId = 'test-league'; - const seasonId = 'test-season'; - - await expect( - api.post(`/leagues/${leagueId}/seasons/${seasonId}/schedule/races`, { - track: 'Laguna Seca', - // Missing car and scheduledAtIso - }) - ).rejects.toThrow(); - }); -}); \ No newline at end of file diff --git a/tests/integration/league/standings-data-flow.integration.test.ts b/tests/integration/league/standings-data-flow.integration.test.ts deleted file mode 100644 index eb0771f23..000000000 --- a/tests/integration/league/standings-data-flow.integration.test.ts +++ /dev/null @@ -1,395 +0,0 @@ -/** - * 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'); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/league/stats-data-flow.integration.test.ts b/tests/integration/league/stats-data-flow.integration.test.ts deleted file mode 100644 index b6b560242..000000000 --- a/tests/integration/league/stats-data-flow.integration.test.ts +++ /dev/null @@ -1,493 +0,0 @@ -/** - * 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'); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/race/import-results.integration.test.ts b/tests/integration/race/import-results.integration.test.ts deleted file mode 100644 index 74048f510..000000000 --- a/tests/integration/race/import-results.integration.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Integration Test: Race Results Import API - * - * Tests the race results import endpoint with various scenarios. - */ - -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { ApiClient } from '../harness/api-client'; -import { DockerManager } from '../harness/docker-manager'; - -describe('Race Results Import - API Integration', () => { - let api: ApiClient; - let docker: DockerManager; - - beforeAll(async () => { - docker = DockerManager.getInstance(); - await docker.start(); - - api = new ApiClient({ baseUrl: 'http://localhost:3101', timeout: 60000 }); - await api.waitForReady(); - }, 120000); - - afterAll(async () => { - docker.stop(); - }, 30000); - - it('should return 404 for non-existent race', async () => { - const nonExistentRaceId = 'non-existent-race-123'; - const results = [ - { - driverId: 'driver-1', - position: 1, - fastestLap: 100, - incidents: 0, - startPosition: 1, - }, - ]; - - await expect( - api.post(`/races/${nonExistentRaceId}/import-results`, { - resultsFileContent: JSON.stringify(results), - raceId: nonExistentRaceId, - }) - ).rejects.toThrow(); - }); - - it('should handle invalid JSON gracefully', async () => { - const raceId = 'test-race-1'; - - await expect( - api.post(`/races/${raceId}/import-results`, { - resultsFileContent: 'invalid json {', - raceId, - }) - ).rejects.toThrow(); - }); - - it('should reject empty results array', async () => { - const raceId = 'test-race-1'; - const emptyResults: unknown[] = []; - - await expect( - api.post(`/races/${raceId}/import-results`, { - resultsFileContent: JSON.stringify(emptyResults), - raceId, - }) - ).rejects.toThrow(); - }); - - it('should handle missing required fields', async () => { - const raceId = 'test-race-1'; - const invalidResults = [ - { - // Missing required fields - driverId: 'driver-1', - position: 1, - }, - ]; - - await expect( - api.post(`/races/${raceId}/import-results`, { - resultsFileContent: JSON.stringify(invalidResults), - raceId, - }) - ).rejects.toThrow(); - }); - - it('should verify API health endpoint works', async () => { - const isHealthy = await api.health(); - expect(isHealthy).toBe(true); - }); -}); \ No newline at end of file