website refactor
This commit is contained in:
782
tests/e2e/api/league-api.test.ts
Normal file
782
tests/e2e/api/league-api.test.ts
Normal 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`);
|
||||
}
|
||||
});
|
||||
628
tests/e2e/website/league-pages.e2e.test.ts
Normal file
628
tests/e2e/website/league-pages.e2e.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
305
tests/integration/league/members-data-flow.integration.test.ts
Normal file
305
tests/integration/league/members-data-flow.integration.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
386
tests/integration/league/schedule-data-flow.integration.test.ts
Normal file
386
tests/integration/league/schedule-data-flow.integration.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
395
tests/integration/league/standings-data-flow.integration.test.ts
Normal file
395
tests/integration/league/standings-data-flow.integration.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
493
tests/integration/league/stats-data-flow.integration.test.ts
Normal file
493
tests/integration/league/stats-data-flow.integration.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
364
tests/integration/website/LeaguesPageQuery.integration.test.ts
Normal file
364
tests/integration/website/LeaguesPageQuery.integration.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
149
tests/integration/website/mocks/MockLeaguesApiClient.ts
Normal file
149
tests/integration/website/mocks/MockLeaguesApiClient.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()); },
|
||||
|
||||
827
tests/unit/website/LeagueDetailViewDataBuilder.test.ts
Normal file
827
tests/unit/website/LeagueDetailViewDataBuilder.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
386
tests/unit/website/LeagueScheduleViewDataBuilder.test.ts
Normal file
386
tests/unit/website/LeagueScheduleViewDataBuilder.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
541
tests/unit/website/LeagueStandingsViewDataBuilder.test.ts
Normal file
541
tests/unit/website/LeagueStandingsViewDataBuilder.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
932
tests/unit/website/LeaguesViewDataBuilder.test.ts
Normal file
932
tests/unit/website/LeaguesViewDataBuilder.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user