website refactor

This commit is contained in:
2026-01-21 22:36:01 +01:00
parent ea58909070
commit 5ed958281d
49 changed files with 8763 additions and 131 deletions

View File

@@ -0,0 +1,782 @@
/**
* League API Tests
*
* This test suite performs comprehensive API testing for league-related endpoints.
* It validates:
* - Response structure matches expected DTO
* - Required fields are present
* - Data types are correct
* - Edge cases (empty results, missing data)
* - Business logic (sorting, filtering, calculations)
*
* This test is designed to run in the Docker e2e environment and can be executed with:
* npm run test:e2e:website (which runs everything in Docker)
*/
import { test, expect, request } from '@playwright/test';
import * as fs from 'fs/promises';
import * as path from 'path';
interface TestResult {
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
status: number;
success: boolean;
error?: string;
response?: unknown;
hasPresenterError: boolean;
responseTime: number;
}
const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
// Auth file paths
const USER_AUTH_FILE = path.join(__dirname, '.auth/user-session.json');
const ADMIN_AUTH_FILE = path.join(__dirname, '.auth/admin-session.json');
test.describe('League API Tests', () => {
const allResults: TestResult[] = [];
let testResults: TestResult[] = [];
test.beforeAll(async () => {
console.log(`[LEAGUE API] Testing API at: ${API_BASE_URL}`);
// Verify auth files exist
const userAuthExists = await fs.access(USER_AUTH_FILE).then(() => true).catch(() => false);
const adminAuthExists = await fs.access(ADMIN_AUTH_FILE).then(() => true).catch(() => false);
if (!userAuthExists || !adminAuthExists) {
throw new Error('Auth files not found. Run global setup first.');
}
console.log('[LEAGUE API] Auth files verified');
});
test.afterAll(async () => {
await generateReport();
});
test('League Discovery Endpoints - Public endpoints', async ({ request }) => {
testResults = [];
const endpoints = [
{ method: 'GET' as const, path: '/leagues/all-with-capacity', name: 'Get all leagues with capacity' },
{ method: 'GET' as const, path: '/leagues/all-with-capacity-and-scoring', name: 'Get all leagues with capacity and scoring' },
{ method: 'GET' as const, path: '/leagues/total-leagues', name: 'Get total leagues count' },
{ method: 'GET' as const, path: '/leagues/all', name: 'Get all leagues (alias)' },
{ method: 'GET' as const, path: '/leagues/available', name: 'Get available leagues (alias)' },
];
console.log(`\n[LEAGUE API] Testing ${endpoints.length} league discovery endpoints...`);
for (const endpoint of endpoints) {
await testEndpoint(request, endpoint);
}
// Check for failures
const failures = testResults.filter(r => !r.success);
if (failures.length > 0) {
console.log('\n❌ FAILURES FOUND:');
failures.forEach(r => {
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
});
}
// Assert all endpoints succeeded
expect(failures.length).toBe(0);
});
test('League Discovery - Response structure validation', async ({ request }) => {
testResults = [];
// Test /leagues/all-with-capacity
const allLeaguesResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
expect(allLeaguesResponse.ok()).toBe(true);
const allLeaguesData = await allLeaguesResponse.json();
expect(allLeaguesData).toHaveProperty('leagues');
expect(allLeaguesData).toHaveProperty('totalCount');
expect(Array.isArray(allLeaguesData.leagues)).toBe(true);
expect(typeof allLeaguesData.totalCount).toBe('number');
// Validate league structure if leagues exist
if (allLeaguesData.leagues.length > 0) {
const league = allLeaguesData.leagues[0];
expect(league).toHaveProperty('id');
expect(league).toHaveProperty('name');
expect(league).toHaveProperty('description');
expect(league).toHaveProperty('ownerId');
expect(league).toHaveProperty('createdAt');
expect(league).toHaveProperty('settings');
expect(league.settings).toHaveProperty('maxDrivers');
expect(league).toHaveProperty('usedSlots');
// Validate data types
expect(typeof league.id).toBe('string');
expect(typeof league.name).toBe('string');
expect(typeof league.description).toBe('string');
expect(typeof league.ownerId).toBe('string');
expect(typeof league.createdAt).toBe('string');
expect(typeof league.settings.maxDrivers).toBe('number');
expect(typeof league.usedSlots).toBe('number');
// Validate business logic: usedSlots <= maxDrivers
expect(league.usedSlots).toBeLessThanOrEqual(league.settings.maxDrivers);
}
// Test /leagues/all-with-capacity-and-scoring
const scoredLeaguesResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity-and-scoring`);
expect(scoredLeaguesResponse.ok()).toBe(true);
const scoredLeaguesData = await scoredLeaguesResponse.json();
expect(scoredLeaguesData).toHaveProperty('leagues');
expect(scoredLeaguesData).toHaveProperty('totalCount');
expect(Array.isArray(scoredLeaguesData.leagues)).toBe(true);
// Validate scoring structure if leagues exist
if (scoredLeaguesData.leagues.length > 0) {
const league = scoredLeaguesData.leagues[0];
expect(league).toHaveProperty('scoring');
expect(league.scoring).toHaveProperty('gameId');
expect(league.scoring).toHaveProperty('scoringPresetId');
// Validate data types
expect(typeof league.scoring.gameId).toBe('string');
expect(typeof league.scoring.scoringPresetId).toBe('string');
}
// Test /leagues/total-leagues
const totalResponse = await request.get(`${API_BASE_URL}/leagues/total-leagues`);
expect(totalResponse.ok()).toBe(true);
const totalData = await totalResponse.json();
expect(totalData).toHaveProperty('totalLeagues');
expect(typeof totalData.totalLeagues).toBe('number');
expect(totalData.totalLeagues).toBeGreaterThanOrEqual(0);
// Validate consistency: totalCount from all-with-capacity should match totalLeagues
expect(allLeaguesData.totalCount).toBe(totalData.totalLeagues);
testResults.push({
endpoint: '/leagues/all-with-capacity',
method: 'GET',
status: allLeaguesResponse.status(),
success: true,
hasPresenterError: false,
responseTime: 0,
});
testResults.push({
endpoint: '/leagues/all-with-capacity-and-scoring',
method: 'GET',
status: scoredLeaguesResponse.status(),
success: true,
hasPresenterError: false,
responseTime: 0,
});
testResults.push({
endpoint: '/leagues/total-leagues',
method: 'GET',
status: totalResponse.status(),
success: true,
hasPresenterError: false,
responseTime: 0,
});
allResults.push(...testResults);
});
test('League Detail Endpoints - Public endpoints', async ({ request }) => {
testResults = [];
// First, get a valid league ID from the discovery endpoint
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
const discoveryData = await discoveryResponse.json();
if (discoveryData.leagues.length === 0) {
console.log('[LEAGUE API] No leagues found, skipping detail endpoint tests');
return;
}
const leagueId = discoveryData.leagues[0].id;
const endpoints = [
{ method: 'GET' as const, path: `/leagues/${leagueId}`, name: 'Get league details' },
{ method: 'GET' as const, path: `/leagues/${leagueId}/seasons`, name: 'Get league seasons' },
{ method: 'GET' as const, path: `/leagues/${leagueId}/stats`, name: 'Get league stats' },
{ method: 'GET' as const, path: `/leagues/${leagueId}/memberships`, name: 'Get league memberships' },
];
console.log(`\n[LEAGUE API] Testing ${endpoints.length} league detail endpoints for league ${leagueId}...`);
for (const endpoint of endpoints) {
await testEndpoint(request, endpoint);
}
// Check for failures
const failures = testResults.filter(r => !r.success);
if (failures.length > 0) {
console.log('\n❌ FAILURES FOUND:');
failures.forEach(r => {
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
});
}
// Assert all endpoints succeeded
expect(failures.length).toBe(0);
});
test('League Detail - Response structure validation', async ({ request }) => {
testResults = [];
// First, get a valid league ID from the discovery endpoint
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
const discoveryData = await discoveryResponse.json();
if (discoveryData.leagues.length === 0) {
console.log('[LEAGUE API] No leagues found, skipping detail validation tests');
return;
}
const leagueId = discoveryData.leagues[0].id;
// Test /leagues/{id}
const leagueResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}`);
expect(leagueResponse.ok()).toBe(true);
const leagueData = await leagueResponse.json();
expect(leagueData).toHaveProperty('id');
expect(leagueData).toHaveProperty('name');
expect(leagueData).toHaveProperty('description');
expect(leagueData).toHaveProperty('ownerId');
expect(leagueData).toHaveProperty('createdAt');
// Validate data types
expect(typeof leagueData.id).toBe('string');
expect(typeof leagueData.name).toBe('string');
expect(typeof leagueData.description).toBe('string');
expect(typeof leagueData.ownerId).toBe('string');
expect(typeof leagueData.createdAt).toBe('string');
// Validate ID matches requested ID
expect(leagueData.id).toBe(leagueId);
// Test /leagues/{id}/seasons
const seasonsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/seasons`);
expect(seasonsResponse.ok()).toBe(true);
const seasonsData = await seasonsResponse.json();
expect(Array.isArray(seasonsData)).toBe(true);
// Validate season structure if seasons exist
if (seasonsData.length > 0) {
const season = seasonsData[0];
expect(season).toHaveProperty('id');
expect(season).toHaveProperty('name');
expect(season).toHaveProperty('status');
// Validate data types
expect(typeof season.id).toBe('string');
expect(typeof season.name).toBe('string');
expect(typeof season.status).toBe('string');
}
// Test /leagues/{id}/stats
const statsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/stats`);
expect(statsResponse.ok()).toBe(true);
const statsData = await statsResponse.json();
expect(statsData).toHaveProperty('memberCount');
expect(statsData).toHaveProperty('raceCount');
expect(statsData).toHaveProperty('avgSOF');
// Validate data types
expect(typeof statsData.memberCount).toBe('number');
expect(typeof statsData.raceCount).toBe('number');
expect(typeof statsData.avgSOF).toBe('number');
// Validate business logic: counts should be non-negative
expect(statsData.memberCount).toBeGreaterThanOrEqual(0);
expect(statsData.raceCount).toBeGreaterThanOrEqual(0);
expect(statsData.avgSOF).toBeGreaterThanOrEqual(0);
// Test /leagues/{id}/memberships
const membershipsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/memberships`);
expect(membershipsResponse.ok()).toBe(true);
const membershipsData = await membershipsResponse.json();
expect(membershipsData).toHaveProperty('members');
expect(Array.isArray(membershipsData.members)).toBe(true);
// Validate membership structure if members exist
if (membershipsData.members.length > 0) {
const member = membershipsData.members[0];
expect(member).toHaveProperty('driverId');
expect(member).toHaveProperty('role');
expect(member).toHaveProperty('joinedAt');
// Validate data types
expect(typeof member.driverId).toBe('string');
expect(typeof member.role).toBe('string');
expect(typeof member.joinedAt).toBe('string');
// Validate business logic: at least one owner must exist
const hasOwner = membershipsData.members.some((m: any) => m.role === 'owner');
expect(hasOwner).toBe(true);
}
testResults.push({
endpoint: `/leagues/${leagueId}`,
method: 'GET',
status: leagueResponse.status(),
success: true,
hasPresenterError: false,
responseTime: 0,
});
testResults.push({
endpoint: `/leagues/${leagueId}/seasons`,
method: 'GET',
status: seasonsResponse.status(),
success: true,
hasPresenterError: false,
responseTime: 0,
});
testResults.push({
endpoint: `/leagues/${leagueId}/stats`,
method: 'GET',
status: statsResponse.status(),
success: true,
hasPresenterError: false,
responseTime: 0,
});
testResults.push({
endpoint: `/leagues/${leagueId}/memberships`,
method: 'GET',
status: membershipsResponse.status(),
success: true,
hasPresenterError: false,
responseTime: 0,
});
allResults.push(...testResults);
});
test('League Schedule Endpoints - Public endpoints', async ({ request }) => {
testResults = [];
// First, get a valid league ID from the discovery endpoint
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
const discoveryData = await discoveryResponse.json();
if (discoveryData.leagues.length === 0) {
console.log('[LEAGUE API] No leagues found, skipping schedule endpoint tests');
return;
}
const leagueId = discoveryData.leagues[0].id;
const endpoints = [
{ method: 'GET' as const, path: `/leagues/${leagueId}/schedule`, name: 'Get league schedule' },
];
console.log(`\n[LEAGUE API] Testing ${endpoints.length} league schedule endpoints for league ${leagueId}...`);
for (const endpoint of endpoints) {
await testEndpoint(request, endpoint);
}
// Check for failures
const failures = testResults.filter(r => !r.success);
if (failures.length > 0) {
console.log('\n❌ FAILURES FOUND:');
failures.forEach(r => {
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
});
}
// Assert all endpoints succeeded
expect(failures.length).toBe(0);
});
test('League Schedule - Response structure validation', async ({ request }) => {
testResults = [];
// First, get a valid league ID from the discovery endpoint
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
const discoveryData = await discoveryResponse.json();
if (discoveryData.leagues.length === 0) {
console.log('[LEAGUE API] No leagues found, skipping schedule validation tests');
return;
}
const leagueId = discoveryData.leagues[0].id;
// Test /leagues/{id}/schedule
const scheduleResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/schedule`);
expect(scheduleResponse.ok()).toBe(true);
const scheduleData = await scheduleResponse.json();
expect(scheduleData).toHaveProperty('seasonId');
expect(scheduleData).toHaveProperty('races');
expect(Array.isArray(scheduleData.races)).toBe(true);
// Validate data types
expect(typeof scheduleData.seasonId).toBe('string');
// Validate race structure if races exist
if (scheduleData.races.length > 0) {
const race = scheduleData.races[0];
expect(race).toHaveProperty('id');
expect(race).toHaveProperty('track');
expect(race).toHaveProperty('car');
expect(race).toHaveProperty('scheduledAt');
// Validate data types
expect(typeof race.id).toBe('string');
expect(typeof race.track).toBe('string');
expect(typeof race.car).toBe('string');
expect(typeof race.scheduledAt).toBe('string');
// Validate business logic: races should be sorted by scheduledAt
const scheduledTimes = scheduleData.races.map((r: any) => new Date(r.scheduledAt).getTime());
const sortedTimes = [...scheduledTimes].sort((a, b) => a - b);
expect(scheduledTimes).toEqual(sortedTimes);
}
testResults.push({
endpoint: `/leagues/${leagueId}/schedule`,
method: 'GET',
status: scheduleResponse.status(),
success: true,
hasPresenterError: false,
responseTime: 0,
});
allResults.push(...testResults);
});
test('League Standings Endpoints - Public endpoints', async ({ request }) => {
testResults = [];
// First, get a valid league ID from the discovery endpoint
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
const discoveryData = await discoveryResponse.json();
if (discoveryData.leagues.length === 0) {
console.log('[LEAGUE API] No leagues found, skipping standings endpoint tests');
return;
}
const leagueId = discoveryData.leagues[0].id;
const endpoints = [
{ method: 'GET' as const, path: `/leagues/${leagueId}/standings`, name: 'Get league standings' },
];
console.log(`\n[LEAGUE API] Testing ${endpoints.length} league standings endpoints for league ${leagueId}...`);
for (const endpoint of endpoints) {
await testEndpoint(request, endpoint);
}
// Check for failures
const failures = testResults.filter(r => !r.success);
if (failures.length > 0) {
console.log('\n❌ FAILURES FOUND:');
failures.forEach(r => {
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
});
}
// Assert all endpoints succeeded
expect(failures.length).toBe(0);
});
test('League Standings - Response structure validation', async ({ request }) => {
testResults = [];
// First, get a valid league ID from the discovery endpoint
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
const discoveryData = await discoveryResponse.json();
if (discoveryData.leagues.length === 0) {
console.log('[LEAGUE API] No leagues found, skipping standings validation tests');
return;
}
const leagueId = discoveryData.leagues[0].id;
// Test /leagues/{id}/standings
const standingsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/standings`);
expect(standingsResponse.ok()).toBe(true);
const standingsData = await standingsResponse.json();
expect(standingsData).toHaveProperty('standings');
expect(Array.isArray(standingsData.standings)).toBe(true);
// Validate standing structure if standings exist
if (standingsData.standings.length > 0) {
const standing = standingsData.standings[0];
expect(standing).toHaveProperty('position');
expect(standing).toHaveProperty('driverId');
expect(standing).toHaveProperty('points');
expect(standing).toHaveProperty('races');
// Validate data types
expect(typeof standing.position).toBe('number');
expect(typeof standing.driverId).toBe('string');
expect(typeof standing.points).toBe('number');
expect(typeof standing.races).toBe('number');
// Validate business logic: position must be sequential starting from 1
const positions = standingsData.standings.map((s: any) => s.position);
const expectedPositions = Array.from({ length: positions.length }, (_, i) => i + 1);
expect(positions).toEqual(expectedPositions);
// Validate business logic: points must be non-negative
expect(standing.points).toBeGreaterThanOrEqual(0);
// Validate business logic: races count must be non-negative
expect(standing.races).toBeGreaterThanOrEqual(0);
}
testResults.push({
endpoint: `/leagues/${leagueId}/standings`,
method: 'GET',
status: standingsResponse.status(),
success: true,
hasPresenterError: false,
responseTime: 0,
});
allResults.push(...testResults);
});
test('Edge Cases - Invalid league IDs', async ({ request }) => {
testResults = [];
const endpoints = [
{ method: 'GET' as const, path: '/leagues/non-existent-league-id', name: 'Get non-existent league' },
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/seasons', name: 'Get seasons for non-existent league' },
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/stats', name: 'Get stats for non-existent league' },
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/schedule', name: 'Get schedule for non-existent league' },
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/standings', name: 'Get standings for non-existent league' },
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/memberships', name: 'Get memberships for non-existent league' },
];
console.log(`\n[LEAGUE API] Testing ${endpoints.length} edge case endpoints with invalid IDs...`);
for (const endpoint of endpoints) {
await testEndpoint(request, endpoint);
}
// Check for failures
const failures = testResults.filter(r => !r.success);
if (failures.length > 0) {
console.log('\n❌ FAILURES FOUND:');
failures.forEach(r => {
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
});
}
// Assert all endpoints succeeded (404 is acceptable for non-existent resources)
expect(failures.length).toBe(0);
});
test('Edge Cases - Empty results', async ({ request }) => {
testResults = [];
// Test discovery endpoints with filters (if available)
// Note: The current API doesn't seem to have filter parameters, but we test the base endpoints
const endpoints = [
{ method: 'GET' as const, path: '/leagues/all-with-capacity', name: 'Get all leagues (empty check)' },
{ method: 'GET' as const, path: '/leagues/all-with-capacity-and-scoring', name: 'Get all leagues with scoring (empty check)' },
];
console.log(`\n[LEAGUE API] Testing ${endpoints.length} endpoints for empty result handling...`);
for (const endpoint of endpoints) {
await testEndpoint(request, endpoint);
}
// Check for failures
const failures = testResults.filter(r => !r.success);
if (failures.length > 0) {
console.log('\n❌ FAILURES FOUND:');
failures.forEach(r => {
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
});
}
// Assert all endpoints succeeded
expect(failures.length).toBe(0);
});
async function testEndpoint(
request: import('@playwright/test').APIRequestContext,
endpoint: { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; path: string; name?: string; body?: unknown; requiresAuth?: boolean }
): Promise<void> {
const startTime = Date.now();
const fullUrl = `${API_BASE_URL}${endpoint.path}`;
console.log(`\n[TEST] ${endpoint.method} ${endpoint.path} (${endpoint.name || 'Unknown'})`);
try {
let response;
const headers: Record<string, string> = {};
// Playwright's request context handles cookies automatically
// No need to set Authorization header for cookie-based auth
switch (endpoint.method) {
case 'GET':
response = await request.get(fullUrl, { headers });
break;
case 'POST':
response = await request.post(fullUrl, { data: endpoint.body || {}, headers });
break;
case 'PUT':
response = await request.put(fullUrl, { data: endpoint.body || {}, headers });
break;
case 'DELETE':
response = await request.delete(fullUrl, { headers });
break;
case 'PATCH':
response = await request.patch(fullUrl, { data: endpoint.body || {}, headers });
break;
}
const responseTime = Date.now() - startTime;
const status = response.status();
const body = await response.json().catch(() => null);
const bodyText = await response.text().catch(() => '');
// Check for presenter errors
const hasPresenterError =
bodyText.includes('Presenter not presented') ||
bodyText.includes('presenter not presented') ||
(body && body.message && body.message.includes('Presenter not presented')) ||
(body && body.error && body.error.includes('Presenter not presented'));
// Success is 200-299 status, or 404 for non-existent resources, and no presenter error
const isNotFound = status === 404;
const success = (status >= 200 && status < 300 || isNotFound) && !hasPresenterError;
const result: TestResult = {
endpoint: endpoint.path,
method: endpoint.method,
status,
success,
hasPresenterError,
responseTime,
response: body || bodyText.substring(0, 200),
};
if (!success) {
result.error = body?.message || bodyText.substring(0, 200);
}
testResults.push(result);
allResults.push(result);
if (hasPresenterError) {
console.log(` ❌ PRESENTER ERROR: ${status} - ${body?.message || bodyText.substring(0, 100)}`);
} else if (success) {
console.log(`${status} (${responseTime}ms)`);
} else {
console.log(` ⚠️ ${status} (${responseTime}ms) - ${body?.message || 'Error'}`);
}
} catch (error: unknown) {
const responseTime = Date.now() - startTime;
const errorString = error instanceof Error ? error.message : String(error);
const result: TestResult = {
endpoint: endpoint.path,
method: endpoint.method,
status: 0,
success: false,
hasPresenterError: false,
responseTime,
error: errorString,
};
// Check if it's a presenter error
if (errorString.includes('Presenter not presented')) {
result.hasPresenterError = true;
console.log(` ❌ PRESENTER ERROR (exception): ${errorString}`);
} else {
console.log(` ❌ EXCEPTION: ${errorString}`);
}
testResults.push(result);
allResults.push(result);
}
}
async function generateReport(): Promise<void> {
const summary = {
total: allResults.length,
success: allResults.filter(r => r.success).length,
failed: allResults.filter(r => !r.success).length,
presenterErrors: allResults.filter(r => r.hasPresenterError).length,
avgResponseTime: allResults.reduce((sum, r) => sum + r.responseTime, 0) / allResults.length || 0,
};
const report = {
timestamp: new Date().toISOString(),
summary,
results: allResults,
failures: allResults.filter(r => !r.success),
};
// Write JSON report
const jsonPath = path.join(__dirname, '../../../league-api-test-report.json');
await fs.writeFile(jsonPath, JSON.stringify(report, null, 2));
// Write Markdown report
const mdPath = path.join(__dirname, '../../../league-api-test-report.md');
let md = `# League API Test Report\n\n`;
md += `**Generated:** ${new Date().toISOString()}\n`;
md += `**API Base URL:** ${API_BASE_URL}\n\n`;
md += `## Summary\n\n`;
md += `- **Total Endpoints:** ${summary.total}\n`;
md += `- **✅ Success:** ${summary.success}\n`;
md += `- **❌ Failed:** ${summary.failed}\n`;
md += `- **⚠️ Presenter Errors:** ${summary.presenterErrors}\n`;
md += `- **Avg Response Time:** ${summary.avgResponseTime.toFixed(2)}ms\n\n`;
if (summary.presenterErrors > 0) {
md += `## Presenter Errors\n\n`;
const presenterFailures = allResults.filter(r => r.hasPresenterError);
presenterFailures.forEach((r, i) => {
md += `${i + 1}. **${r.method} ${r.endpoint}**\n`;
md += ` - Status: ${r.status}\n`;
md += ` - Error: ${r.error || 'No error message'}\n\n`;
});
}
if (summary.failed > 0 && summary.presenterErrors < summary.failed) {
md += `## Other Failures\n\n`;
const otherFailures = allResults.filter(r => !r.success && !r.hasPresenterError);
otherFailures.forEach((r, i) => {
md += `${i + 1}. **${r.method} ${r.endpoint}**\n`;
md += ` - Status: ${r.status}\n`;
md += ` - Error: ${r.error || 'No error message'}\n\n`;
});
}
await fs.writeFile(mdPath, md);
console.log(`\n📊 Reports generated:`);
console.log(` JSON: ${jsonPath}`);
console.log(` Markdown: ${mdPath}`);
console.log(`\nSummary: ${summary.success}/${summary.total} passed, ${summary.presenterErrors} presenter errors`);
}
});

View File

@@ -0,0 +1,628 @@
import { test, expect, Browser, APIRequestContext } from '@playwright/test';
import { WebsiteAuthManager, AuthContext } from '../../shared/website/WebsiteAuthManager';
import { ConsoleErrorCapture } from '../../shared/website/ConsoleErrorCapture';
import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
/**
* E2E Tests for League Pages with Data Validation
*
* Tests cover:
* 1. /leagues (Discovery Page) - League cards, filters, quick actions
* 2. /leagues/[id] (Overview Page) - Stats, next race, season progress
* 3. /leagues/[id]/schedule (Schedule Page) - Race list, registration, admin controls
* 4. /leagues/[id]/standings (Standings Page) - Trend indicators, stats, team toggle
* 5. /leagues/[id]/roster (Roster Page) - Driver cards, admin actions
*/
test.describe('League Pages - E2E with Data Validation', () => {
const routeManager = new WebsiteRouteManager();
const leagueId = routeManager.resolvePathTemplate('/leagues/[id]', { id: WebsiteRouteManager.IDs.LEAGUE });
const CONSOLE_ALLOWLIST = [
/Download the React DevTools/i,
/Next.js-specific warning/i,
/Failed to load resource: the server responded with a status of 404/i,
/Failed to load resource: the server responded with a status of 403/i,
/Failed to load resource: the server responded with a status of 401/i,
/Failed to load resource: the server responded with a status of 500/i,
/net::ERR_NAME_NOT_RESOLVED/i,
/net::ERR_CONNECTION_CLOSED/i,
/net::ERR_ACCESS_DENIED/i,
/Minified React error #418/i,
/Event/i,
/An error occurred in the Server Components render/i,
/Route Error Boundary/i,
];
test.beforeEach(async ({ page }) => {
const allowedHosts = [
new URL(process.env.PLAYWRIGHT_BASE_URL || 'http://website:3000').host,
new URL(process.env.API_BASE_URL || 'http://api:3000').host,
];
await page.route('**/*', (route) => {
const url = new URL(route.request().url());
if (allowedHosts.includes(url.host) || url.protocol === 'data:') {
route.continue();
} else {
route.abort('accessdenied');
}
});
});
test.describe('1. /leagues (Discovery Page)', () => {
test('Unauthenticated user can view league discovery page', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
await page.goto('/leagues', { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/leagues');
// Verify featured leagues section displays
await expect(page.getByTestId('featured-leagues-section')).toBeVisible();
// Verify league cards are present
const leagueCards = page.getByTestId('league-card');
await expect(leagueCards.first()).toBeVisible();
// Verify league cards show correct metadata
const firstCard = leagueCards.first();
await expect(firstCard.getByTestId('league-card-title')).toBeVisible();
await expect(firstCard.getByTestId('league-card-next-race')).toBeVisible();
await expect(firstCard.getByTestId('league-card-active-drivers')).toBeVisible();
// Verify category filters are present
await expect(page.getByTestId('category-filters')).toBeVisible();
// Verify Quick Join/Follow buttons are present
await expect(page.getByTestId('quick-join-button')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
test('Authenticated user can view league discovery page', async ({ browser, request }) => {
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
try {
await page.goto('/leagues', { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/leagues');
// Verify featured leagues section displays
await expect(page.getByTestId('featured-leagues-section')).toBeVisible();
// Verify league cards are present
const leagueCards = page.getByTestId('league-card');
await expect(leagueCards.first()).toBeVisible();
// Verify league cards show correct metadata
const firstCard = leagueCards.first();
await expect(firstCard.getByTestId('league-card-title')).toBeVisible();
await expect(firstCard.getByTestId('league-card-next-race')).toBeVisible();
await expect(firstCard.getByTestId('league-card-active-drivers')).toBeVisible();
// Verify category filters are present
await expect(page.getByTestId('category-filters')).toBeVisible();
// Verify Quick Join/Follow buttons are present
await expect(page.getByTestId('quick-join-button')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
} finally {
await context.close();
}
});
test('Category filters work correctly', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
await page.goto('/leagues', { waitUntil: 'commit', timeout: 15000 });
// Verify category filters are present
await expect(page.getByTestId('category-filters')).toBeVisible();
// Click on a category filter
const filterButton = page.getByTestId('category-filter-all');
await filterButton.click();
// Wait for filter to apply
await page.waitForTimeout(1000);
// Verify league cards are still visible after filtering
const leagueCards = page.getByTestId('league-card');
await expect(leagueCards.first()).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
});
test.describe('2. /leagues/[id] (Overview Page)', () => {
test('Unauthenticated user can view league overview', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/leagues/');
// Verify league name is displayed
await expect(page.getByTestId('league-detail-title')).toBeVisible();
// Verify stats section displays
await expect(page.getByTestId('league-stats-section')).toBeVisible();
// Verify Next Race countdown displays correctly
await expect(page.getByTestId('next-race-countdown')).toBeVisible();
// Verify Season progress bar shows correct percentage
await expect(page.getByTestId('season-progress-bar')).toBeVisible();
// Verify Activity feed shows recent activity
await expect(page.getByTestId('activity-feed')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
test('Authenticated user can view league overview', async ({ browser, request }) => {
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
try {
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/leagues/');
// Verify league name is displayed
await expect(page.getByTestId('league-detail-title')).toBeVisible();
// Verify stats section displays
await expect(page.getByTestId('league-stats-section')).toBeVisible();
// Verify Next Race countdown displays correctly
await expect(page.getByTestId('next-race-countdown')).toBeVisible();
// Verify Season progress bar shows correct percentage
await expect(page.getByTestId('season-progress-bar')).toBeVisible();
// Verify Activity feed shows recent activity
await expect(page.getByTestId('activity-feed')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
} finally {
await context.close();
}
});
test('Admin user can view admin widgets', async ({ browser, request }) => {
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'admin');
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
try {
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/leagues/');
// Verify admin widgets are visible for authorized users
await expect(page.getByTestId('admin-widgets')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
} finally {
await context.close();
}
});
test('Stats match API values', async ({ page, request }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
// Fetch API data
const apiResponse = await request.get(`${process.env.API_BASE_URL || 'http://api:3000'}/leagues/${WebsiteRouteManager.IDs.LEAGUE}`);
const apiData = await apiResponse.json();
// Navigate to league overview
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
// Verify stats match API values
const membersStat = page.getByTestId('stat-members');
const racesStat = page.getByTestId('stat-races');
const avgSofStat = page.getByTestId('stat-avg-sof');
await expect(membersStat).toBeVisible();
await expect(racesStat).toBeVisible();
await expect(avgSofStat).toBeVisible();
// Verify the stats contain expected values from API
const membersText = await membersStat.textContent();
const racesText = await racesStat.textContent();
const avgSofText = await avgSofStat.textContent();
// Basic validation - stats should not be empty
expect(membersText).toBeTruthy();
expect(racesText).toBeTruthy();
expect(avgSofText).toBeTruthy();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
});
test.describe('3. /leagues/[id]/schedule (Schedule Page)', () => {
const schedulePath = routeManager.resolvePathTemplate('/leagues/[id]/schedule', { id: WebsiteRouteManager.IDs.LEAGUE });
test('Unauthenticated user can view schedule page', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
await page.goto(schedulePath, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/schedule');
// Verify races are grouped by month
await expect(page.getByTestId('schedule-month-group')).toBeVisible();
// Verify race list is present
const raceItems = page.getByTestId('race-item');
await expect(raceItems.first()).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
test('Authenticated user can view schedule page', async ({ browser, request }) => {
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
try {
await page.goto(schedulePath, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/schedule');
// Verify races are grouped by month
await expect(page.getByTestId('schedule-month-group')).toBeVisible();
// Verify race list is present
const raceItems = page.getByTestId('race-item');
await expect(raceItems.first()).toBeVisible();
// Verify Register/Withdraw buttons are present
await expect(page.getByTestId('register-button')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
} finally {
await context.close();
}
});
test('Admin user can view admin controls', async ({ browser, request }) => {
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'admin');
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
try {
await page.goto(schedulePath, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/schedule');
// Verify admin controls are visible for authorized users
await expect(page.getByTestId('admin-controls')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
} finally {
await context.close();
}
});
test('Race detail modal shows correct data', async ({ page, request }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
// Fetch API data
const apiResponse = await request.get(`${process.env.API_BASE_URL || 'http://api:3000'}/leagues/${WebsiteRouteManager.IDs.LEAGUE}/schedule`);
const apiData = await apiResponse.json();
// Navigate to schedule page
await page.goto(schedulePath, { waitUntil: 'commit', timeout: 15000 });
// Click on a race item to open modal
const raceItem = page.getByTestId('race-item').first();
await raceItem.click();
// Verify modal is visible
await expect(page.getByTestId('race-detail-modal')).toBeVisible();
// Verify modal contains race data
const modalContent = page.getByTestId('race-detail-modal');
await expect(modalContent.getByTestId('race-track')).toBeVisible();
await expect(modalContent.getByTestId('race-car')).toBeVisible();
await expect(modalContent.getByTestId('race-date')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
});
test.describe('4. /leagues/[id]/standings (Standings Page)', () => {
const standingsPath = routeManager.resolvePathTemplate('/leagues/[id]/standings', { id: WebsiteRouteManager.IDs.LEAGUE });
test('Unauthenticated user can view standings page', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/standings');
// Verify standings table is present
await expect(page.getByTestId('standings-table')).toBeVisible();
// Verify trend indicators display correctly
await expect(page.getByTestId('trend-indicator')).toBeVisible();
// Verify championship stats show correct data
await expect(page.getByTestId('championship-stats')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
test('Authenticated user can view standings page', async ({ browser, request }) => {
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
try {
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/standings');
// Verify standings table is present
await expect(page.getByTestId('standings-table')).toBeVisible();
// Verify trend indicators display correctly
await expect(page.getByTestId('trend-indicator')).toBeVisible();
// Verify championship stats show correct data
await expect(page.getByTestId('championship-stats')).toBeVisible();
// Verify team standings toggle is present
await expect(page.getByTestId('team-standings-toggle')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
} finally {
await context.close();
}
});
test('Team standings toggle works correctly', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
// Verify team standings toggle is present
await expect(page.getByTestId('team-standings-toggle')).toBeVisible();
// Click on team standings toggle
const toggle = page.getByTestId('team-standings-toggle');
await toggle.click();
// Wait for toggle to apply
await page.waitForTimeout(1000);
// Verify team standings are visible
await expect(page.getByTestId('team-standings-table')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
test('Drop weeks are marked correctly', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
// Verify drop weeks are marked
const dropWeeks = page.getByTestId('drop-week-marker');
await expect(dropWeeks.first()).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
test('Standings data matches API values', async ({ page, request }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
// Fetch API data
const apiResponse = await request.get(`${process.env.API_BASE_URL || 'http://api:3000'}/leagues/${WebsiteRouteManager.IDs.LEAGUE}/standings`);
const apiData = await apiResponse.json();
// Navigate to standings page
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
// Verify standings table is present
await expect(page.getByTestId('standings-table')).toBeVisible();
// Verify table rows match API data
const tableRows = page.getByTestId('standings-row');
const rowCount = await tableRows.count();
// Basic validation - should have at least one row
expect(rowCount).toBeGreaterThan(0);
// Verify first row contains expected data
const firstRow = tableRows.first();
await expect(firstRow.getByTestId('standing-position')).toBeVisible();
await expect(firstRow.getByTestId('standing-driver')).toBeVisible();
await expect(firstRow.getByTestId('standing-points')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
});
test.describe('5. /leagues/[id]/roster (Roster Page)', () => {
const rosterPath = routeManager.resolvePathTemplate('/leagues/[id]/roster', { id: WebsiteRouteManager.IDs.LEAGUE });
test('Unauthenticated user can view roster page', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
await page.goto(rosterPath, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/roster');
// Verify driver cards are present
const driverCards = page.getByTestId('driver-card');
await expect(driverCards.first()).toBeVisible();
// Verify driver cards show correct stats
const firstCard = driverCards.first();
await expect(firstCard.getByTestId('driver-card-name')).toBeVisible();
await expect(firstCard.getByTestId('driver-card-stats')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
test('Authenticated user can view roster page', async ({ browser, request }) => {
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
try {
await page.goto(rosterPath, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/roster');
// Verify driver cards are present
const driverCards = page.getByTestId('driver-card');
await expect(driverCards.first()).toBeVisible();
// Verify driver cards show correct stats
const firstCard = driverCards.first();
await expect(firstCard.getByTestId('driver-card-name')).toBeVisible();
await expect(firstCard.getByTestId('driver-card-stats')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
} finally {
await context.close();
}
});
test('Admin user can view admin actions', async ({ browser, request }) => {
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'admin');
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
try {
await page.goto(rosterPath, { waitUntil: 'commit', timeout: 15000 });
// Verify page loads successfully
expect(page.url()).toContain('/roster');
// Verify admin actions are visible for authorized users
await expect(page.getByTestId('admin-actions')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
} finally {
await context.close();
}
});
test('Roster data matches API values', async ({ page, request }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
// Fetch API data
const apiResponse = await request.get(`${process.env.API_BASE_URL || 'http://api:3000'}/leagues/${WebsiteRouteManager.IDs.LEAGUE}/memberships`);
const apiData = await apiResponse.json();
// Navigate to roster page
await page.goto(rosterPath, { waitUntil: 'commit', timeout: 15000 });
// Verify driver cards are present
const driverCards = page.getByTestId('driver-card');
const cardCount = await driverCards.count();
// Basic validation - should have at least one driver
expect(cardCount).toBeGreaterThan(0);
// Verify first card contains expected data
const firstCard = driverCards.first();
await expect(firstCard.getByTestId('driver-card-name')).toBeVisible();
await expect(firstCard.getByTestId('driver-card-stats')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
});
test.describe('6. Navigation Between League Pages', () => {
test('User can navigate from discovery to league overview', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
// Navigate to leagues discovery page
await page.goto('/leagues', { waitUntil: 'commit', timeout: 15000 });
// Click on a league card
const leagueCard = page.getByTestId('league-card').first();
await leagueCard.click();
// Verify navigation to league overview
await page.waitForURL(/\/leagues\/[^/]+$/, { timeout: 15000 });
expect(page.url()).toMatch(/\/leagues\/[^/]+$/);
// Verify league overview content is visible
await expect(page.getByTestId('league-detail-title')).toBeVisible();
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
test('User can navigate between league sub-pages', async ({ page }) => {
const capture = new ConsoleErrorCapture(page);
capture.setAllowlist(CONSOLE_ALLOWLIST);
// Navigate to league overview
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
// Click on Schedule tab
const scheduleTab = page.getByTestId('schedule-tab');
await scheduleTab.click();
// Verify navigation to schedule page
await page.waitForURL(/\/leagues\/[^/]+\/schedule$/, { timeout: 15000 });
expect(page.url()).toMatch(/\/leagues\/[^/]+\/schedule$/);
// Click on Standings tab
const standingsTab = page.getByTestId('standings-tab');
await standingsTab.click();
// Verify navigation to standings page
await page.waitForURL(/\/leagues\/[^/]+\/standings$/, { timeout: 15000 });
expect(page.url()).toMatch(/\/leagues\/[^/]+\/standings$/);
// Click on Roster tab
const rosterTab = page.getByTestId('roster-tab');
await rosterTab.click();
// Verify navigation to roster page
await page.waitForURL(/\/leagues\/[^/]+\/roster$/, { timeout: 15000 });
expect(page.url()).toMatch(/\/leagues\/[^/]+\/roster$/);
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
});
});
});

View File

@@ -0,0 +1,305 @@
/**
* 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();
});
});
});

View File

@@ -0,0 +1,386 @@
/**
* 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');
});
});
});

View File

@@ -0,0 +1,395 @@
/**
* 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');
});
});
});

View File

@@ -0,0 +1,493 @@
/**
* 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');
});
});
});

View File

@@ -0,0 +1,662 @@
/**
* Integration Tests for LeagueDetailPageQuery
*
* Tests the LeagueDetailPageQuery with mocked API clients to verify:
* - Happy path: API returns valid league detail data
* - Error handling: 404 when league not found
* - Error handling: 500 when API server error
* - Missing data: API returns partial data
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { LeagueDetailPageQuery } from '@/lib/page-queries/LeagueDetailPageQuery';
import { MockLeaguesApiClient } from './mocks/MockLeaguesApiClient';
import { ApiError } from '../../../apps/website/lib/api/base/ApiError';
// Mock data factories
const createMockLeagueDetailData = () => ({
leagues: [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
capacity: 10,
currentMembers: 5,
ownerId: 'driver-1',
status: 'active' as const,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
});
const createMockMembershipsData = () => ({
members: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'Test Driver',
country: 'US',
joinedAt: new Date().toISOString(),
},
role: 'owner' as const,
status: 'active' as const,
joinedAt: new Date().toISOString(),
},
],
});
const createMockRacesPageData = () => ({
races: [
{
id: 'race-1',
track: 'Test Track',
car: 'Test Car',
scheduledAt: new Date().toISOString(),
leagueName: 'Test League',
status: 'scheduled' as const,
strengthOfField: 50,
},
],
});
const createMockDriverData = () => ({
id: 'driver-1',
iracingId: '12345',
name: 'Test Driver',
country: 'US',
joinedAt: new Date().toISOString(),
});
const createMockLeagueConfigData = () => ({
form: {
scoring: {
presetId: 'preset-1',
},
},
});
describe('LeagueDetailPageQuery Integration', () => {
let mockLeaguesApiClient: MockLeaguesApiClient;
beforeEach(() => {
mockLeaguesApiClient = new MockLeaguesApiClient();
});
afterEach(() => {
mockLeaguesApiClient.clearMocks();
});
describe('Happy Path', () => {
it('should return valid league detail data when API returns success', async () => {
// Arrange
const leagueId = 'league-1';
const mockLeaguesData = createMockLeagueDetailData();
const mockMembershipsData = createMockMembershipsData();
const mockRacesPageData = createMockRacesPageData();
const mockDriverData = createMockDriverData();
const mockLeagueConfigData = createMockLeagueConfigData();
// Mock fetch to return different data based on the URL
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve(createMockResponse(mockLeaguesData));
}
if (url.includes('/memberships')) {
return Promise.resolve(createMockResponse(mockMembershipsData));
}
if (url.includes('/races/page-data')) {
return Promise.resolve(createMockResponse(mockRacesPageData));
}
if (url.includes('/drivers/driver-1')) {
return Promise.resolve(createMockResponse(mockDriverData));
}
if (url.includes('/config')) {
return Promise.resolve(createMockResponse(mockLeagueConfigData));
}
return Promise.resolve(createMockErrorResponse(404, 'Not Found', 'Not Found'));
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data).toBeDefined();
expect(data.league).toBeDefined();
expect(data.league.id).toBe('league-1');
expect(data.league.name).toBe('Test League');
expect(data.league.capacity).toBe(10);
expect(data.league.currentMembers).toBe(5);
expect(data.owner).toBeDefined();
expect(data.owner?.id).toBe('driver-1');
expect(data.owner?.name).toBe('Test Driver');
expect(data.memberships).toBeDefined();
expect(data.memberships.members).toBeDefined();
expect(data.memberships.members.length).toBe(1);
expect(data.races).toBeDefined();
expect(data.races.length).toBe(1);
expect(data.races[0].id).toBe('race-1');
expect(data.races[0].name).toBe('Test Track - Test Car');
expect(data.scoringConfig).toBeDefined();
expect(data.scoringConfig?.scoringPresetId).toBe('preset-1');
});
it('should handle league without owner', async () => {
// Arrange
const leagueId = 'league-2';
const mockLeaguesData = {
leagues: [
{
id: 'league-2',
name: 'League Without Owner',
description: 'A league without an owner',
capacity: 15,
currentMembers: 8,
// No ownerId
status: 'active' as const,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
};
const mockMembershipsData = createMockMembershipsData();
const mockRacesPageData = createMockRacesPageData();
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve(createMockResponse(mockLeaguesData));
}
if (url.includes('/memberships')) {
return Promise.resolve(createMockResponse(mockMembershipsData));
}
if (url.includes('/races/page-data')) {
return Promise.resolve(createMockResponse(mockRacesPageData));
}
return Promise.resolve(createMockErrorResponse(404, 'Not Found', 'Not Found'));
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.owner).toBeNull();
expect(data.league.id).toBe('league-2');
expect(data.league.name).toBe('League Without Owner');
});
it('should handle league with no races', async () => {
// Arrange
const leagueId = 'league-3';
const mockLeaguesData = createMockLeagueDetailData();
const mockMembershipsData = createMockMembershipsData();
const mockRacesPageData = { races: [] };
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve(createMockResponse(mockLeaguesData));
}
if (url.includes('/memberships')) {
return Promise.resolve(createMockResponse(mockMembershipsData));
}
if (url.includes('/races/page-data')) {
return Promise.resolve(createMockResponse(mockRacesPageData));
}
return Promise.resolve(createMockErrorResponse(404, 'Not Found', 'Not Found'));
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.races).toBeDefined();
expect(data.races.length).toBe(0);
});
});
describe('Error Handling', () => {
it('should handle 404 error when league not found', async () => {
// Arrange
const leagueId = 'non-existent-league';
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve(createMockResponse({ leagues: [] }));
}
return Promise.resolve(createMockErrorResponse(404, 'Not Found', 'League not found'));
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('notFound');
});
it('should handle 500 error when API server error', async () => {
// Arrange
const leagueId = 'league-1';
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve(createMockErrorResponse(500, 'Internal Server Error', 'Internal Server Error'));
}
return Promise.resolve(createMockErrorResponse(500, 'Internal Server Error', 'Internal Server Error'));
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('serverError');
});
it('should handle network error', async () => {
// Arrange
const leagueId = 'league-1';
global.fetch = vi.fn().mockRejectedValue(new Error('Network error: Unable to reach the API server'));
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('serverError');
});
it('should handle timeout error', async () => {
// Arrange
const leagueId = 'league-1';
const timeoutError = new Error('Request timed out after 30 seconds');
timeoutError.name = 'AbortError';
global.fetch = vi.fn().mockRejectedValue(timeoutError);
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('serverError');
});
it('should handle unauthorized error', async () => {
// Arrange
const leagueId = 'league-1';
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve({
ok: false,
status: 401,
statusText: 'Unauthorized',
text: async () => 'Unauthorized',
});
}
return Promise.resolve({
ok: false,
status: 401,
statusText: 'Unauthorized',
text: async () => 'Unauthorized',
});
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('unauthorized');
});
it('should handle forbidden error', async () => {
// Arrange
const leagueId = 'league-1';
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve({
ok: false,
status: 403,
statusText: 'Forbidden',
text: async () => 'Forbidden',
});
}
return Promise.resolve({
ok: false,
status: 403,
statusText: 'Forbidden',
text: async () => 'Forbidden',
});
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('unauthorized');
});
});
describe('Missing Data', () => {
it('should handle API returning partial data (missing memberships)', async () => {
// Arrange
const leagueId = 'league-1';
const mockLeaguesData = createMockLeagueDetailData();
const mockRacesPageData = createMockRacesPageData();
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve({
ok: true,
json: async () => mockLeaguesData,
});
}
if (url.includes('/memberships')) {
return Promise.resolve({
ok: true,
json: async () => ({ members: [] }),
});
}
if (url.includes('/races/page-data')) {
return Promise.resolve({
ok: true,
json: async () => mockRacesPageData,
});
}
return Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Not Found',
});
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.memberships).toBeDefined();
expect(data.memberships.members).toBeDefined();
expect(data.memberships.members.length).toBe(0);
});
it('should handle API returning partial data (missing races)', async () => {
// Arrange
const leagueId = 'league-1';
const mockLeaguesData = createMockLeagueDetailData();
const mockMembershipsData = createMockMembershipsData();
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve({
ok: true,
json: async () => mockLeaguesData,
});
}
if (url.includes('/memberships')) {
return Promise.resolve({
ok: true,
json: async () => mockMembershipsData,
});
}
if (url.includes('/races/page-data')) {
return Promise.resolve({
ok: true,
json: async () => ({ races: [] }),
});
}
return Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Not Found',
});
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.races).toBeDefined();
expect(data.races.length).toBe(0);
});
it('should handle API returning partial data (missing scoring config)', async () => {
// Arrange
const leagueId = 'league-1';
const mockLeaguesData = createMockLeagueDetailData();
const mockMembershipsData = createMockMembershipsData();
const mockRacesPageData = createMockRacesPageData();
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve({
ok: true,
json: async () => mockLeaguesData,
});
}
if (url.includes('/memberships')) {
return Promise.resolve({
ok: true,
json: async () => mockMembershipsData,
});
}
if (url.includes('/races/page-data')) {
return Promise.resolve({
ok: true,
json: async () => mockRacesPageData,
});
}
if (url.includes('/config')) {
return Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Config not found',
});
}
return Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Not Found',
});
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.scoringConfig).toBeNull();
});
it('should handle API returning partial data (missing owner)', async () => {
// Arrange
const leagueId = 'league-1';
const mockLeaguesData = {
leagues: [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
capacity: 10,
currentMembers: 5,
ownerId: 'driver-1',
status: 'active' as const,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
};
const mockMembershipsData = createMockMembershipsData();
const mockRacesPageData = createMockRacesPageData();
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve({
ok: true,
json: async () => mockLeaguesData,
});
}
if (url.includes('/memberships')) {
return Promise.resolve({
ok: true,
json: async () => mockMembershipsData,
});
}
if (url.includes('/races/page-data')) {
return Promise.resolve({
ok: true,
json: async () => mockRacesPageData,
});
}
if (url.includes('/drivers/driver-1')) {
return Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Driver not found',
});
}
return Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Not Found',
});
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.owner).toBeNull();
});
});
describe('Edge Cases', () => {
it('should handle API returning empty leagues array', async () => {
// Arrange
const leagueId = 'league-1';
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve({
ok: true,
json: async () => ({ leagues: [] }),
});
}
return Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Not Found',
});
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error.type).toBe('notFound');
expect(error.message).toContain('Leagues not found');
});
it('should handle API returning null data', async () => {
// Arrange
const leagueId = 'league-1';
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve({
ok: true,
json: async () => null,
});
}
return Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Not Found',
});
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error.type).toBe('notFound');
expect(error.message).toContain('Leagues not found');
});
it('should handle API returning malformed data', async () => {
// Arrange
const leagueId = 'league-1';
global.fetch = vi.fn((url: string) => {
if (url.includes('/leagues/all-with-capacity-and-scoring')) {
return Promise.resolve({
ok: true,
json: async () => ({ someOtherProperty: 'value' }),
});
}
return Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Not Found',
});
});
// Act
const result = await LeagueDetailPageQuery.execute(leagueId);
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error.type).toBe('notFound');
expect(error.message).toContain('Leagues not found');
});
});
});

View File

@@ -0,0 +1,364 @@
/**
* Integration Tests for LeaguesPageQuery
*
* Tests the LeaguesPageQuery with mocked API clients to verify:
* - Happy path: API returns valid leagues data
* - Error handling: 404 when leagues endpoint not found
* - Error handling: 500 when API server error
* - Empty results: API returns empty leagues list
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { LeaguesPageQuery } from '@/lib/page-queries/LeaguesPageQuery';
// Mock data factories
const createMockLeaguesData = () => ({
leagues: [
{
id: 'league-1',
name: 'Test League 1',
description: 'A test league',
ownerId: 'driver-1',
createdAt: new Date().toISOString(),
usedSlots: 5,
settings: {
maxDrivers: 10,
},
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'driver' as const,
scoringPresetId: 'preset-1',
scoringPresetName: 'Test Preset',
dropPolicySummary: 'No drops',
scoringPatternSummary: 'Standard scoring',
},
},
{
id: 'league-2',
name: 'Test League 2',
description: 'Another test league',
ownerId: 'driver-2',
createdAt: new Date().toISOString(),
usedSlots: 15,
settings: {
maxDrivers: 20,
},
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'driver' as const,
scoringPresetId: 'preset-1',
scoringPresetName: 'Test Preset',
dropPolicySummary: 'No drops',
scoringPatternSummary: 'Standard scoring',
},
},
],
totalCount: 2,
});
const createMockEmptyLeaguesData = () => ({
leagues: [],
});
describe('LeaguesPageQuery Integration', () => {
let originalFetch: typeof global.fetch;
beforeEach(() => {
// Store original fetch to restore later
originalFetch = global.fetch;
});
afterEach(() => {
// Restore original fetch
global.fetch = originalFetch;
});
describe('Happy Path', () => {
it('should return valid leagues data when API returns success', async () => {
// Arrange
const mockData = createMockLeaguesData();
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockData,
text: async () => JSON.stringify(mockData),
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isOk()).toBe(true);
const viewData = result.unwrap();
expect(viewData).toBeDefined();
expect(viewData.leagues).toBeDefined();
expect(viewData.leagues.length).toBe(2);
// Verify first league
expect(viewData.leagues[0].id).toBe('league-1');
expect(viewData.leagues[0].name).toBe('Test League 1');
expect(viewData.leagues[0].settings.maxDrivers).toBe(10);
expect(viewData.leagues[0].usedSlots).toBe(5);
// Verify second league
expect(viewData.leagues[1].id).toBe('league-2');
expect(viewData.leagues[1].name).toBe('Test League 2');
expect(viewData.leagues[1].settings.maxDrivers).toBe(20);
expect(viewData.leagues[1].usedSlots).toBe(15);
});
it('should handle single league correctly', async () => {
// Arrange
const mockData = {
leagues: [
{
id: 'single-league',
name: 'Single League',
description: 'Only one league',
ownerId: 'driver-1',
createdAt: new Date().toISOString(),
usedSlots: 3,
settings: {
maxDrivers: 5,
},
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'driver' as const,
scoringPresetId: 'preset-1',
scoringPresetName: 'Test Preset',
dropPolicySummary: 'No drops',
scoringPatternSummary: 'Standard scoring',
},
},
],
};
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockData,
text: async () => JSON.stringify(mockData),
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isOk()).toBe(true);
const viewData = result.unwrap();
expect(viewData.leagues.length).toBe(1);
expect(viewData.leagues[0].id).toBe('single-league');
expect(viewData.leagues[0].name).toBe('Single League');
});
});
describe('Empty Results', () => {
it('should handle empty leagues list from API', async () => {
// Arrange
const mockData = createMockEmptyLeaguesData();
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockData,
text: async () => JSON.stringify(mockData),
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isOk()).toBe(true);
const viewData = result.unwrap();
expect(viewData).toBeDefined();
expect(viewData.leagues).toBeDefined();
expect(viewData.leagues.length).toBe(0);
});
});
describe('Error Handling', () => {
it('should handle 404 error when leagues endpoint not found', async () => {
// Arrange
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
text: async () => 'Leagues not found',
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('LEAGUES_FETCH_FAILED');
});
it('should handle 500 error when API server error', async () => {
// Arrange
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
statusText: 'Internal Server Error',
text: async () => 'Internal Server Error',
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('LEAGUES_FETCH_FAILED');
});
it('should handle network error', async () => {
// Arrange
global.fetch = vi.fn().mockRejectedValue(new Error('Network error: Unable to reach the API server'));
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('LEAGUES_FETCH_FAILED');
});
it('should handle timeout error', async () => {
// Arrange
const timeoutError = new Error('Request timed out after 30 seconds');
timeoutError.name = 'AbortError';
global.fetch = vi.fn().mockRejectedValue(timeoutError);
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('LEAGUES_FETCH_FAILED');
});
it('should handle unauthorized error (redirect)', async () => {
// Arrange
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 401,
statusText: 'Unauthorized',
text: async () => 'Unauthorized',
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('redirect');
});
it('should handle forbidden error (redirect)', async () => {
// Arrange
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 403,
statusText: 'Forbidden',
text: async () => 'Forbidden',
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('redirect');
});
it('should handle unknown error type', async () => {
// Arrange
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 999,
statusText: 'Unknown Error',
text: async () => 'Unknown error',
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('UNKNOWN_ERROR');
});
});
describe('Edge Cases', () => {
it('should handle API returning null or undefined data', async () => {
// Arrange
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => null,
text: async () => 'null',
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('LEAGUES_FETCH_FAILED');
});
it('should handle API returning malformed data', async () => {
// Arrange
const mockData = {
// Missing 'leagues' property
someOtherProperty: 'value',
};
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockData,
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error).toBe('LEAGUES_FETCH_FAILED');
});
it('should handle API returning leagues with missing required fields', async () => {
// Arrange
const mockData = {
leagues: [
{
id: 'league-1',
name: 'Test League',
// Missing other required fields
},
],
};
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockData,
});
// Act
const result = await LeaguesPageQuery.execute();
// Assert
// Should still succeed - the builder should handle partial data
expect(result.isOk()).toBe(true);
const viewData = result.unwrap();
expect(viewData.leagues.length).toBe(1);
});
});
});

View File

@@ -0,0 +1,149 @@
import { LeaguesApiClient } from '../../../../apps/website/lib/api/leagues/LeaguesApiClient';
import { ApiError } from '../../../../apps/website/lib/api/base/ApiError';
import type { Logger } from '../../../../apps/website/lib/interfaces/Logger';
import type { ErrorReporter } from '../../../../apps/website/lib/interfaces/ErrorReporter';
/**
* Mock LeaguesApiClient for testing
* Allows controlled responses without making actual HTTP calls
*/
export class MockLeaguesApiClient extends LeaguesApiClient {
private mockResponses: Map<string, any> = new Map();
private mockErrors: Map<string, ApiError> = new Map();
constructor(
baseUrl: string = 'http://localhost:3001',
errorReporter: ErrorReporter = {
report: () => {},
} as any,
logger: Logger = {
info: () => {},
warn: () => {},
error: () => {},
} as any
) {
super(baseUrl, errorReporter, logger);
}
/**
* Set a mock response for a specific endpoint
*/
setMockResponse(endpoint: string, response: any): void {
this.mockResponses.set(endpoint, response);
}
/**
* Set a mock error for a specific endpoint
*/
setMockError(endpoint: string, error: ApiError): void {
this.mockErrors.set(endpoint, error);
}
/**
* Clear all mock responses and errors
*/
clearMocks(): void {
this.mockResponses.clear();
this.mockErrors.clear();
}
/**
* Override getAllWithCapacityAndScoring to return mock data
*/
async getAllWithCapacityAndScoring(): Promise<any> {
const endpoint = '/leagues/all-with-capacity-and-scoring';
if (this.mockErrors.has(endpoint)) {
throw this.mockErrors.get(endpoint);
}
if (this.mockResponses.has(endpoint)) {
return this.mockResponses.get(endpoint);
}
// Default mock response
return {
leagues: [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
ownerId: 'driver-1',
createdAt: new Date().toISOString(),
usedSlots: 5,
settings: {
maxDrivers: 10,
},
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'driver',
scoringPresetId: 'preset-1',
scoringPresetName: 'Test Preset',
dropPolicySummary: 'No drops',
scoringPatternSummary: 'Standard scoring',
},
},
],
totalCount: 1,
};
}
/**
* Override getMemberships to return mock data
*/
async getMemberships(leagueId: string): Promise<any> {
const endpoint = `/leagues/${leagueId}/memberships`;
if (this.mockErrors.has(endpoint)) {
throw this.mockErrors.get(endpoint);
}
if (this.mockResponses.has(endpoint)) {
return this.mockResponses.get(endpoint);
}
// Default mock response
return {
members: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'Test Driver',
country: 'US',
joinedAt: new Date().toISOString(),
},
role: 'owner',
status: 'active',
joinedAt: new Date().toISOString(),
},
],
};
}
/**
* Override getLeagueConfig to return mock data
*/
async getLeagueConfig(leagueId: string): Promise<any> {
const endpoint = `/leagues/${leagueId}/config`;
if (this.mockErrors.has(endpoint)) {
throw this.mockErrors.get(endpoint);
}
if (this.mockResponses.has(endpoint)) {
return this.mockResponses.get(endpoint);
}
// Default mock response
return {
form: {
scoring: {
presetId: 'preset-1',
},
},
};
}
}

View File

@@ -20,7 +20,7 @@ export class WebsiteRouteManager {
return mode;
}
private static readonly IDs = {
public static readonly IDs = {
get LEAGUE() { return seedId('league-1', WebsiteRouteManager.getPersistenceMode()); },
get DRIVER() { return seedId('driver-1', WebsiteRouteManager.getPersistenceMode()); },
get TEAM() { return seedId('team-1', WebsiteRouteManager.getPersistenceMode()); },

View File

@@ -0,0 +1,827 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeagueDetailViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeagueDetailViewDataBuilder';
import type { LeagueWithCapacityAndScoringDTO } from '../../../apps/website/lib/types/generated/LeagueWithCapacityAndScoringDTO';
import type { LeagueMembershipsDTO } from '../../../apps/website/lib/types/generated/LeagueMembershipsDTO';
import type { RaceDTO } from '../../../apps/website/lib/types/generated/RaceDTO';
import type { GetDriverOutputDTO } from '../../../apps/website/lib/types/generated/GetDriverOutputDTO';
import type { LeagueScoringConfigDTO } from '../../../apps/website/lib/types/generated/LeagueScoringConfigDTO';
describe('LeagueDetailViewDataBuilder', () => {
const mockLeague: LeagueWithCapacityAndScoringDTO = {
id: 'league-123',
name: 'Test League',
description: 'A test league description',
ownerId: 'owner-456',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
socialLinks: {
discordUrl: 'https://discord.gg/test',
youtubeUrl: 'https://youtube.com/test',
websiteUrl: 'https://test.com',
},
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
logoUrl: 'https://logo.com/test.png',
pendingJoinRequestsCount: 3,
pendingProtestsCount: 1,
walletBalance: 1000,
};
const mockOwner: GetDriverOutputDTO = {
id: 'owner-456',
iracingId: '12345',
name: 'John Doe',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
avatarUrl: 'https://avatar.com/john.png',
rating: 850,
};
const mockScoringConfig: LeagueScoringConfigDTO = {
leagueId: 'league-123',
seasonId: 'season-1',
gameId: 'game-1',
gameName: 'Test Game',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
championships: [],
};
const mockMemberships: LeagueMembershipsDTO = {
members: [
{
driverId: 'owner-456',
driver: {
id: 'owner-456',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
role: 'owner',
joinedAt: '2024-01-01T00:00:00Z',
},
{
driverId: 'admin-789',
driver: {
id: 'admin-789',
name: 'Jane Smith',
iracingId: '67890',
country: 'UK',
joinedAt: '2024-01-02T00:00:00Z',
},
role: 'admin',
joinedAt: '2024-01-02T00:00:00Z',
},
{
driverId: 'steward-101',
driver: {
id: 'steward-101',
name: 'Bob Wilson',
iracingId: '11111',
country: 'CA',
joinedAt: '2024-01-03T00:00:00Z',
},
role: 'steward',
joinedAt: '2024-01-03T00:00:00Z',
},
{
driverId: 'member-202',
driver: {
id: 'member-202',
name: 'Alice Brown',
iracingId: '22222',
country: 'AU',
joinedAt: '2024-01-04T00:00:00Z',
},
role: 'member',
joinedAt: '2024-01-04T00:00:00Z',
},
],
};
const mockSponsors = [
{
id: 'sponsor-1',
name: 'Test Sponsor',
tier: 'main' as const,
logoUrl: 'https://sponsor.com/logo.png',
websiteUrl: 'https://sponsor.com',
tagline: 'Best sponsor ever',
},
];
describe('build()', () => {
it('should transform all input data correctly', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-02-08T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.leagueId).toBe('league-123');
expect(result.name).toBe('Test League');
expect(result.description).toBe('A test league description');
expect(result.logoUrl).toBe('https://logo.com/test.png');
expect(result.walletBalance).toBe(1000);
expect(result.pendingProtestsCount).toBe(1);
expect(result.pendingJoinRequestsCount).toBe(3);
// Check info data
expect(result.info.name).toBe('Test League');
expect(result.info.description).toBe('A test league description');
expect(result.info.membersCount).toBe(4);
expect(result.info.racesCount).toBe(2);
expect(result.info.avgSOF).toBeNull();
expect(result.info.structure).toBe('Solo • 32 max');
expect(result.info.scoring).toBe('preset-1');
expect(result.info.createdAt).toBe('2024-01-01T00:00:00Z');
expect(result.info.discordUrl).toBe('https://discord.gg/test');
expect(result.info.youtubeUrl).toBe('https://youtube.com/test');
expect(result.info.websiteUrl).toBe('https://test.com');
// Check owner summary
expect(result.ownerSummary).not.toBeNull();
expect(result.ownerSummary?.driverId).toBe('owner-456');
expect(result.ownerSummary?.driverName).toBe('John Doe');
expect(result.ownerSummary?.avatarUrl).toBe('https://avatar.com/john.png');
expect(result.ownerSummary?.roleBadgeText).toBe('Owner');
expect(result.ownerSummary?.profileUrl).toBe('/drivers/owner-456');
// Check admin summaries
expect(result.adminSummaries).toHaveLength(1);
expect(result.adminSummaries[0].driverId).toBe('admin-789');
expect(result.adminSummaries[0].roleBadgeText).toBe('Admin');
// Check steward summaries
expect(result.stewardSummaries).toHaveLength(1);
expect(result.stewardSummaries[0].driverId).toBe('steward-101');
expect(result.stewardSummaries[0].roleBadgeText).toBe('Steward');
// Check member summaries
expect(result.memberSummaries).toHaveLength(1);
expect(result.memberSummaries[0].driverId).toBe('member-202');
expect(result.memberSummaries[0].roleBadgeText).toBe('Member');
// Check sponsors
expect(result.sponsors).toHaveLength(1);
expect(result.sponsors[0].id).toBe('sponsor-1');
expect(result.sponsors[0].name).toBe('Test Sponsor');
expect(result.sponsors[0].tier).toBe('main');
// Check running races (empty in this case)
expect(result.runningRaces).toEqual([]);
});
it('should calculate next race correctly', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now
const pastDate = new Date(now.getTime() - 86400000).toISOString(); // 1 day ago
const races: RaceDTO[] = [
{
id: 'race-past',
name: 'Past Race',
date: pastDate,
leagueName: 'Test League',
},
{
id: 'race-future-1',
name: 'Future Race 1',
date: futureDate,
leagueName: 'Test League',
},
{
id: 'race-future-2',
name: 'Future Race 2',
date: new Date(now.getTime() + 172800000).toISOString(), // 2 days from now
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.nextRace).toBeDefined();
expect(result.nextRace?.id).toBe('race-future-1');
expect(result.nextRace?.name).toBe('Future Race 1');
expect(result.nextRace?.date).toBe(futureDate);
});
it('should handle no upcoming races', () => {
const pastDate = new Date(Date.now() - 86400000).toISOString();
const races: RaceDTO[] = [
{
id: 'race-past-1',
name: 'Past Race 1',
date: pastDate,
leagueName: 'Test League',
},
{
id: 'race-past-2',
name: 'Past Race 2',
date: new Date(Date.now() - 172800000).toISOString(),
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.nextRace).toBeUndefined();
});
it('should calculate season progress correctly', () => {
const now = new Date();
const pastDate = new Date(now.getTime() - 86400000).toISOString();
const futureDate = new Date(now.getTime() + 86400000).toISOString();
const races: RaceDTO[] = [
{
id: 'race-past-1',
name: 'Past Race 1',
date: pastDate,
leagueName: 'Test League',
},
{
id: 'race-past-2',
name: 'Past Race 2',
date: new Date(now.getTime() - 172800000).toISOString(),
leagueName: 'Test League',
},
{
id: 'race-future-1',
name: 'Future Race 1',
date: futureDate,
leagueName: 'Test League',
},
{
id: 'race-future-2',
name: 'Future Race 2',
date: new Date(now.getTime() + 172800000).toISOString(),
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.seasonProgress).toBeDefined();
expect(result.seasonProgress?.completedRaces).toBe(2);
expect(result.seasonProgress?.totalRaces).toBe(4);
expect(result.seasonProgress?.percentage).toBe(50);
});
it('should handle no races for season progress', () => {
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races: [],
sponsors: mockSponsors,
});
expect(result.seasonProgress).toBeDefined();
expect(result.seasonProgress?.completedRaces).toBe(0);
expect(result.seasonProgress?.totalRaces).toBe(0);
expect(result.seasonProgress?.percentage).toBe(0);
});
it('should extract recent results from last completed race', () => {
const now = new Date();
const pastDate = new Date(now.getTime() - 86400000).toISOString();
const futureDate = new Date(now.getTime() + 86400000).toISOString();
const races: RaceDTO[] = [
{
id: 'race-past-1',
name: 'Past Race 1',
date: pastDate,
leagueName: 'Test League',
},
{
id: 'race-past-2',
name: 'Past Race 2',
date: new Date(now.getTime() - 172800000).toISOString(),
leagueName: 'Test League',
},
{
id: 'race-future-1',
name: 'Future Race 1',
date: futureDate,
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.recentResults).toBeDefined();
expect(result.recentResults?.length).toBe(2);
expect(result.recentResults?.[0].raceId).toBe('race-past-1');
expect(result.recentResults?.[0].raceName).toBe('Past Race 1');
expect(result.recentResults?.[1].raceId).toBe('race-past-2');
});
it('should handle no completed races for recent results', () => {
const futureDate = new Date(Date.now() + 86400000).toISOString();
const races: RaceDTO[] = [
{
id: 'race-future-1',
name: 'Future Race 1',
date: futureDate,
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.recentResults).toBeDefined();
expect(result.recentResults?.length).toBe(0);
});
it('should handle null owner', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: null,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.ownerSummary).toBeNull();
});
it('should handle null scoring config', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: null,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.info.scoring).toBe('Standard');
});
it('should handle empty memberships', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: { members: [] },
races,
sponsors: mockSponsors,
});
expect(result.info.membersCount).toBe(0);
expect(result.adminSummaries).toHaveLength(0);
expect(result.stewardSummaries).toHaveLength(0);
expect(result.memberSummaries).toHaveLength(0);
});
it('should calculate avgSOF from races with strengthOfField', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-02-08T18:00:00Z',
leagueName: 'Test League',
},
];
// Add strengthOfField to races
(races[0] as any).strengthOfField = 1500;
(races[1] as any).strengthOfField = 1800;
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.info.avgSOF).toBe(1650);
});
it('should ignore races with zero or null strengthOfField', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-02-08T18:00:00Z',
leagueName: 'Test League',
},
];
// Add strengthOfField to races
(races[0] as any).strengthOfField = 0;
(races[1] as any).strengthOfField = null;
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.info.avgSOF).toBeNull();
});
it('should handle empty races array', () => {
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races: [],
sponsors: mockSponsors,
});
expect(result.info.racesCount).toBe(0);
expect(result.info.avgSOF).toBeNull();
expect(result.nextRace).toBeUndefined();
expect(result.seasonProgress?.completedRaces).toBe(0);
expect(result.seasonProgress?.totalRaces).toBe(0);
expect(result.recentResults?.length).toBe(0);
});
it('should handle empty sponsors array', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: [],
});
expect(result.sponsors).toHaveLength(0);
});
it('should handle missing social links', () => {
const leagueWithoutSocialLinks: LeagueWithCapacityAndScoringDTO = {
...mockLeague,
socialLinks: undefined,
};
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: leagueWithoutSocialLinks,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.info.discordUrl).toBeUndefined();
expect(result.info.youtubeUrl).toBeUndefined();
expect(result.info.websiteUrl).toBeUndefined();
});
it('should handle missing category', () => {
const leagueWithoutCategory: LeagueWithCapacityAndScoringDTO = {
...mockLeague,
category: undefined,
};
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: leagueWithoutCategory,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.info).toBeDefined();
});
it('should handle missing description', () => {
const leagueWithoutDescription: LeagueWithCapacityAndScoringDTO = {
...mockLeague,
description: '',
};
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: leagueWithoutDescription,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.description).toBe('');
expect(result.info.description).toBe('');
});
it('should handle missing logoUrl', () => {
const leagueWithoutLogo: LeagueWithCapacityAndScoringDTO = {
...mockLeague,
logoUrl: undefined,
};
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: leagueWithoutLogo,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.logoUrl).toBeUndefined();
});
it('should handle missing admin fields', () => {
const leagueWithoutAdminFields: LeagueWithCapacityAndScoringDTO = {
...mockLeague,
pendingJoinRequestsCount: undefined,
pendingProtestsCount: undefined,
walletBalance: undefined,
};
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: leagueWithoutAdminFields,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.walletBalance).toBeUndefined();
expect(result.pendingProtestsCount).toBeUndefined();
expect(result.pendingJoinRequestsCount).toBeUndefined();
});
it('should extract running races correctly', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Running Race 1',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
{
id: 'race-2',
name: 'Past Race',
date: '2024-01-01T18:00:00Z',
leagueName: 'Test League',
},
{
id: 'race-3',
name: 'Running Race 2',
date: '2024-02-08T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.runningRaces).toHaveLength(2);
expect(result.runningRaces[0].id).toBe('race-1');
expect(result.runningRaces[0].name).toBe('Running Race 1');
expect(result.runningRaces[0].date).toBe('2024-02-01T18:00:00Z');
expect(result.runningRaces[1].id).toBe('race-3');
expect(result.runningRaces[1].name).toBe('Running Race 2');
});
it('should handle no running races', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Past Race 1',
date: '2024-01-01T18:00:00Z',
leagueName: 'Test League',
},
{
id: 'race-2',
name: 'Past Race 2',
date: '2024-01-08T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.runningRaces).toEqual([]);
});
it('should handle races with "Running" in different positions', () => {
const races: RaceDTO[] = [
{
id: 'race-1',
name: 'Race Running',
date: '2024-02-01T18:00:00Z',
leagueName: 'Test League',
},
{
id: 'race-2',
name: 'Running',
date: '2024-02-08T18:00:00Z',
leagueName: 'Test League',
},
{
id: 'race-3',
name: 'Completed Race',
date: '2024-02-15T18:00:00Z',
leagueName: 'Test League',
},
];
const result = LeagueDetailViewDataBuilder.build({
league: mockLeague,
owner: mockOwner,
scoringConfig: mockScoringConfig,
memberships: mockMemberships,
races,
sponsors: mockSponsors,
});
expect(result.runningRaces).toHaveLength(2);
expect(result.runningRaces[0].id).toBe('race-1');
expect(result.runningRaces[1].id).toBe('race-2');
});
});
});

View File

@@ -0,0 +1,386 @@
import { describe, it, expect } from 'vitest';
import { LeagueScheduleViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder';
import type { LeagueScheduleApiDto } from '../../../apps/website/lib/types/tbd/LeagueScheduleApiDto';
describe('LeagueScheduleViewDataBuilder', () => {
const mockApiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
track: 'Track A',
car: 'Car A',
sessionType: 'Qualifying',
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-02-08T18:00:00Z',
track: 'Track B',
car: 'Car B',
sessionType: 'Race',
},
{
id: 'race-3',
name: 'Race 3',
date: '2024-02-15T18:00:00Z',
track: 'Track C',
car: 'Car C',
sessionType: 'Race',
},
],
};
describe('build()', () => {
it('should transform all races correctly', () => {
const result = LeagueScheduleViewDataBuilder.build(mockApiDto);
expect(result.leagueId).toBe('league-123');
expect(result.races).toHaveLength(3);
// Check first race
expect(result.races[0].id).toBe('race-1');
expect(result.races[0].name).toBe('Race 1');
expect(result.races[0].scheduledAt).toBe('2024-02-01T18:00:00Z');
expect(result.races[0].track).toBe('Track A');
expect(result.races[0].car).toBe('Car A');
expect(result.races[0].sessionType).toBe('Qualifying');
});
it('should mark past races correctly', () => {
const now = new Date();
const pastDate = new Date(now.getTime() - 86400000).toISOString(); // 1 day ago
const futureDate = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-past',
name: 'Past Race',
date: pastDate,
track: 'Track A',
car: 'Car A',
sessionType: 'Race',
},
{
id: 'race-future',
name: 'Future Race',
date: futureDate,
track: 'Track B',
car: 'Car B',
sessionType: 'Race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races[0].isPast).toBe(true);
expect(result.races[0].isUpcoming).toBe(false);
expect(result.races[0].status).toBe('completed');
expect(result.races[1].isPast).toBe(false);
expect(result.races[1].isUpcoming).toBe(true);
expect(result.races[1].status).toBe('scheduled');
});
it('should mark upcoming races correctly', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-future',
name: 'Future Race',
date: futureDate,
track: 'Track A',
car: 'Car A',
sessionType: 'Race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races[0].isPast).toBe(false);
expect(result.races[0].isUpcoming).toBe(true);
expect(result.races[0].status).toBe('scheduled');
});
it('should handle empty schedule', () => {
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.leagueId).toBe('league-123');
expect(result.races).toHaveLength(0);
});
it('should handle races with missing optional fields', () => {
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
track: undefined,
car: undefined,
sessionType: undefined,
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races[0].track).toBeUndefined();
expect(result.races[0].car).toBeUndefined();
expect(result.races[0].sessionType).toBeUndefined();
});
it('should handle current driver ID parameter', () => {
const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456');
expect(result.currentDriverId).toBe('driver-456');
});
it('should handle admin permission parameter', () => {
const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456', true);
expect(result.isAdmin).toBe(true);
expect(result.races[0].canEdit).toBe(true);
expect(result.races[0].canReschedule).toBe(true);
});
it('should handle non-admin permission parameter', () => {
const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456', false);
expect(result.isAdmin).toBe(false);
expect(result.races[0].canEdit).toBe(false);
expect(result.races[0].canReschedule).toBe(false);
});
it('should handle default admin parameter as false', () => {
const result = LeagueScheduleViewDataBuilder.build(mockApiDto, 'driver-456');
expect(result.isAdmin).toBe(false);
expect(result.races[0].canEdit).toBe(false);
expect(result.races[0].canReschedule).toBe(false);
});
it('should handle registration status for upcoming races', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 86400000).toISOString();
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-future',
name: 'Future Race',
date: futureDate,
track: 'Track A',
car: 'Car A',
sessionType: 'Race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races[0].isUserRegistered).toBe(false);
expect(result.races[0].canRegister).toBe(true);
});
it('should handle registration status for past races', () => {
const now = new Date();
const pastDate = new Date(now.getTime() - 86400000).toISOString();
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-past',
name: 'Past Race',
date: pastDate,
track: 'Track A',
car: 'Car A',
sessionType: 'Race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races[0].isUserRegistered).toBe(false);
expect(result.races[0].canRegister).toBe(false);
});
it('should handle races exactly at current time', () => {
const now = new Date();
const exactDate = now.toISOString();
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-exact',
name: 'Exact Race',
date: exactDate,
track: 'Track A',
car: 'Car A',
sessionType: 'Race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
// Race at exact current time is considered upcoming (not past)
// because the comparison uses < (strictly less than)
expect(result.races[0].isPast).toBe(false);
expect(result.races[0].isUpcoming).toBe(true);
expect(result.races[0].status).toBe('scheduled');
});
it('should handle races with different session types', () => {
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-qualifying',
name: 'Qualifying',
date: '2024-02-01T18:00:00Z',
track: 'Track A',
car: 'Car A',
sessionType: 'Qualifying',
},
{
id: 'race-practice',
name: 'Practice',
date: '2024-02-02T18:00:00Z',
track: 'Track B',
car: 'Car B',
sessionType: 'Practice',
},
{
id: 'race-race',
name: 'Race',
date: '2024-02-03T18:00:00Z',
track: 'Track C',
car: 'Car C',
sessionType: 'Race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races[0].sessionType).toBe('Qualifying');
expect(result.races[1].sessionType).toBe('Practice');
expect(result.races[2].sessionType).toBe('Race');
});
it('should handle races without session type', () => {
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
track: 'Track A',
car: 'Car A',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races[0].sessionType).toBeUndefined();
});
it('should handle races with empty track and car', () => {
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-02-01T18:00:00Z',
track: '',
car: '',
sessionType: 'Race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races[0].track).toBe('');
expect(result.races[0].car).toBe('');
});
it('should handle multiple races with mixed dates', () => {
const now = new Date();
const pastDate1 = new Date(now.getTime() - 172800000).toISOString(); // 2 days ago
const pastDate2 = new Date(now.getTime() - 86400000).toISOString(); // 1 day ago
const futureDate1 = new Date(now.getTime() + 86400000).toISOString(); // 1 day from now
const futureDate2 = new Date(now.getTime() + 172800000).toISOString(); // 2 days from now
const apiDto: LeagueScheduleApiDto = {
leagueId: 'league-123',
races: [
{
id: 'race-past-2',
name: 'Past Race 2',
date: pastDate1,
track: 'Track A',
car: 'Car A',
sessionType: 'Race',
},
{
id: 'race-past-1',
name: 'Past Race 1',
date: pastDate2,
track: 'Track B',
car: 'Car B',
sessionType: 'Race',
},
{
id: 'race-future-1',
name: 'Future Race 1',
date: futureDate1,
track: 'Track C',
car: 'Car C',
sessionType: 'Race',
},
{
id: 'race-future-2',
name: 'Future Race 2',
date: futureDate2,
track: 'Track D',
car: 'Car D',
sessionType: 'Race',
},
],
};
const result = LeagueScheduleViewDataBuilder.build(apiDto);
expect(result.races).toHaveLength(4);
expect(result.races[0].isPast).toBe(true);
expect(result.races[1].isPast).toBe(true);
expect(result.races[2].isPast).toBe(false);
expect(result.races[3].isPast).toBe(false);
});
});
});

View File

@@ -0,0 +1,541 @@
import { describe, it, expect } from 'vitest';
import { LeagueStandingsViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeagueStandingsViewDataBuilder';
import type { LeagueStandingDTO } from '../../../apps/website/lib/types/generated/LeagueStandingDTO';
import type { LeagueMemberDTO } from '../../../apps/website/lib/types/generated/LeagueMemberDTO';
describe('LeagueStandingsViewDataBuilder', () => {
const mockStandings: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 150,
position: 1,
wins: 3,
podiums: 5,
races: 10,
positionChange: 0,
lastRacePoints: 25,
droppedRaceIds: ['race-1', 'race-2'],
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
name: 'Jane Smith',
iracingId: '67890',
country: 'UK',
joinedAt: '2024-01-02T00:00:00Z',
},
points: 120,
position: 2,
wins: 2,
podiums: 4,
races: 10,
positionChange: 1,
lastRacePoints: 18,
droppedRaceIds: ['race-3'],
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
name: 'Bob Wilson',
iracingId: '11111',
country: 'CA',
joinedAt: '2024-01-03T00:00:00Z',
},
points: 90,
position: 3,
wins: 1,
podiums: 3,
races: 10,
positionChange: -1,
lastRacePoints: 12,
droppedRaceIds: [],
},
];
const mockMemberships: LeagueMemberDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
role: 'member',
joinedAt: '2024-01-01T00:00:00Z',
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
name: 'Jane Smith',
iracingId: '67890',
country: 'UK',
joinedAt: '2024-01-02T00:00:00Z',
},
role: 'member',
joinedAt: '2024-01-02T00:00:00Z',
},
];
describe('build()', () => {
it('should transform standings correctly', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: mockMemberships },
'league-123'
);
expect(result.leagueId).toBe('league-123');
expect(result.standings).toHaveLength(3);
// Check first standing
expect(result.standings[0].driverId).toBe('driver-1');
expect(result.standings[0].position).toBe(1);
expect(result.standings[0].totalPoints).toBe(150);
expect(result.standings[0].racesFinished).toBe(10);
expect(result.standings[0].racesStarted).toBe(10);
expect(result.standings[0].positionChange).toBe(0);
expect(result.standings[0].lastRacePoints).toBe(25);
expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']);
expect(result.standings[0].wins).toBe(3);
expect(result.standings[0].podiums).toBe(5);
});
it('should calculate position change correctly', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].positionChange).toBe(0); // No change
expect(result.standings[1].positionChange).toBe(1); // Moved up
expect(result.standings[2].positionChange).toBe(-1); // Moved down
});
it('should map last race points correctly', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].lastRacePoints).toBe(25);
expect(result.standings[1].lastRacePoints).toBe(18);
expect(result.standings[2].lastRacePoints).toBe(12);
});
it('should handle dropped race IDs correctly', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']);
expect(result.standings[1].droppedRaceIds).toEqual(['race-3']);
expect(result.standings[2].droppedRaceIds).toEqual([]);
});
it('should calculate championship stats (wins, podiums)', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].wins).toBe(3);
expect(result.standings[0].podiums).toBe(5);
expect(result.standings[1].wins).toBe(2);
expect(result.standings[1].podiums).toBe(4);
expect(result.standings[2].wins).toBe(1);
expect(result.standings[2].podiums).toBe(3);
});
it('should extract driver metadata correctly', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: mockMemberships },
'league-123'
);
expect(result.drivers).toHaveLength(3);
// Check first driver
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[0].name).toBe('John Doe');
expect(result.drivers[0].iracingId).toBe('12345');
expect(result.drivers[0].country).toBe('US');
});
it('should convert memberships correctly', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: mockMemberships },
'league-123'
);
expect(result.memberships).toHaveLength(2);
// Check first membership
expect(result.memberships[0].driverId).toBe('driver-1');
expect(result.memberships[0].leagueId).toBe('league-123');
expect(result.memberships[0].role).toBe('member');
expect(result.memberships[0].status).toBe('active');
});
it('should handle empty standings', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: [] },
{ members: mockMemberships },
'league-123'
);
expect(result.standings).toHaveLength(0);
expect(result.drivers).toHaveLength(0);
});
it('should handle empty memberships', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: [] },
'league-123'
);
expect(result.memberships).toHaveLength(0);
});
it('should handle missing driver objects in standings', () => {
const standingsWithMissingDriver: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 150,
position: 1,
wins: 3,
podiums: 5,
races: 10,
positionChange: 0,
lastRacePoints: 25,
droppedRaceIds: [],
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
name: 'Jane Smith',
iracingId: '67890',
country: 'UK',
joinedAt: '2024-01-02T00:00:00Z',
},
points: 120,
position: 2,
wins: 2,
podiums: 4,
races: 10,
positionChange: 1,
lastRacePoints: 18,
droppedRaceIds: [],
},
];
const result = LeagueStandingsViewDataBuilder.build(
{ standings: standingsWithMissingDriver },
{ members: mockMemberships },
'league-123'
);
expect(result.drivers).toHaveLength(2);
expect(result.drivers[0].id).toBe('driver-1');
expect(result.drivers[1].id).toBe('driver-2');
});
it('should handle standings with missing positionChange', () => {
const standingsWithoutPositionChange: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 150,
position: 1,
wins: 3,
podiums: 5,
races: 10,
positionChange: undefined as any,
lastRacePoints: 25,
droppedRaceIds: [],
},
];
const result = LeagueStandingsViewDataBuilder.build(
{ standings: standingsWithoutPositionChange },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].positionChange).toBe(0);
});
it('should handle standings with missing lastRacePoints', () => {
const standingsWithoutLastRacePoints: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 150,
position: 1,
wins: 3,
podiums: 5,
races: 10,
positionChange: 0,
lastRacePoints: undefined as any,
droppedRaceIds: [],
},
];
const result = LeagueStandingsViewDataBuilder.build(
{ standings: standingsWithoutLastRacePoints },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].lastRacePoints).toBe(0);
});
it('should handle standings with missing droppedRaceIds', () => {
const standingsWithoutDroppedRaceIds: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 150,
position: 1,
wins: 3,
podiums: 5,
races: 10,
positionChange: 0,
lastRacePoints: 25,
droppedRaceIds: undefined as any,
},
];
const result = LeagueStandingsViewDataBuilder.build(
{ standings: standingsWithoutDroppedRaceIds },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].droppedRaceIds).toEqual([]);
});
it('should handle standings with missing wins', () => {
const standingsWithoutWins: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 150,
position: 1,
wins: undefined as any,
podiums: 5,
races: 10,
positionChange: 0,
lastRacePoints: 25,
droppedRaceIds: [],
},
];
const result = LeagueStandingsViewDataBuilder.build(
{ standings: standingsWithoutWins },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].wins).toBe(0);
});
it('should handle standings with missing podiums', () => {
const standingsWithoutPodiums: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 150,
position: 1,
wins: 3,
podiums: undefined as any,
races: 10,
positionChange: 0,
lastRacePoints: 25,
droppedRaceIds: [],
},
];
const result = LeagueStandingsViewDataBuilder.build(
{ standings: standingsWithoutPodiums },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].podiums).toBe(0);
});
it('should handle team championship mode', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: mockMemberships },
'league-123',
true
);
expect(result.isTeamChampionship).toBe(true);
});
it('should handle non-team championship mode by default', () => {
const result = LeagueStandingsViewDataBuilder.build(
{ standings: mockStandings },
{ members: mockMemberships },
'league-123'
);
expect(result.isTeamChampionship).toBe(false);
});
it('should handle standings with zero points', () => {
const standingsWithZeroPoints: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 0,
position: 1,
wins: 0,
podiums: 0,
races: 10,
positionChange: 0,
lastRacePoints: 0,
droppedRaceIds: [],
},
];
const result = LeagueStandingsViewDataBuilder.build(
{ standings: standingsWithZeroPoints },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].totalPoints).toBe(0);
expect(result.standings[0].wins).toBe(0);
expect(result.standings[0].podiums).toBe(0);
});
it('should handle standings with negative position change', () => {
const standingsWithNegativeChange: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 150,
position: 1,
wins: 3,
podiums: 5,
races: 10,
positionChange: -2,
lastRacePoints: 25,
droppedRaceIds: [],
},
];
const result = LeagueStandingsViewDataBuilder.build(
{ standings: standingsWithNegativeChange },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].positionChange).toBe(-2);
});
it('should handle standings with positive position change', () => {
const standingsWithPositiveChange: LeagueStandingDTO[] = [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
points: 150,
position: 1,
wins: 3,
podiums: 5,
races: 10,
positionChange: 3,
lastRacePoints: 25,
droppedRaceIds: [],
},
];
const result = LeagueStandingsViewDataBuilder.build(
{ standings: standingsWithPositiveChange },
{ members: mockMemberships },
'league-123'
);
expect(result.standings[0].positionChange).toBe(3);
});
});
});

View File

@@ -0,0 +1,932 @@
import { describe, it, expect } from 'vitest';
import { LeaguesViewDataBuilder } from '../../../apps/website/lib/builders/view-data/LeaguesViewDataBuilder';
import type { AllLeaguesWithCapacityAndScoringDTO } from '../../../apps/website/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO';
import type { LeagueWithCapacityAndScoringDTO } from '../../../apps/website/lib/types/generated/LeagueWithCapacityAndScoringDTO';
describe('LeaguesViewDataBuilder', () => {
const mockLeagues: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League 1',
description: 'A test league description',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
socialLinks: {
discordUrl: 'https://discord.gg/test1',
youtubeUrl: 'https://youtube.com/test1',
websiteUrl: 'https://test1.com',
},
scoring: {
gameId: 'game-1',
gameName: 'Test Game 1',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
logoUrl: 'https://logo.com/test1.png',
pendingJoinRequestsCount: 3,
pendingProtestsCount: 1,
walletBalance: 1000,
},
{
id: 'league-2',
name: 'Test League 2',
description: 'Another test league',
ownerId: 'owner-2',
createdAt: '2024-01-02T00:00:00Z',
settings: {
maxDrivers: 16,
qualifyingFormat: 'Team',
},
usedSlots: 8,
category: 'Oval',
socialLinks: {
discordUrl: 'https://discord.gg/test2',
},
scoring: {
gameId: 'game-2',
gameName: 'Test Game 2',
primaryChampionshipType: 'Team',
scoringPresetId: 'preset-2',
scoringPresetName: 'Advanced',
dropPolicySummary: 'Drop 1 worst race',
scoringPatternSummary: 'Points based on finish position with bonuses',
},
timingSummary: 'Every Saturday at 7 PM',
logoUrl: 'https://logo.com/test2.png',
},
{
id: 'league-3',
name: 'Test League 3',
description: 'A third test league',
ownerId: 'owner-3',
createdAt: '2024-01-03T00:00:00Z',
settings: {
maxDrivers: 24,
qualifyingFormat: 'Solo',
},
usedSlots: 24,
category: 'Road',
scoring: {
gameId: 'game-3',
gameName: 'Test Game 3',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-3',
scoringPresetName: 'Custom',
dropPolicySummary: 'No drops',
scoringPatternSummary: 'Fixed points per position',
},
timingSummary: 'Every Friday at 9 PM',
},
];
describe('build()', () => {
it('should transform all leagues correctly', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues).toHaveLength(3);
// Check first league
expect(result.leagues[0].id).toBe('league-1');
expect(result.leagues[0].name).toBe('Test League 1');
expect(result.leagues[0].description).toBe('A test league description');
expect(result.leagues[0].logoUrl).toBe('https://logo.com/test1.png');
expect(result.leagues[0].ownerId).toBe('owner-1');
expect(result.leagues[0].createdAt).toBe('2024-01-01T00:00:00Z');
expect(result.leagues[0].maxDrivers).toBe(32);
expect(result.leagues[0].usedDriverSlots).toBe(15);
expect(result.leagues[0].structureSummary).toBe('Solo');
expect(result.leagues[0].timingSummary).toBe('Every Sunday at 8 PM');
expect(result.leagues[0].category).toBe('Road');
// Check scoring
expect(result.leagues[0].scoring).toBeDefined();
expect(result.leagues[0].scoring?.gameId).toBe('game-1');
expect(result.leagues[0].scoring?.gameName).toBe('Test Game 1');
expect(result.leagues[0].scoring?.primaryChampionshipType).toBe('Solo');
expect(result.leagues[0].scoring?.scoringPresetId).toBe('preset-1');
expect(result.leagues[0].scoring?.scoringPresetName).toBe('Standard');
expect(result.leagues[0].scoring?.dropPolicySummary).toBe('Drop 2 worst races');
expect(result.leagues[0].scoring?.scoringPatternSummary).toBe('Points based on finish position');
});
it('should handle leagues with missing description', () => {
const leaguesWithoutDescription: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League',
description: '',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithoutDescription,
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].description).toBe(null);
});
it('should handle leagues with missing logoUrl', () => {
const leaguesWithoutLogo: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithoutLogo,
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].logoUrl).toBe(null);
});
it('should handle leagues with missing category', () => {
const leaguesWithoutCategory: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithoutCategory,
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].category).toBe(null);
});
it('should handle leagues with missing scoring', () => {
const leaguesWithoutScoring: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
timingSummary: 'Every Sunday at 8 PM',
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithoutScoring,
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].scoring).toBeUndefined();
});
it('should handle leagues with missing social links', () => {
const leaguesWithoutSocialLinks: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithoutSocialLinks,
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0]).toBeDefined();
});
it('should handle leagues with missing timingSummary', () => {
const leaguesWithoutTimingSummary: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithoutTimingSummary,
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].timingSummary).toBe('');
});
it('should handle empty leagues array', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: [],
totalCount: 0,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues).toHaveLength(0);
});
it('should handle leagues with different categories', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].category).toBe('Road');
expect(result.leagues[1].category).toBe('Oval');
expect(result.leagues[2].category).toBe('Road');
});
it('should handle leagues with different structures', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].structureSummary).toBe('Solo');
expect(result.leagues[1].structureSummary).toBe('Team');
expect(result.leagues[2].structureSummary).toBe('Solo');
});
it('should handle leagues with different scoring presets', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].scoring?.scoringPresetName).toBe('Standard');
expect(result.leagues[1].scoring?.scoringPresetName).toBe('Advanced');
expect(result.leagues[2].scoring?.scoringPresetName).toBe('Custom');
});
it('should handle leagues with different drop policies', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].scoring?.dropPolicySummary).toBe('Drop 2 worst races');
expect(result.leagues[1].scoring?.dropPolicySummary).toBe('Drop 1 worst race');
expect(result.leagues[2].scoring?.dropPolicySummary).toBe('No drops');
});
it('should handle leagues with different scoring patterns', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].scoring?.scoringPatternSummary).toBe('Points based on finish position');
expect(result.leagues[1].scoring?.scoringPatternSummary).toBe('Points based on finish position with bonuses');
expect(result.leagues[2].scoring?.scoringPatternSummary).toBe('Fixed points per position');
});
it('should handle leagues with different primary championship types', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].scoring?.primaryChampionshipType).toBe('Solo');
expect(result.leagues[1].scoring?.primaryChampionshipType).toBe('Team');
expect(result.leagues[2].scoring?.primaryChampionshipType).toBe('Solo');
});
it('should handle leagues with different game names', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].scoring?.gameName).toBe('Test Game 1');
expect(result.leagues[1].scoring?.gameName).toBe('Test Game 2');
expect(result.leagues[2].scoring?.gameName).toBe('Test Game 3');
});
it('should handle leagues with different game IDs', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].scoring?.gameId).toBe('game-1');
expect(result.leagues[1].scoring?.gameId).toBe('game-2');
expect(result.leagues[2].scoring?.gameId).toBe('game-3');
});
it('should handle leagues with different max drivers', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].maxDrivers).toBe(32);
expect(result.leagues[1].maxDrivers).toBe(16);
expect(result.leagues[2].maxDrivers).toBe(24);
});
it('should handle leagues with different used slots', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].usedDriverSlots).toBe(15);
expect(result.leagues[1].usedDriverSlots).toBe(8);
expect(result.leagues[2].usedDriverSlots).toBe(24);
});
it('should handle leagues with different owners', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].ownerId).toBe('owner-1');
expect(result.leagues[1].ownerId).toBe('owner-2');
expect(result.leagues[2].ownerId).toBe('owner-3');
});
it('should handle leagues with different creation dates', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].createdAt).toBe('2024-01-01T00:00:00Z');
expect(result.leagues[1].createdAt).toBe('2024-01-02T00:00:00Z');
expect(result.leagues[2].createdAt).toBe('2024-01-03T00:00:00Z');
});
it('should handle leagues with different timing summaries', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].timingSummary).toBe('Every Sunday at 8 PM');
expect(result.leagues[1].timingSummary).toBe('Every Saturday at 7 PM');
expect(result.leagues[2].timingSummary).toBe('Every Friday at 9 PM');
});
it('should handle leagues with different names', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].name).toBe('Test League 1');
expect(result.leagues[1].name).toBe('Test League 2');
expect(result.leagues[2].name).toBe('Test League 3');
});
it('should handle leagues with different descriptions', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].description).toBe('A test league description');
expect(result.leagues[1].description).toBe('Another test league');
expect(result.leagues[2].description).toBe('A third test league');
});
it('should handle leagues with different logo URLs', () => {
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: mockLeagues,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].logoUrl).toBe('https://logo.com/test1.png');
expect(result.leagues[1].logoUrl).toBe('https://logo.com/test2.png');
expect(result.leagues[2].logoUrl).toBeNull();
});
it('should handle leagues with activeDriversCount', () => {
const leaguesWithActiveDrivers: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
];
// Add activeDriversCount to the league
(leaguesWithActiveDrivers[0] as any).activeDriversCount = 12;
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithActiveDrivers,
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].activeDriversCount).toBe(12);
});
it('should handle leagues with nextRaceAt', () => {
const leaguesWithNextRace: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
];
// Add nextRaceAt to the league
(leaguesWithNextRace[0] as any).nextRaceAt = '2024-02-01T18:00:00Z';
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithNextRace,
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].nextRaceAt).toBe('2024-02-01T18:00:00Z');
});
it('should handle leagues without activeDriversCount and nextRaceAt', () => {
const leaguesWithoutMetadata: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Test League',
description: 'A test league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithoutMetadata,
totalCount: 1,
};
const result = LeaguesViewDataBuilder.build(apiDto);
expect(result.leagues[0].activeDriversCount).toBeUndefined();
expect(result.leagues[0].nextRaceAt).toBeUndefined();
});
it('should handle leagues with different usedDriverSlots for featured leagues', () => {
const leaguesWithDifferentSlots: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Small League',
description: 'A small league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 16,
qualifyingFormat: 'Solo',
},
usedSlots: 8,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
{
id: 'league-2',
name: 'Large League',
description: 'A large league',
ownerId: 'owner-2',
createdAt: '2024-01-02T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 25,
category: 'Road',
scoring: {
gameId: 'game-2',
gameName: 'Test Game 2',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-2',
scoringPresetName: 'Advanced',
dropPolicySummary: 'Drop 1 worst race',
scoringPatternSummary: 'Points based on finish position with bonuses',
},
timingSummary: 'Every Saturday at 7 PM',
},
{
id: 'league-3',
name: 'Medium League',
description: 'A medium league',
ownerId: 'owner-3',
createdAt: '2024-01-03T00:00:00Z',
settings: {
maxDrivers: 24,
qualifyingFormat: 'Team',
},
usedSlots: 20,
category: 'Oval',
scoring: {
gameId: 'game-3',
gameName: 'Test Game 3',
primaryChampionshipType: 'Team',
scoringPresetId: 'preset-3',
scoringPresetName: 'Custom',
dropPolicySummary: 'No drops',
scoringPatternSummary: 'Fixed points per position',
},
timingSummary: 'Every Friday at 9 PM',
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithDifferentSlots,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
// Verify that usedDriverSlots is correctly mapped
expect(result.leagues[0].usedDriverSlots).toBe(8);
expect(result.leagues[1].usedDriverSlots).toBe(25);
expect(result.leagues[2].usedDriverSlots).toBe(20);
// Verify that leagues can be filtered for featured leagues (usedDriverSlots > 20)
const featuredLeagues = result.leagues.filter(l => (l.usedDriverSlots ?? 0) > 20);
expect(featuredLeagues).toHaveLength(1);
expect(featuredLeagues[0].id).toBe('league-2');
});
it('should handle leagues with different categories for filtering', () => {
const leaguesWithDifferentCategories: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'Road League 1',
description: 'A road league',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
{
id: 'league-2',
name: 'Oval League 1',
description: 'An oval league',
ownerId: 'owner-2',
createdAt: '2024-01-02T00:00:00Z',
settings: {
maxDrivers: 16,
qualifyingFormat: 'Solo',
},
usedSlots: 8,
category: 'Oval',
scoring: {
gameId: 'game-2',
gameName: 'Test Game 2',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-2',
scoringPresetName: 'Advanced',
dropPolicySummary: 'Drop 1 worst race',
scoringPatternSummary: 'Points based on finish position with bonuses',
},
timingSummary: 'Every Saturday at 7 PM',
},
{
id: 'league-3',
name: 'Road League 2',
description: 'Another road league',
ownerId: 'owner-3',
createdAt: '2024-01-03T00:00:00Z',
settings: {
maxDrivers: 24,
qualifyingFormat: 'Team',
},
usedSlots: 20,
category: 'Road',
scoring: {
gameId: 'game-3',
gameName: 'Test Game 3',
primaryChampionshipType: 'Team',
scoringPresetId: 'preset-3',
scoringPresetName: 'Custom',
dropPolicySummary: 'No drops',
scoringPatternSummary: 'Fixed points per position',
},
timingSummary: 'Every Friday at 9 PM',
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithDifferentCategories,
totalCount: 3,
};
const result = LeaguesViewDataBuilder.build(apiDto);
// Verify that category is correctly mapped
expect(result.leagues[0].category).toBe('Road');
expect(result.leagues[1].category).toBe('Oval');
expect(result.leagues[2].category).toBe('Road');
// Verify that leagues can be filtered by category
const roadLeagues = result.leagues.filter(l => l.category === 'Road');
expect(roadLeagues).toHaveLength(2);
expect(roadLeagues[0].id).toBe('league-1');
expect(roadLeagues[1].id).toBe('league-3');
const ovalLeagues = result.leagues.filter(l => l.category === 'Oval');
expect(ovalLeagues).toHaveLength(1);
expect(ovalLeagues[0].id).toBe('league-2');
});
it('should handle leagues with null category for filtering', () => {
const leaguesWithNullCategory: LeagueWithCapacityAndScoringDTO[] = [
{
id: 'league-1',
name: 'League with Category',
description: 'A league with category',
ownerId: 'owner-1',
createdAt: '2024-01-01T00:00:00Z',
settings: {
maxDrivers: 32,
qualifyingFormat: 'Solo',
},
usedSlots: 15,
category: 'Road',
scoring: {
gameId: 'game-1',
gameName: 'Test Game',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-1',
scoringPresetName: 'Standard',
dropPolicySummary: 'Drop 2 worst races',
scoringPatternSummary: 'Points based on finish position',
},
timingSummary: 'Every Sunday at 8 PM',
},
{
id: 'league-2',
name: 'League without Category',
description: 'A league without category',
ownerId: 'owner-2',
createdAt: '2024-01-02T00:00:00Z',
settings: {
maxDrivers: 16,
qualifyingFormat: 'Solo',
},
usedSlots: 8,
scoring: {
gameId: 'game-2',
gameName: 'Test Game 2',
primaryChampionshipType: 'Solo',
scoringPresetId: 'preset-2',
scoringPresetName: 'Advanced',
dropPolicySummary: 'Drop 1 worst race',
scoringPatternSummary: 'Points based on finish position with bonuses',
},
timingSummary: 'Every Saturday at 7 PM',
},
];
const apiDto: AllLeaguesWithCapacityAndScoringDTO = {
leagues: leaguesWithNullCategory,
totalCount: 2,
};
const result = LeaguesViewDataBuilder.build(apiDto);
// Verify that null category is handled correctly
expect(result.leagues[0].category).toBe('Road');
expect(result.leagues[1].category).toBe(null);
// Verify that leagues can be filtered by category (null category should be filterable)
const roadLeagues = result.leagues.filter(l => l.category === 'Road');
expect(roadLeagues).toHaveLength(1);
expect(roadLeagues[0].id).toBe('league-1');
const noCategoryLeagues = result.leagues.filter(l => l.category === null);
expect(noCategoryLeagues).toHaveLength(1);
expect(noCategoryLeagues[0].id).toBe('league-2');
});
});
});