1886 lines
59 KiB
TypeScript
1886 lines
59 KiB
TypeScript
/**
|
|
* View Data Layer Tests - Leagues Functionality
|
|
*
|
|
* This test file covers the view data layer for leagues functionality.
|
|
*
|
|
* The view data layer is responsible for:
|
|
* - DTO → UI model mapping
|
|
* - Formatting, sorting, and grouping
|
|
* - Derived fields and defaults
|
|
* - UI-specific semantics
|
|
*
|
|
* This layer isolates the UI from API churn by providing a stable interface
|
|
* between the API layer and the presentation layer.
|
|
*
|
|
* Test coverage includes:
|
|
* - League list data transformation and sorting
|
|
* - Individual league profile view models
|
|
* - League roster data formatting and member management
|
|
* - League schedule and standings view models
|
|
* - League stewarding and protest handling data transformation
|
|
* - League wallet and sponsorship data formatting
|
|
* - League creation and migration data transformation
|
|
* - Derived league fields (member counts, status, permissions, etc.)
|
|
* - Default values and fallbacks for league views
|
|
* - League-specific formatting (dates, points, positions, race formats, etc.)
|
|
* - Data grouping and categorization for league components
|
|
* - League search and filtering view models
|
|
* - Real-time league data updates and state management
|
|
*/
|
|
|
|
import { LeaguesViewDataBuilder } from '@/lib/builders/view-data/LeaguesViewDataBuilder';
|
|
import { LeagueDetailViewDataBuilder } from '@/lib/builders/view-data/LeagueDetailViewDataBuilder';
|
|
import { LeagueRosterAdminViewDataBuilder } from '@/lib/builders/view-data/LeagueRosterAdminViewDataBuilder';
|
|
import { LeagueScheduleViewDataBuilder } from '@/lib/builders/view-data/LeagueScheduleViewDataBuilder';
|
|
import { LeagueStandingsViewDataBuilder } from '@/lib/builders/view-data/LeagueStandingsViewDataBuilder';
|
|
import type { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO';
|
|
import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
|
|
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
|
|
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
|
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
|
|
import type { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
|
|
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
|
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
|
|
import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
|
|
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
|
|
|
describe('LeaguesViewDataBuilder', () => {
|
|
describe('happy paths', () => {
|
|
it('should transform AllLeaguesWithCapacityAndScoringDTO to LeaguesViewData correctly', () => {
|
|
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
|
leagues: [
|
|
{
|
|
id: 'league-1',
|
|
name: 'Pro League',
|
|
description: 'A competitive league for experienced drivers',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
qualifyingFormat: 'Solo • 32 max',
|
|
},
|
|
usedSlots: 25,
|
|
category: 'competitive',
|
|
scoring: {
|
|
gameId: 'game-1',
|
|
gameName: 'iRacing',
|
|
primaryChampionshipType: 'Single Championship',
|
|
scoringPresetId: 'preset-1',
|
|
scoringPresetName: 'Standard',
|
|
dropPolicySummary: 'Drop 2 worst races',
|
|
scoringPatternSummary: 'Points based on finish position',
|
|
},
|
|
timingSummary: 'Weekly races on Sundays',
|
|
logoUrl: 'https://example.com/logo.png',
|
|
pendingJoinRequestsCount: 3,
|
|
pendingProtestsCount: 1,
|
|
walletBalance: 1000,
|
|
},
|
|
{
|
|
id: 'league-2',
|
|
name: 'Rookie League',
|
|
description: null,
|
|
ownerId: 'owner-2',
|
|
createdAt: '2024-02-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 16,
|
|
qualifyingFormat: 'Solo • 16 max',
|
|
},
|
|
usedSlots: 10,
|
|
category: 'rookie',
|
|
scoring: {
|
|
gameId: 'game-1',
|
|
gameName: 'iRacing',
|
|
primaryChampionshipType: 'Single Championship',
|
|
scoringPresetId: 'preset-2',
|
|
scoringPresetName: 'Rookie',
|
|
dropPolicySummary: 'No drops',
|
|
scoringPatternSummary: 'Points based on finish position',
|
|
},
|
|
timingSummary: 'Bi-weekly races',
|
|
logoUrl: null,
|
|
pendingJoinRequestsCount: 0,
|
|
pendingProtestsCount: 0,
|
|
walletBalance: 0,
|
|
},
|
|
],
|
|
totalCount: 2,
|
|
};
|
|
|
|
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
|
|
|
expect(result.leagues).toHaveLength(2);
|
|
expect(result.leagues[0]).toEqual({
|
|
id: 'league-1',
|
|
name: 'Pro League',
|
|
description: 'A competitive league for experienced drivers',
|
|
logoUrl: 'https://example.com/logo.png',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
maxDrivers: 32,
|
|
usedDriverSlots: 25,
|
|
activeDriversCount: undefined,
|
|
nextRaceAt: undefined,
|
|
maxTeams: undefined,
|
|
usedTeamSlots: undefined,
|
|
structureSummary: 'Solo • 32 max',
|
|
timingSummary: 'Weekly races on Sundays',
|
|
category: 'competitive',
|
|
scoring: {
|
|
gameId: 'game-1',
|
|
gameName: 'iRacing',
|
|
primaryChampionshipType: 'Single Championship',
|
|
scoringPresetId: 'preset-1',
|
|
scoringPresetName: 'Standard',
|
|
dropPolicySummary: 'Drop 2 worst races',
|
|
scoringPatternSummary: 'Points based on finish position',
|
|
},
|
|
});
|
|
expect(result.leagues[1]).toEqual({
|
|
id: 'league-2',
|
|
name: 'Rookie League',
|
|
description: null,
|
|
logoUrl: null,
|
|
ownerId: 'owner-2',
|
|
createdAt: '2024-02-01T00:00:00.000Z',
|
|
maxDrivers: 16,
|
|
usedDriverSlots: 10,
|
|
activeDriversCount: undefined,
|
|
nextRaceAt: undefined,
|
|
maxTeams: undefined,
|
|
usedTeamSlots: undefined,
|
|
structureSummary: 'Solo • 16 max',
|
|
timingSummary: 'Bi-weekly races',
|
|
category: 'rookie',
|
|
scoring: {
|
|
gameId: 'game-1',
|
|
gameName: 'iRacing',
|
|
primaryChampionshipType: 'Single Championship',
|
|
scoringPresetId: 'preset-2',
|
|
scoringPresetName: 'Rookie',
|
|
dropPolicySummary: 'No drops',
|
|
scoringPatternSummary: 'Points based on finish position',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should handle empty leagues list', () => {
|
|
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
|
leagues: [],
|
|
totalCount: 0,
|
|
};
|
|
|
|
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
|
|
|
expect(result.leagues).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle leagues with missing optional fields', () => {
|
|
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
|
leagues: [
|
|
{
|
|
id: 'league-1',
|
|
name: 'Minimal League',
|
|
description: '',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 20,
|
|
},
|
|
usedSlots: 5,
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
};
|
|
|
|
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
|
|
|
expect(result.leagues[0].description).toBe(null);
|
|
expect(result.leagues[0].logoUrl).toBe(null);
|
|
expect(result.leagues[0].category).toBe(null);
|
|
expect(result.leagues[0].scoring).toBeUndefined();
|
|
expect(result.leagues[0].timingSummary).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('data transformation', () => {
|
|
it('should preserve all DTO fields in the output', () => {
|
|
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
|
leagues: [
|
|
{
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
qualifyingFormat: 'Solo • 32 max',
|
|
},
|
|
usedSlots: 20,
|
|
category: 'test',
|
|
scoring: {
|
|
gameId: 'game-1',
|
|
gameName: 'Test Game',
|
|
primaryChampionshipType: 'Test Type',
|
|
scoringPresetId: 'preset-1',
|
|
scoringPresetName: 'Test Preset',
|
|
dropPolicySummary: 'Test drop policy',
|
|
scoringPatternSummary: 'Test pattern',
|
|
},
|
|
timingSummary: 'Test timing',
|
|
logoUrl: 'https://example.com/test.png',
|
|
pendingJoinRequestsCount: 5,
|
|
pendingProtestsCount: 2,
|
|
walletBalance: 500,
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
};
|
|
|
|
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
|
|
|
expect(result.leagues[0].id).toBe(leaguesDTO.leagues[0].id);
|
|
expect(result.leagues[0].name).toBe(leaguesDTO.leagues[0].name);
|
|
expect(result.leagues[0].description).toBe(leaguesDTO.leagues[0].description);
|
|
expect(result.leagues[0].logoUrl).toBe(leaguesDTO.leagues[0].logoUrl);
|
|
expect(result.leagues[0].ownerId).toBe(leaguesDTO.leagues[0].ownerId);
|
|
expect(result.leagues[0].createdAt).toBe(leaguesDTO.leagues[0].createdAt);
|
|
expect(result.leagues[0].maxDrivers).toBe(leaguesDTO.leagues[0].settings.maxDrivers);
|
|
expect(result.leagues[0].usedDriverSlots).toBe(leaguesDTO.leagues[0].usedSlots);
|
|
expect(result.leagues[0].structureSummary).toBe(leaguesDTO.leagues[0].settings.qualifyingFormat);
|
|
expect(result.leagues[0].timingSummary).toBe(leaguesDTO.leagues[0].timingSummary);
|
|
expect(result.leagues[0].category).toBe(leaguesDTO.leagues[0].category);
|
|
expect(result.leagues[0].scoring).toEqual(leaguesDTO.leagues[0].scoring);
|
|
});
|
|
|
|
it('should not modify the input DTO', () => {
|
|
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
|
leagues: [
|
|
{
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
qualifyingFormat: 'Solo • 32 max',
|
|
},
|
|
usedSlots: 20,
|
|
category: 'test',
|
|
scoring: {
|
|
gameId: 'game-1',
|
|
gameName: 'Test Game',
|
|
primaryChampionshipType: 'Test Type',
|
|
scoringPresetId: 'preset-1',
|
|
scoringPresetName: 'Test Preset',
|
|
dropPolicySummary: 'Test drop policy',
|
|
scoringPatternSummary: 'Test pattern',
|
|
},
|
|
timingSummary: 'Test timing',
|
|
logoUrl: 'https://example.com/test.png',
|
|
pendingJoinRequestsCount: 5,
|
|
pendingProtestsCount: 2,
|
|
walletBalance: 500,
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
};
|
|
|
|
const originalDTO = JSON.parse(JSON.stringify(leaguesDTO));
|
|
LeaguesViewDataBuilder.build(leaguesDTO);
|
|
|
|
expect(leaguesDTO).toEqual(originalDTO);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle leagues with very long descriptions', () => {
|
|
const longDescription = 'A'.repeat(1000);
|
|
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
|
leagues: [
|
|
{
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: longDescription,
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 20,
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
};
|
|
|
|
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
|
|
|
expect(result.leagues[0].description).toBe(longDescription);
|
|
});
|
|
|
|
it('should handle leagues with special characters in name', () => {
|
|
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
|
leagues: [
|
|
{
|
|
id: 'league-1',
|
|
name: 'League & Co. (2024)',
|
|
description: 'Test league',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 20,
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
};
|
|
|
|
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
|
|
|
expect(result.leagues[0].name).toBe('League & Co. (2024)');
|
|
});
|
|
|
|
it('should handle leagues with zero used slots', () => {
|
|
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
|
leagues: [
|
|
{
|
|
id: 'league-1',
|
|
name: 'Empty League',
|
|
description: 'No members yet',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 0,
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
};
|
|
|
|
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
|
|
|
expect(result.leagues[0].usedDriverSlots).toBe(0);
|
|
});
|
|
|
|
it('should handle leagues with maximum capacity', () => {
|
|
const leaguesDTO: AllLeaguesWithCapacityAndScoringDTO = {
|
|
leagues: [
|
|
{
|
|
id: 'league-1',
|
|
name: 'Full League',
|
|
description: 'At maximum capacity',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 32,
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
};
|
|
|
|
const result = LeaguesViewDataBuilder.build(leaguesDTO);
|
|
|
|
expect(result.leagues[0].usedDriverSlots).toBe(32);
|
|
expect(result.leagues[0].maxDrivers).toBe(32);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('LeagueDetailViewDataBuilder', () => {
|
|
describe('happy paths', () => {
|
|
it('should transform league DTOs to LeagueDetailViewData correctly', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Pro League',
|
|
description: 'A competitive league for experienced drivers',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
qualifyingFormat: 'Solo • 32 max',
|
|
},
|
|
usedSlots: 25,
|
|
category: 'competitive',
|
|
scoring: {
|
|
gameId: 'game-1',
|
|
gameName: 'iRacing',
|
|
primaryChampionshipType: 'Single Championship',
|
|
scoringPresetId: 'preset-1',
|
|
scoringPresetName: 'Standard',
|
|
dropPolicySummary: 'Drop 2 worst races',
|
|
scoringPatternSummary: 'Points based on finish position',
|
|
},
|
|
timingSummary: 'Weekly races on Sundays',
|
|
logoUrl: 'https://example.com/logo.png',
|
|
pendingJoinRequestsCount: 3,
|
|
pendingProtestsCount: 1,
|
|
walletBalance: 1000,
|
|
};
|
|
|
|
const owner: GetDriverOutputDTO = {
|
|
id: 'owner-1',
|
|
name: 'John Doe',
|
|
iracingId: '12345',
|
|
country: 'USA',
|
|
bio: 'Experienced driver',
|
|
joinedAt: '2023-01-01T00:00:00.000Z',
|
|
avatarUrl: 'https://example.com/avatar.jpg',
|
|
};
|
|
|
|
const scoringConfig: LeagueScoringConfigDTO = {
|
|
id: 'config-1',
|
|
leagueId: 'league-1',
|
|
gameId: 'game-1',
|
|
gameName: 'iRacing',
|
|
primaryChampionshipType: 'Single Championship',
|
|
scoringPresetId: 'preset-1',
|
|
scoringPresetName: 'Standard',
|
|
dropPolicySummary: 'Drop 2 worst races',
|
|
scoringPatternSummary: 'Points based on finish position',
|
|
dropRaces: 2,
|
|
pointsPerRace: 100,
|
|
pointsForWin: 25,
|
|
pointsForPodium: [20, 15, 10],
|
|
};
|
|
|
|
const memberships: LeagueMembershipsDTO = {
|
|
members: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
role: 'admin',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
{
|
|
driverId: 'driver-2',
|
|
driver: {
|
|
id: 'driver-2',
|
|
name: 'Bob',
|
|
iracingId: '22222',
|
|
country: 'Germany',
|
|
joinedAt: '2023-07-01T00:00:00.000Z',
|
|
},
|
|
role: 'steward',
|
|
joinedAt: '2023-07-01T00:00:00.000Z',
|
|
},
|
|
{
|
|
driverId: 'driver-3',
|
|
driver: {
|
|
id: 'driver-3',
|
|
name: 'Charlie',
|
|
iracingId: '33333',
|
|
country: 'France',
|
|
joinedAt: '2023-08-01T00:00:00.000Z',
|
|
},
|
|
role: 'member',
|
|
joinedAt: '2023-08-01T00:00:00.000Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
const races: RaceDTO[] = [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Race 1',
|
|
date: '2024-01-15T14:00:00.000Z',
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
strengthOfField: 1500,
|
|
},
|
|
{
|
|
id: 'race-2',
|
|
name: 'Race 2',
|
|
date: '2024-01-22T14:00:00.000Z',
|
|
track: 'Monza',
|
|
car: 'Ferrari 488 GT3',
|
|
sessionType: 'race',
|
|
strengthOfField: 1600,
|
|
},
|
|
];
|
|
|
|
const sponsors: any[] = [
|
|
{
|
|
id: 'sponsor-1',
|
|
name: 'Sponsor A',
|
|
tier: 'main',
|
|
logoUrl: 'https://example.com/sponsor-a.png',
|
|
websiteUrl: 'https://sponsor-a.com',
|
|
tagline: 'Premium racing gear',
|
|
},
|
|
];
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner,
|
|
scoringConfig,
|
|
memberships,
|
|
races,
|
|
sponsors,
|
|
});
|
|
|
|
expect(result.leagueId).toBe('league-1');
|
|
expect(result.name).toBe('Pro League');
|
|
expect(result.description).toBe('A competitive league for experienced drivers');
|
|
expect(result.logoUrl).toBe('https://example.com/logo.png');
|
|
expect(result.info.name).toBe('Pro League');
|
|
expect(result.info.description).toBe('A competitive league for experienced drivers');
|
|
expect(result.info.membersCount).toBe(3);
|
|
expect(result.info.racesCount).toBe(2);
|
|
expect(result.info.avgSOF).toBe(1550);
|
|
expect(result.info.structure).toBe('Solo • 32 max');
|
|
expect(result.info.scoring).toBe('preset-1');
|
|
expect(result.info.createdAt).toBe('2024-01-01T00:00:00.000Z');
|
|
expect(result.info.discordUrl).toBeUndefined();
|
|
expect(result.info.youtubeUrl).toBeUndefined();
|
|
expect(result.info.websiteUrl).toBeUndefined();
|
|
expect(result.ownerSummary).not.toBeNull();
|
|
expect(result.ownerSummary?.driverId).toBe('owner-1');
|
|
expect(result.ownerSummary?.driverName).toBe('John Doe');
|
|
expect(result.ownerSummary?.avatarUrl).toBe('https://example.com/avatar.jpg');
|
|
expect(result.ownerSummary?.roleBadgeText).toBe('Owner');
|
|
expect(result.adminSummaries).toHaveLength(1);
|
|
expect(result.adminSummaries[0].driverId).toBe('driver-1');
|
|
expect(result.adminSummaries[0].driverName).toBe('Alice');
|
|
expect(result.adminSummaries[0].roleBadgeText).toBe('Admin');
|
|
expect(result.stewardSummaries).toHaveLength(1);
|
|
expect(result.stewardSummaries[0].driverId).toBe('driver-2');
|
|
expect(result.stewardSummaries[0].driverName).toBe('Bob');
|
|
expect(result.stewardSummaries[0].roleBadgeText).toBe('Steward');
|
|
expect(result.memberSummaries).toHaveLength(1);
|
|
expect(result.memberSummaries[0].driverId).toBe('driver-3');
|
|
expect(result.memberSummaries[0].driverName).toBe('Charlie');
|
|
expect(result.memberSummaries[0].roleBadgeText).toBe('Member');
|
|
expect(result.sponsors).toHaveLength(1);
|
|
expect(result.sponsors[0].id).toBe('sponsor-1');
|
|
expect(result.sponsors[0].name).toBe('Sponsor A');
|
|
expect(result.sponsors[0].tier).toBe('main');
|
|
expect(result.walletBalance).toBe(1000);
|
|
expect(result.pendingProtestsCount).toBe(1);
|
|
expect(result.pendingJoinRequestsCount).toBe(3);
|
|
});
|
|
|
|
it('should handle league with no owner', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 10,
|
|
};
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships: { members: [] },
|
|
races: [],
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(result.ownerSummary).toBeNull();
|
|
});
|
|
|
|
it('should handle league with no scoring config', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 10,
|
|
};
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships: { members: [] },
|
|
races: [],
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(result.info.scoring).toBe('Standard');
|
|
});
|
|
|
|
it('should handle league with no races', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 10,
|
|
};
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships: { members: [] },
|
|
races: [],
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(result.info.racesCount).toBe(0);
|
|
expect(result.info.avgSOF).toBeNull();
|
|
expect(result.runningRaces).toEqual([]);
|
|
expect(result.nextRace).toBeUndefined();
|
|
expect(result.seasonProgress).toEqual({
|
|
completedRaces: 0,
|
|
totalRaces: 0,
|
|
percentage: 0,
|
|
});
|
|
expect(result.recentResults).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('data transformation', () => {
|
|
it('should preserve all DTO fields in the output', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
qualifyingFormat: 'Solo • 32 max',
|
|
},
|
|
usedSlots: 20,
|
|
category: 'test',
|
|
scoring: {
|
|
gameId: 'game-1',
|
|
gameName: 'Test Game',
|
|
primaryChampionshipType: 'Test Type',
|
|
scoringPresetId: 'preset-1',
|
|
scoringPresetName: 'Test Preset',
|
|
dropPolicySummary: 'Test drop policy',
|
|
scoringPatternSummary: 'Test pattern',
|
|
},
|
|
timingSummary: 'Test timing',
|
|
logoUrl: 'https://example.com/test.png',
|
|
pendingJoinRequestsCount: 5,
|
|
pendingProtestsCount: 2,
|
|
walletBalance: 500,
|
|
};
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships: { members: [] },
|
|
races: [],
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(result.leagueId).toBe(league.id);
|
|
expect(result.name).toBe(league.name);
|
|
expect(result.description).toBe(league.description);
|
|
expect(result.logoUrl).toBe(league.logoUrl);
|
|
expect(result.walletBalance).toBe(league.walletBalance);
|
|
expect(result.pendingProtestsCount).toBe(league.pendingProtestsCount);
|
|
expect(result.pendingJoinRequestsCount).toBe(league.pendingJoinRequestsCount);
|
|
});
|
|
|
|
it('should not modify the input DTOs', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 20,
|
|
};
|
|
|
|
const originalLeague = JSON.parse(JSON.stringify(league));
|
|
LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships: { members: [] },
|
|
races: [],
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(league).toEqual(originalLeague);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle league with missing optional fields', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Minimal League',
|
|
description: '',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 10,
|
|
};
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships: { members: [] },
|
|
races: [],
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(result.description).toBe('');
|
|
expect(result.logoUrl).toBeUndefined();
|
|
expect(result.info.description).toBe('');
|
|
expect(result.info.discordUrl).toBeUndefined();
|
|
expect(result.info.youtubeUrl).toBeUndefined();
|
|
expect(result.info.websiteUrl).toBeUndefined();
|
|
});
|
|
|
|
it('should handle races with missing strengthOfField', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 10,
|
|
};
|
|
|
|
const races: RaceDTO[] = [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Race 1',
|
|
date: '2024-01-15T14:00:00.000Z',
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
];
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships: { members: [] },
|
|
races,
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(result.info.avgSOF).toBeNull();
|
|
});
|
|
|
|
it('should handle races with zero strengthOfField', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 10,
|
|
};
|
|
|
|
const races: RaceDTO[] = [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Race 1',
|
|
date: '2024-01-15T14:00:00.000Z',
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
strengthOfField: 0,
|
|
},
|
|
];
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships: { members: [] },
|
|
races,
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(result.info.avgSOF).toBeNull();
|
|
});
|
|
|
|
it('should handle races with different dates for next race calculation', () => {
|
|
const now = new Date();
|
|
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago
|
|
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now
|
|
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 10,
|
|
};
|
|
|
|
const races: RaceDTO[] = [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Past Race',
|
|
date: pastDate.toISOString(),
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
{
|
|
id: 'race-2',
|
|
name: 'Future Race',
|
|
date: futureDate.toISOString(),
|
|
track: 'Monza',
|
|
car: 'Ferrari 488 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
];
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships: { members: [] },
|
|
races,
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(result.nextRace).toBeDefined();
|
|
expect(result.nextRace?.id).toBe('race-2');
|
|
expect(result.nextRace?.name).toBe('Future Race');
|
|
expect(result.seasonProgress.completedRaces).toBe(1);
|
|
expect(result.seasonProgress.totalRaces).toBe(2);
|
|
expect(result.seasonProgress.percentage).toBe(50);
|
|
expect(result.recentResults).toHaveLength(1);
|
|
expect(result.recentResults[0].raceId).toBe('race-1');
|
|
});
|
|
|
|
it('should handle members with different roles', () => {
|
|
const league: LeagueWithCapacityAndScoringDTO = {
|
|
id: 'league-1',
|
|
name: 'Test League',
|
|
description: 'Test description',
|
|
ownerId: 'owner-1',
|
|
createdAt: '2024-01-01T00:00:00.000Z',
|
|
settings: {
|
|
maxDrivers: 32,
|
|
},
|
|
usedSlots: 10,
|
|
};
|
|
|
|
const memberships: LeagueMembershipsDTO = {
|
|
members: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Admin',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
role: 'admin',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
{
|
|
driverId: 'driver-2',
|
|
driver: {
|
|
id: 'driver-2',
|
|
name: 'Steward',
|
|
iracingId: '22222',
|
|
country: 'Germany',
|
|
joinedAt: '2023-07-01T00:00:00.000Z',
|
|
},
|
|
role: 'steward',
|
|
joinedAt: '2023-07-01T00:00:00.000Z',
|
|
},
|
|
{
|
|
driverId: 'driver-3',
|
|
driver: {
|
|
id: 'driver-3',
|
|
name: 'Member',
|
|
iracingId: '33333',
|
|
country: 'France',
|
|
joinedAt: '2023-08-01T00:00:00.000Z',
|
|
},
|
|
role: 'member',
|
|
joinedAt: '2023-08-01T00:00:00.000Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = LeagueDetailViewDataBuilder.build({
|
|
league,
|
|
owner: null,
|
|
scoringConfig: null,
|
|
memberships,
|
|
races: [],
|
|
sponsors: [],
|
|
});
|
|
|
|
expect(result.adminSummaries).toHaveLength(1);
|
|
expect(result.stewardSummaries).toHaveLength(1);
|
|
expect(result.memberSummaries).toHaveLength(1);
|
|
expect(result.info.membersCount).toBe(3);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('LeagueRosterAdminViewDataBuilder', () => {
|
|
describe('happy paths', () => {
|
|
it('should transform roster DTOs to LeagueRosterAdminViewData correctly', () => {
|
|
const members: LeagueRosterMemberDTO[] = [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
role: 'admin',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
{
|
|
driverId: 'driver-2',
|
|
driver: {
|
|
id: 'driver-2',
|
|
name: 'Bob',
|
|
iracingId: '22222',
|
|
country: 'Germany',
|
|
joinedAt: '2023-07-01T00:00:00.000Z',
|
|
},
|
|
role: 'member',
|
|
joinedAt: '2023-07-01T00:00:00.000Z',
|
|
},
|
|
];
|
|
|
|
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
|
{
|
|
id: 'request-1',
|
|
leagueId: 'league-1',
|
|
driverId: 'driver-3',
|
|
requestedAt: '2024-01-15T10:00:00.000Z',
|
|
message: 'I would like to join this league',
|
|
driver: {},
|
|
},
|
|
];
|
|
|
|
const result = LeagueRosterAdminViewDataBuilder.build({
|
|
leagueId: 'league-1',
|
|
members,
|
|
joinRequests,
|
|
});
|
|
|
|
expect(result.leagueId).toBe('league-1');
|
|
expect(result.members).toHaveLength(2);
|
|
expect(result.members[0].driverId).toBe('driver-1');
|
|
expect(result.members[0].driver.id).toBe('driver-1');
|
|
expect(result.members[0].driver.name).toBe('Alice');
|
|
expect(result.members[0].role).toBe('admin');
|
|
expect(result.members[0].joinedAt).toBe('2023-06-01T00:00:00.000Z');
|
|
expect(result.members[0].formattedJoinedAt).toBeDefined();
|
|
expect(result.members[1].driverId).toBe('driver-2');
|
|
expect(result.members[1].driver.id).toBe('driver-2');
|
|
expect(result.members[1].driver.name).toBe('Bob');
|
|
expect(result.members[1].role).toBe('member');
|
|
expect(result.members[1].joinedAt).toBe('2023-07-01T00:00:00.000Z');
|
|
expect(result.members[1].formattedJoinedAt).toBeDefined();
|
|
expect(result.joinRequests).toHaveLength(1);
|
|
expect(result.joinRequests[0].id).toBe('request-1');
|
|
expect(result.joinRequests[0].driver.id).toBe('driver-3');
|
|
expect(result.joinRequests[0].driver.name).toBe('Unknown Driver');
|
|
expect(result.joinRequests[0].requestedAt).toBe('2024-01-15T10:00:00.000Z');
|
|
expect(result.joinRequests[0].formattedRequestedAt).toBeDefined();
|
|
expect(result.joinRequests[0].message).toBe('I would like to join this league');
|
|
});
|
|
|
|
it('should handle empty members and join requests', () => {
|
|
const result = LeagueRosterAdminViewDataBuilder.build({
|
|
leagueId: 'league-1',
|
|
members: [],
|
|
joinRequests: [],
|
|
});
|
|
|
|
expect(result.leagueId).toBe('league-1');
|
|
expect(result.members).toHaveLength(0);
|
|
expect(result.joinRequests).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle members without driver details', () => {
|
|
const members: LeagueRosterMemberDTO[] = [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: undefined as any,
|
|
role: 'member',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
];
|
|
|
|
const result = LeagueRosterAdminViewDataBuilder.build({
|
|
leagueId: 'league-1',
|
|
members,
|
|
joinRequests: [],
|
|
});
|
|
|
|
expect(result.members[0].driver.name).toBe('Unknown Driver');
|
|
});
|
|
});
|
|
|
|
describe('data transformation', () => {
|
|
it('should preserve all DTO fields in the output', () => {
|
|
const members: LeagueRosterMemberDTO[] = [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
role: 'admin',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
];
|
|
|
|
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
|
{
|
|
id: 'request-1',
|
|
leagueId: 'league-1',
|
|
driverId: 'driver-3',
|
|
requestedAt: '2024-01-15T10:00:00.000Z',
|
|
message: 'I would like to join this league',
|
|
driver: {},
|
|
},
|
|
];
|
|
|
|
const result = LeagueRosterAdminViewDataBuilder.build({
|
|
leagueId: 'league-1',
|
|
members,
|
|
joinRequests,
|
|
});
|
|
|
|
expect(result.leagueId).toBe('league-1');
|
|
expect(result.members[0].driverId).toBe(members[0].driverId);
|
|
expect(result.members[0].driver.id).toBe(members[0].driver.id);
|
|
expect(result.members[0].driver.name).toBe(members[0].driver.name);
|
|
expect(result.members[0].role).toBe(members[0].role);
|
|
expect(result.members[0].joinedAt).toBe(members[0].joinedAt);
|
|
expect(result.joinRequests[0].id).toBe(joinRequests[0].id);
|
|
expect(result.joinRequests[0].requestedAt).toBe(joinRequests[0].requestedAt);
|
|
expect(result.joinRequests[0].message).toBe(joinRequests[0].message);
|
|
});
|
|
|
|
it('should not modify the input DTOs', () => {
|
|
const members: LeagueRosterMemberDTO[] = [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
role: 'admin',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
];
|
|
|
|
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
|
{
|
|
id: 'request-1',
|
|
leagueId: 'league-1',
|
|
driverId: 'driver-3',
|
|
requestedAt: '2024-01-15T10:00:00.000Z',
|
|
message: 'I would like to join this league',
|
|
driver: {},
|
|
},
|
|
];
|
|
|
|
const originalMembers = JSON.parse(JSON.stringify(members));
|
|
const originalRequests = JSON.parse(JSON.stringify(joinRequests));
|
|
|
|
LeagueRosterAdminViewDataBuilder.build({
|
|
leagueId: 'league-1',
|
|
members,
|
|
joinRequests,
|
|
});
|
|
|
|
expect(members).toEqual(originalMembers);
|
|
expect(joinRequests).toEqual(originalRequests);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle members with missing driver field', () => {
|
|
const members: LeagueRosterMemberDTO[] = [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: undefined as any,
|
|
role: 'member',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
];
|
|
|
|
const result = LeagueRosterAdminViewDataBuilder.build({
|
|
leagueId: 'league-1',
|
|
members,
|
|
joinRequests: [],
|
|
});
|
|
|
|
expect(result.members[0].driver.name).toBe('Unknown Driver');
|
|
});
|
|
|
|
it('should handle join requests with missing driver field', () => {
|
|
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
|
{
|
|
id: 'request-1',
|
|
leagueId: 'league-1',
|
|
driverId: 'driver-3',
|
|
requestedAt: '2024-01-15T10:00:00.000Z',
|
|
message: 'I would like to join this league',
|
|
driver: undefined,
|
|
},
|
|
];
|
|
|
|
const result = LeagueRosterAdminViewDataBuilder.build({
|
|
leagueId: 'league-1',
|
|
members: [],
|
|
joinRequests,
|
|
});
|
|
|
|
expect(result.joinRequests[0].driver.name).toBe('Unknown Driver');
|
|
});
|
|
|
|
it('should handle join requests without message', () => {
|
|
const joinRequests: LeagueRosterJoinRequestDTO[] = [
|
|
{
|
|
id: 'request-1',
|
|
leagueId: 'league-1',
|
|
driverId: 'driver-3',
|
|
requestedAt: '2024-01-15T10:00:00.000Z',
|
|
driver: {},
|
|
},
|
|
];
|
|
|
|
const result = LeagueRosterAdminViewDataBuilder.build({
|
|
leagueId: 'league-1',
|
|
members: [],
|
|
joinRequests,
|
|
});
|
|
|
|
expect(result.joinRequests[0].message).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('LeagueScheduleViewDataBuilder', () => {
|
|
describe('happy paths', () => {
|
|
it('should transform schedule DTO to LeagueScheduleViewData correctly', () => {
|
|
const now = new Date();
|
|
const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago
|
|
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day from now
|
|
|
|
const apiDto = {
|
|
leagueId: 'league-1',
|
|
races: [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Past Race',
|
|
date: pastDate.toISOString(),
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
{
|
|
id: 'race-2',
|
|
name: 'Future Race',
|
|
date: futureDate.toISOString(),
|
|
track: 'Monza',
|
|
car: 'Ferrari 488 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', true);
|
|
|
|
expect(result.leagueId).toBe('league-1');
|
|
expect(result.races).toHaveLength(2);
|
|
expect(result.races[0].id).toBe('race-1');
|
|
expect(result.races[0].name).toBe('Past Race');
|
|
expect(result.races[0].scheduledAt).toBe(pastDate.toISOString());
|
|
expect(result.races[0].track).toBe('Spa');
|
|
expect(result.races[0].car).toBe('Porsche 911 GT3');
|
|
expect(result.races[0].sessionType).toBe('race');
|
|
expect(result.races[0].isPast).toBe(true);
|
|
expect(result.races[0].isUpcoming).toBe(false);
|
|
expect(result.races[0].status).toBe('completed');
|
|
expect(result.races[0].isUserRegistered).toBe(false);
|
|
expect(result.races[0].canRegister).toBe(false);
|
|
expect(result.races[0].canEdit).toBe(true);
|
|
expect(result.races[0].canReschedule).toBe(true);
|
|
expect(result.races[1].id).toBe('race-2');
|
|
expect(result.races[1].name).toBe('Future Race');
|
|
expect(result.races[1].scheduledAt).toBe(futureDate.toISOString());
|
|
expect(result.races[1].track).toBe('Monza');
|
|
expect(result.races[1].car).toBe('Ferrari 488 GT3');
|
|
expect(result.races[1].sessionType).toBe('race');
|
|
expect(result.races[1].isPast).toBe(false);
|
|
expect(result.races[1].isUpcoming).toBe(true);
|
|
expect(result.races[1].status).toBe('scheduled');
|
|
expect(result.races[1].isUserRegistered).toBe(false);
|
|
expect(result.races[1].canRegister).toBe(true);
|
|
expect(result.races[1].canEdit).toBe(true);
|
|
expect(result.races[1].canReschedule).toBe(true);
|
|
expect(result.currentDriverId).toBe('driver-1');
|
|
expect(result.isAdmin).toBe(true);
|
|
});
|
|
|
|
it('should handle empty races list', () => {
|
|
const apiDto = {
|
|
leagueId: 'league-1',
|
|
races: [],
|
|
};
|
|
|
|
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
|
|
|
expect(result.leagueId).toBe('league-1');
|
|
expect(result.races).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle non-admin user', () => {
|
|
const now = new Date();
|
|
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
|
|
const apiDto = {
|
|
leagueId: 'league-1',
|
|
races: [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Future Race',
|
|
date: futureDate.toISOString(),
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = LeagueScheduleViewDataBuilder.build(apiDto, 'driver-1', false);
|
|
|
|
expect(result.races[0].canEdit).toBe(false);
|
|
expect(result.races[0].canReschedule).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('data transformation', () => {
|
|
it('should preserve all DTO fields in the output', () => {
|
|
const now = new Date();
|
|
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
|
|
const apiDto = {
|
|
leagueId: 'league-1',
|
|
races: [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Test Race',
|
|
date: futureDate.toISOString(),
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
|
|
|
expect(result.leagueId).toBe(apiDto.leagueId);
|
|
expect(result.races[0].id).toBe(apiDto.races[0].id);
|
|
expect(result.races[0].name).toBe(apiDto.races[0].name);
|
|
expect(result.races[0].scheduledAt).toBe(apiDto.races[0].date);
|
|
expect(result.races[0].track).toBe(apiDto.races[0].track);
|
|
expect(result.races[0].car).toBe(apiDto.races[0].car);
|
|
expect(result.races[0].sessionType).toBe(apiDto.races[0].sessionType);
|
|
});
|
|
|
|
it('should not modify the input DTO', () => {
|
|
const now = new Date();
|
|
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
|
|
const apiDto = {
|
|
leagueId: 'league-1',
|
|
races: [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Test Race',
|
|
date: futureDate.toISOString(),
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
],
|
|
};
|
|
|
|
const originalDto = JSON.parse(JSON.stringify(apiDto));
|
|
LeagueScheduleViewDataBuilder.build(apiDto);
|
|
|
|
expect(apiDto).toEqual(originalDto);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle races with missing optional fields', () => {
|
|
const now = new Date();
|
|
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
|
|
const apiDto = {
|
|
leagueId: 'league-1',
|
|
races: [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Test Race',
|
|
date: futureDate.toISOString(),
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
|
|
|
expect(result.races[0].track).toBe('Spa');
|
|
expect(result.races[0].car).toBe('Porsche 911 GT3');
|
|
expect(result.races[0].sessionType).toBe('race');
|
|
});
|
|
|
|
it('should handle races at exactly the current time', () => {
|
|
const now = new Date();
|
|
const currentRaceDate = new Date(now.getTime());
|
|
|
|
const apiDto = {
|
|
leagueId: 'league-1',
|
|
races: [
|
|
{
|
|
id: 'race-1',
|
|
name: 'Current Race',
|
|
date: currentRaceDate.toISOString(),
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3',
|
|
sessionType: 'race',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = LeagueScheduleViewDataBuilder.build(apiDto);
|
|
|
|
// Race at current time should be considered past
|
|
expect(result.races[0].isPast).toBe(true);
|
|
expect(result.races[0].isUpcoming).toBe(false);
|
|
expect(result.races[0].status).toBe('completed');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('LeagueStandingsViewDataBuilder', () => {
|
|
describe('happy paths', () => {
|
|
it('should transform standings DTOs to LeagueStandingsViewData correctly', () => {
|
|
const standingsDto = {
|
|
standings: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
},
|
|
points: 1250,
|
|
position: 1,
|
|
wins: 5,
|
|
podiums: 10,
|
|
races: 15,
|
|
positionChange: 2,
|
|
lastRacePoints: 25,
|
|
droppedRaceIds: ['race-1', 'race-2'],
|
|
},
|
|
{
|
|
driverId: 'driver-2',
|
|
driver: {
|
|
id: 'driver-2',
|
|
name: 'Bob',
|
|
iracingId: '22222',
|
|
country: 'Germany',
|
|
},
|
|
points: 1100,
|
|
position: 2,
|
|
wins: 3,
|
|
podiums: 8,
|
|
races: 15,
|
|
positionChange: -1,
|
|
lastRacePoints: 15,
|
|
droppedRaceIds: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
const membershipsDto = {
|
|
members: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
role: 'member',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
{
|
|
driverId: 'driver-2',
|
|
driver: {
|
|
id: 'driver-2',
|
|
name: 'Bob',
|
|
iracingId: '22222',
|
|
country: 'Germany',
|
|
joinedAt: '2023-07-01T00:00:00.000Z',
|
|
},
|
|
role: 'member',
|
|
joinedAt: '2023-07-01T00:00:00.000Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = LeagueStandingsViewDataBuilder.build(
|
|
standingsDto,
|
|
membershipsDto,
|
|
'league-1',
|
|
false
|
|
);
|
|
|
|
expect(result.leagueId).toBe('league-1');
|
|
expect(result.isTeamChampionship).toBe(false);
|
|
expect(result.currentDriverId).toBeNull();
|
|
expect(result.isAdmin).toBe(false);
|
|
expect(result.standings).toHaveLength(2);
|
|
expect(result.standings[0].driverId).toBe('driver-1');
|
|
expect(result.standings[0].position).toBe(1);
|
|
expect(result.standings[0].totalPoints).toBe(1250);
|
|
expect(result.standings[0].racesFinished).toBe(15);
|
|
expect(result.standings[0].racesStarted).toBe(15);
|
|
expect(result.standings[0].avgFinish).toBeNull();
|
|
expect(result.standings[0].penaltyPoints).toBe(0);
|
|
expect(result.standings[0].bonusPoints).toBe(0);
|
|
expect(result.standings[0].positionChange).toBe(2);
|
|
expect(result.standings[0].lastRacePoints).toBe(25);
|
|
expect(result.standings[0].droppedRaceIds).toEqual(['race-1', 'race-2']);
|
|
expect(result.standings[0].wins).toBe(5);
|
|
expect(result.standings[0].podiums).toBe(10);
|
|
expect(result.standings[1].driverId).toBe('driver-2');
|
|
expect(result.standings[1].position).toBe(2);
|
|
expect(result.standings[1].totalPoints).toBe(1100);
|
|
expect(result.standings[1].racesFinished).toBe(15);
|
|
expect(result.standings[1].racesStarted).toBe(15);
|
|
expect(result.standings[1].avgFinish).toBeNull();
|
|
expect(result.standings[1].penaltyPoints).toBe(0);
|
|
expect(result.standings[1].bonusPoints).toBe(0);
|
|
expect(result.standings[1].positionChange).toBe(-1);
|
|
expect(result.standings[1].lastRacePoints).toBe(15);
|
|
expect(result.standings[1].droppedRaceIds).toEqual([]);
|
|
expect(result.standings[1].wins).toBe(3);
|
|
expect(result.standings[1].podiums).toBe(8);
|
|
expect(result.drivers).toHaveLength(2);
|
|
expect(result.drivers[0].id).toBe('driver-1');
|
|
expect(result.drivers[0].name).toBe('Alice');
|
|
expect(result.drivers[0].iracingId).toBe('11111');
|
|
expect(result.drivers[0].country).toBe('UK');
|
|
expect(result.drivers[0].avatarUrl).toBeNull();
|
|
expect(result.drivers[1].id).toBe('driver-2');
|
|
expect(result.drivers[1].name).toBe('Bob');
|
|
expect(result.drivers[1].iracingId).toBe('22222');
|
|
expect(result.drivers[1].country).toBe('Germany');
|
|
expect(result.drivers[1].avatarUrl).toBeNull();
|
|
expect(result.memberships).toHaveLength(2);
|
|
expect(result.memberships[0].driverId).toBe('driver-1');
|
|
expect(result.memberships[0].leagueId).toBe('league-1');
|
|
expect(result.memberships[0].role).toBe('member');
|
|
expect(result.memberships[0].joinedAt).toBe('2023-06-01T00:00:00.000Z');
|
|
expect(result.memberships[0].status).toBe('active');
|
|
expect(result.memberships[1].driverId).toBe('driver-2');
|
|
expect(result.memberships[1].leagueId).toBe('league-1');
|
|
expect(result.memberships[1].role).toBe('member');
|
|
expect(result.memberships[1].joinedAt).toBe('2023-07-01T00:00:00.000Z');
|
|
expect(result.memberships[1].status).toBe('active');
|
|
});
|
|
|
|
it('should handle empty standings and memberships', () => {
|
|
const standingsDto = {
|
|
standings: [],
|
|
};
|
|
|
|
const membershipsDto = {
|
|
members: [],
|
|
};
|
|
|
|
const result = LeagueStandingsViewDataBuilder.build(
|
|
standingsDto,
|
|
membershipsDto,
|
|
'league-1',
|
|
false
|
|
);
|
|
|
|
expect(result.standings).toHaveLength(0);
|
|
expect(result.drivers).toHaveLength(0);
|
|
expect(result.memberships).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle team championship mode', () => {
|
|
const standingsDto = {
|
|
standings: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
},
|
|
points: 1250,
|
|
position: 1,
|
|
wins: 5,
|
|
podiums: 10,
|
|
races: 15,
|
|
positionChange: 2,
|
|
lastRacePoints: 25,
|
|
droppedRaceIds: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
const membershipsDto = {
|
|
members: [],
|
|
};
|
|
|
|
const result = LeagueStandingsViewDataBuilder.build(
|
|
standingsDto,
|
|
membershipsDto,
|
|
'league-1',
|
|
true
|
|
);
|
|
|
|
expect(result.isTeamChampionship).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('data transformation', () => {
|
|
it('should preserve all DTO fields in the output', () => {
|
|
const standingsDto = {
|
|
standings: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
},
|
|
points: 1250,
|
|
position: 1,
|
|
wins: 5,
|
|
podiums: 10,
|
|
races: 15,
|
|
positionChange: 2,
|
|
lastRacePoints: 25,
|
|
droppedRaceIds: ['race-1'],
|
|
},
|
|
],
|
|
};
|
|
|
|
const membershipsDto = {
|
|
members: [],
|
|
};
|
|
|
|
const result = LeagueStandingsViewDataBuilder.build(
|
|
standingsDto,
|
|
membershipsDto,
|
|
'league-1',
|
|
false
|
|
);
|
|
|
|
expect(result.standings[0].driverId).toBe(standingsDto.standings[0].driverId);
|
|
expect(result.standings[0].position).toBe(standingsDto.standings[0].position);
|
|
expect(result.standings[0].totalPoints).toBe(standingsDto.standings[0].points);
|
|
expect(result.standings[0].racesFinished).toBe(standingsDto.standings[0].races);
|
|
expect(result.standings[0].racesStarted).toBe(standingsDto.standings[0].races);
|
|
expect(result.standings[0].positionChange).toBe(standingsDto.standings[0].positionChange);
|
|
expect(result.standings[0].lastRacePoints).toBe(standingsDto.standings[0].lastRacePoints);
|
|
expect(result.standings[0].droppedRaceIds).toEqual(standingsDto.standings[0].droppedRaceIds);
|
|
expect(result.standings[0].wins).toBe(standingsDto.standings[0].wins);
|
|
expect(result.standings[0].podiums).toBe(standingsDto.standings[0].podiums);
|
|
expect(result.drivers[0].id).toBe(standingsDto.standings[0].driver.id);
|
|
expect(result.drivers[0].name).toBe(standingsDto.standings[0].driver.name);
|
|
expect(result.drivers[0].iracingId).toBe(standingsDto.standings[0].driver.iracingId);
|
|
expect(result.drivers[0].country).toBe(standingsDto.standings[0].driver.country);
|
|
});
|
|
|
|
it('should not modify the input DTOs', () => {
|
|
const standingsDto = {
|
|
standings: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
},
|
|
points: 1250,
|
|
position: 1,
|
|
wins: 5,
|
|
podiums: 10,
|
|
races: 15,
|
|
positionChange: 2,
|
|
lastRacePoints: 25,
|
|
droppedRaceIds: ['race-1'],
|
|
},
|
|
],
|
|
};
|
|
|
|
const membershipsDto = {
|
|
members: [],
|
|
};
|
|
|
|
const originalStandings = JSON.parse(JSON.stringify(standingsDto));
|
|
const originalMemberships = JSON.parse(JSON.stringify(membershipsDto));
|
|
|
|
LeagueStandingsViewDataBuilder.build(
|
|
standingsDto,
|
|
membershipsDto,
|
|
'league-1',
|
|
false
|
|
);
|
|
|
|
expect(standingsDto).toEqual(originalStandings);
|
|
expect(membershipsDto).toEqual(originalMemberships);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle standings with missing optional fields', () => {
|
|
const standingsDto = {
|
|
standings: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
},
|
|
points: 1250,
|
|
position: 1,
|
|
wins: 5,
|
|
podiums: 10,
|
|
races: 15,
|
|
},
|
|
],
|
|
};
|
|
|
|
const membershipsDto = {
|
|
members: [],
|
|
};
|
|
|
|
const result = LeagueStandingsViewDataBuilder.build(
|
|
standingsDto,
|
|
membershipsDto,
|
|
'league-1',
|
|
false
|
|
);
|
|
|
|
expect(result.standings[0].positionChange).toBe(0);
|
|
expect(result.standings[0].lastRacePoints).toBe(0);
|
|
expect(result.standings[0].droppedRaceIds).toEqual([]);
|
|
});
|
|
|
|
it('should handle standings with missing driver field', () => {
|
|
const standingsDto = {
|
|
standings: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: undefined as any,
|
|
points: 1250,
|
|
position: 1,
|
|
wins: 5,
|
|
podiums: 10,
|
|
races: 15,
|
|
positionChange: 2,
|
|
lastRacePoints: 25,
|
|
droppedRaceIds: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
const membershipsDto = {
|
|
members: [],
|
|
};
|
|
|
|
const result = LeagueStandingsViewDataBuilder.build(
|
|
standingsDto,
|
|
membershipsDto,
|
|
'league-1',
|
|
false
|
|
);
|
|
|
|
expect(result.drivers).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle duplicate drivers in standings', () => {
|
|
const standingsDto = {
|
|
standings: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
},
|
|
points: 1250,
|
|
position: 1,
|
|
wins: 5,
|
|
podiums: 10,
|
|
races: 15,
|
|
positionChange: 2,
|
|
lastRacePoints: 25,
|
|
droppedRaceIds: [],
|
|
},
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
},
|
|
points: 1100,
|
|
position: 2,
|
|
wins: 3,
|
|
podiums: 8,
|
|
races: 15,
|
|
positionChange: -1,
|
|
lastRacePoints: 15,
|
|
droppedRaceIds: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
const membershipsDto = {
|
|
members: [],
|
|
};
|
|
|
|
const result = LeagueStandingsViewDataBuilder.build(
|
|
standingsDto,
|
|
membershipsDto,
|
|
'league-1',
|
|
false
|
|
);
|
|
|
|
// Should only have one driver entry
|
|
expect(result.drivers).toHaveLength(1);
|
|
expect(result.drivers[0].id).toBe('driver-1');
|
|
});
|
|
|
|
it('should handle members with different roles', () => {
|
|
const standingsDto = {
|
|
standings: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
},
|
|
points: 1250,
|
|
position: 1,
|
|
wins: 5,
|
|
podiums: 10,
|
|
races: 15,
|
|
positionChange: 2,
|
|
lastRacePoints: 25,
|
|
droppedRaceIds: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
const membershipsDto = {
|
|
members: [
|
|
{
|
|
driverId: 'driver-1',
|
|
driver: {
|
|
id: 'driver-1',
|
|
name: 'Alice',
|
|
iracingId: '11111',
|
|
country: 'UK',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
role: 'admin',
|
|
joinedAt: '2023-06-01T00:00:00.000Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = LeagueStandingsViewDataBuilder.build(
|
|
standingsDto,
|
|
membershipsDto,
|
|
'league-1',
|
|
false
|
|
);
|
|
|
|
expect(result.memberships[0].role).toBe('admin');
|
|
});
|
|
});
|
|
});
|