website refactor

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

View File

@@ -0,0 +1,398 @@
import { describe, expect, it, vi } from 'vitest';
import { LeagueController } from './LeagueController';
import { LeagueService } from './LeagueService';
describe('LeagueController - Detail Endpoints', () => {
let controller: LeagueController;
let mockService: ReturnType<typeof vi.mocked<LeagueService>>;
beforeEach(() => {
mockService = {
getLeagueOwnerSummary: vi.fn(),
getLeagueSeasons: vi.fn(),
getLeagueStats: vi.fn(),
getLeagueMemberships: vi.fn(),
} as never;
controller = new LeagueController(mockService);
});
describe('getLeague', () => {
it('should return league details by ID', async () => {
const mockResult = {
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'John Doe',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
rating: 1500,
rank: 10,
};
mockService.getLeagueOwnerSummary.mockResolvedValue(mockResult as never);
const result = await controller.getLeague('league-1');
expect(result).toEqual(mockResult);
expect(mockService.getLeagueOwnerSummary).toHaveBeenCalledWith({
ownerId: 'unknown',
leagueId: 'league-1',
});
});
it('should handle league not found gracefully', async () => {
mockService.getLeagueOwnerSummary.mockRejectedValue(new Error('League not found'));
await expect(controller.getLeague('non-existent-league')).rejects.toThrow('League not found');
});
it('should return league with minimal information', async () => {
const mockResult = {
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'Simple Driver',
country: 'DE',
joinedAt: '2024-01-01T00:00:00Z',
},
rating: null,
rank: null,
};
mockService.getLeagueOwnerSummary.mockResolvedValue(mockResult as never);
const result = await controller.getLeague('league-1');
expect(result).toEqual(mockResult);
expect(result.driver.name).toBe('Simple Driver');
expect(result.rating).toBeNull();
});
});
describe('getLeagueSeasons', () => {
it('should return seasons for a league', async () => {
const mockResult = [
{
seasonId: 'season-1',
name: 'Season 1',
status: 'active',
startDate: new Date('2024-01-01'),
endDate: new Date('2024-06-30'),
isPrimary: true,
isParallelActive: false,
totalRaces: 12,
completedRaces: 6,
nextRaceAt: new Date('2024-03-15'),
},
{
seasonId: 'season-2',
name: 'Season 2',
status: 'upcoming',
startDate: new Date('2024-07-01'),
endDate: new Date('2024-12-31'),
isPrimary: false,
isParallelActive: false,
totalRaces: 12,
completedRaces: 0,
},
];
mockService.getLeagueSeasons.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSeasons('league-1');
expect(result).toEqual(mockResult);
expect(mockService.getLeagueSeasons).toHaveBeenCalledWith({ leagueId: 'league-1' });
});
it('should return empty array when league has no seasons', async () => {
const mockResult: never[] = [];
mockService.getLeagueSeasons.mockResolvedValue(mockResult);
const result = await controller.getLeagueSeasons('league-1');
expect(result).toEqual([]);
expect(result).toHaveLength(0);
});
it('should handle league with single season', async () => {
const mockResult = [
{
seasonId: 'season-1',
name: 'Season 1',
status: 'active',
startDate: new Date('2024-01-01'),
endDate: new Date('2024-12-31'),
isPrimary: true,
isParallelActive: false,
totalRaces: 24,
completedRaces: 12,
nextRaceAt: new Date('2024-06-15'),
},
];
mockService.getLeagueSeasons.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSeasons('league-1');
expect(result).toEqual(mockResult);
expect(result).toHaveLength(1);
expect(result[0]?.totalRaces).toBe(24);
});
it('should handle seasons with different statuses', async () => {
const mockResult = [
{
seasonId: 'season-1',
name: 'Season 1',
status: 'completed',
startDate: new Date('2024-01-01'),
endDate: new Date('2024-06-30'),
isPrimary: true,
isParallelActive: false,
totalRaces: 12,
completedRaces: 12,
},
{
seasonId: 'season-2',
name: 'Season 2',
status: 'active',
startDate: new Date('2024-07-01'),
endDate: new Date('2024-12-31'),
isPrimary: false,
isParallelActive: false,
totalRaces: 12,
completedRaces: 6,
nextRaceAt: new Date('2024-10-15'),
},
{
seasonId: 'season-3',
name: 'Season 3',
status: 'upcoming',
startDate: new Date('2025-01-01'),
endDate: new Date('2025-06-30'),
isPrimary: false,
isParallelActive: false,
totalRaces: 12,
completedRaces: 0,
},
];
mockService.getLeagueSeasons.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSeasons('league-1');
expect(result).toEqual(mockResult);
expect(result).toHaveLength(3);
expect(result[0]?.status).toBe('completed');
expect(result[1]?.status).toBe('active');
expect(result[2]?.status).toBe('upcoming');
});
});
describe('getLeagueStats', () => {
it('should return league statistics', async () => {
const mockResult = {
totalMembers: 25,
totalRaces: 150,
averageRating: 1450.5,
};
mockService.getLeagueStats.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStats('league-1');
expect(result).toEqual(mockResult);
expect(mockService.getLeagueStats).toHaveBeenCalledWith('league-1');
});
it('should return empty stats for new league', async () => {
const mockResult = {
totalMembers: 0,
totalRaces: 0,
averageRating: 0,
};
mockService.getLeagueStats.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStats('league-1');
expect(result).toEqual(mockResult);
expect(result.totalMembers).toBe(0);
expect(result.totalRaces).toBe(0);
});
it('should handle league with extensive statistics', async () => {
const mockResult = {
totalMembers: 100,
totalRaces: 500,
averageRating: 1650.75,
};
mockService.getLeagueStats.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStats('league-1');
expect(result).toEqual(mockResult);
expect(result.totalRaces).toBe(500);
expect(result.totalMembers).toBe(100);
});
});
describe('getLeagueMemberships', () => {
it('should return league memberships', async () => {
const mockResult = {
members: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'John Doe',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
role: 'owner',
joinedAt: '2024-01-01T00:00:00Z',
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
iracingId: '67890',
name: 'Jane Smith',
country: 'UK',
joinedAt: '2024-01-15T00:00:00Z',
},
role: 'admin',
joinedAt: '2024-01-15T00:00:00Z',
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
iracingId: '11111',
name: 'Bob Johnson',
country: 'CA',
joinedAt: '2024-02-01T00:00:00Z',
},
role: 'member',
joinedAt: '2024-02-01T00:00:00Z',
},
],
};
mockService.getLeagueMemberships.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueMemberships('league-1');
expect(result).toEqual(mockResult);
expect(mockService.getLeagueMemberships).toHaveBeenCalledWith('league-1');
});
it('should return empty memberships for league with no members', async () => {
const mockResult = { members: [] };
mockService.getLeagueMemberships.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueMemberships('league-1');
expect(result).toEqual(mockResult);
expect(result.members).toHaveLength(0);
});
it('should handle league with only owner', async () => {
const mockResult = {
members: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'Owner',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
role: 'owner',
joinedAt: '2024-01-01T00:00:00Z',
},
],
};
mockService.getLeagueMemberships.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueMemberships('league-1');
expect(result).toEqual(mockResult);
expect(result.members).toHaveLength(1);
expect(result.members[0]?.role).toBe('owner');
});
it('should handle league with mixed roles', async () => {
const mockResult = {
members: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'Owner',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
role: 'owner',
joinedAt: '2024-01-01T00:00:00Z',
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
iracingId: '67890',
name: 'Admin 1',
country: 'UK',
joinedAt: '2024-01-02T00:00:00Z',
},
role: 'admin',
joinedAt: '2024-01-02T00:00:00Z',
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
iracingId: '11111',
name: 'Admin 2',
country: 'CA',
joinedAt: '2024-01-03T00:00:00Z',
},
role: 'admin',
joinedAt: '2024-01-03T00:00:00Z',
},
{
driverId: 'driver-4',
driver: {
id: 'driver-4',
iracingId: '22222',
name: 'Member 1',
country: 'DE',
joinedAt: '2024-01-04T00:00:00Z',
},
role: 'member',
joinedAt: '2024-01-04T00:00:00Z',
},
{
driverId: 'driver-5',
driver: {
id: 'driver-5',
iracingId: '33333',
name: 'Member 2',
country: 'FR',
joinedAt: '2024-01-05T00:00:00Z',
},
role: 'member',
joinedAt: '2024-01-05T00:00:00Z',
},
],
};
mockService.getLeagueMemberships.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueMemberships('league-1');
expect(result).toEqual(mockResult);
expect(result.members).toHaveLength(5);
expect(result.members.filter(m => m.role === 'owner')).toHaveLength(1);
expect(result.members.filter(m => m.role === 'admin')).toHaveLength(2);
expect(result.members.filter(m => m.role === 'member')).toHaveLength(2);
});
});
});

View File

@@ -0,0 +1,205 @@
import { describe, expect, it, vi } from 'vitest';
import { LeagueController } from './LeagueController';
import { LeagueService } from './LeagueService';
describe('LeagueController - Discovery Endpoints', () => {
let controller: LeagueController;
let mockService: ReturnType<typeof vi.mocked<LeagueService>>;
beforeEach(() => {
mockService = {
getAllLeaguesWithCapacity: vi.fn(),
getAllLeaguesWithCapacityAndScoring: vi.fn(),
getTotalLeagues: vi.fn(),
} as never;
controller = new LeagueController(mockService);
});
describe('getAllLeaguesWithCapacity', () => {
it('should return leagues with capacity information', async () => {
const mockResult = {
leagues: [
{
id: 'league-1',
name: 'GT3 Masters',
description: 'A GT3 racing league',
ownerId: 'owner-1',
maxDrivers: 32,
currentDrivers: 25,
isPublic: true,
},
],
totalCount: 1,
};
mockService.getAllLeaguesWithCapacity.mockResolvedValue(mockResult as never);
const result = await controller.getAllLeaguesWithCapacity();
expect(result).toEqual(mockResult);
expect(mockService.getAllLeaguesWithCapacity).toHaveBeenCalledTimes(1);
});
it('should return empty array when no leagues exist', async () => {
const mockResult = { leagues: [], totalCount: 0 };
mockService.getAllLeaguesWithCapacity.mockResolvedValue(mockResult as never);
const result = await controller.getAllLeaguesWithCapacity();
expect(result).toEqual(mockResult);
expect(result.leagues).toHaveLength(0);
expect(result.totalCount).toBe(0);
});
it('should handle multiple leagues with different capacities', async () => {
const mockResult = {
leagues: [
{
id: 'league-1',
name: 'Small League',
description: 'Small league',
ownerId: 'owner-1',
maxDrivers: 10,
currentDrivers: 8,
isPublic: true,
},
{
id: 'league-2',
name: 'Large League',
description: 'Large league',
ownerId: 'owner-2',
maxDrivers: 50,
currentDrivers: 45,
isPublic: true,
},
],
totalCount: 2,
};
mockService.getAllLeaguesWithCapacity.mockResolvedValue(mockResult as never);
const result = await controller.getAllLeaguesWithCapacity();
expect(result).toEqual(mockResult);
expect(result.leagues).toHaveLength(2);
expect(result.leagues[0]?.maxDrivers).toBe(10);
expect(result.leagues[1]?.maxDrivers).toBe(50);
});
});
describe('getAllLeaguesWithCapacityAndScoring', () => {
it('should return leagues with capacity and scoring information', async () => {
const mockResult = {
leagues: [
{
id: 'league-1',
name: 'GT3 Masters',
description: 'A GT3 racing league',
ownerId: 'owner-1',
maxDrivers: 32,
currentDrivers: 25,
isPublic: true,
scoringConfig: {
pointsSystem: 'standard',
pointsPerRace: 25,
bonusPoints: true,
},
},
],
totalCount: 1,
};
mockService.getAllLeaguesWithCapacityAndScoring.mockResolvedValue(mockResult as never);
const result = await controller.getAllLeaguesWithCapacityAndScoring();
expect(result).toEqual(mockResult);
expect(mockService.getAllLeaguesWithCapacityAndScoring).toHaveBeenCalledTimes(1);
});
it('should return empty array when no leagues exist', async () => {
const mockResult = { leagues: [], totalCount: 0 };
mockService.getAllLeaguesWithCapacityAndScoring.mockResolvedValue(mockResult as never);
const result = await controller.getAllLeaguesWithCapacityAndScoring();
expect(result).toEqual(mockResult);
expect(result.leagues).toHaveLength(0);
expect(result.totalCount).toBe(0);
});
it('should handle leagues with different scoring configurations', async () => {
const mockResult = {
leagues: [
{
id: 'league-1',
name: 'Standard League',
description: 'Standard scoring',
ownerId: 'owner-1',
maxDrivers: 32,
currentDrivers: 20,
isPublic: true,
scoringConfig: {
pointsSystem: 'standard',
pointsPerRace: 25,
bonusPoints: true,
},
},
{
id: 'league-2',
name: 'Custom League',
description: 'Custom scoring',
ownerId: 'owner-2',
maxDrivers: 20,
currentDrivers: 15,
isPublic: true,
scoringConfig: {
pointsSystem: 'custom',
pointsPerRace: 50,
bonusPoints: false,
},
},
],
totalCount: 2,
};
mockService.getAllLeaguesWithCapacityAndScoring.mockResolvedValue(mockResult as never);
const result = await controller.getAllLeaguesWithCapacityAndScoring();
expect(result).toEqual(mockResult);
expect(result.leagues).toHaveLength(2);
expect(result.leagues[0]?.scoringConfig.pointsSystem).toBe('standard');
expect(result.leagues[1]?.scoringConfig.pointsSystem).toBe('custom');
});
});
describe('getTotalLeagues', () => {
it('should return total leagues count', async () => {
const mockResult = { totalLeagues: 42 };
mockService.getTotalLeagues.mockResolvedValue(mockResult as never);
const result = await controller.getTotalLeagues();
expect(result).toEqual(mockResult);
expect(mockService.getTotalLeagues).toHaveBeenCalledTimes(1);
});
it('should return zero when no leagues exist', async () => {
const mockResult = { totalLeagues: 0 };
mockService.getTotalLeagues.mockResolvedValue(mockResult as never);
const result = await controller.getTotalLeagues();
expect(result).toEqual(mockResult);
expect(result.totalLeagues).toBe(0);
});
it('should handle large league counts', async () => {
const mockResult = { totalLeagues: 1000 };
mockService.getTotalLeagues.mockResolvedValue(mockResult as never);
const result = await controller.getTotalLeagues();
expect(result).toEqual(mockResult);
expect(result.totalLeagues).toBe(1000);
});
});
});

View File

@@ -0,0 +1,231 @@
import { describe, expect, it, vi } from 'vitest';
import { LeagueController } from './LeagueController';
import { LeagueService } from './LeagueService';
describe('LeagueController - Schedule Endpoints', () => {
let controller: LeagueController;
let mockService: ReturnType<typeof vi.mocked<LeagueService>>;
beforeEach(() => {
mockService = {
getLeagueSchedule: vi.fn(),
} as never;
controller = new LeagueController(mockService);
});
describe('getLeagueSchedule', () => {
it('should return league schedule', async () => {
const mockResult = {
seasonId: 'season-1',
published: true,
races: [
{
id: 'race-1',
name: 'Spa Endurance',
date: '2024-03-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-2',
name: 'Monza Sprint',
date: '2024-03-22T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-3',
name: 'Nürburgring Endurance',
date: '2024-03-29T14:00:00Z',
leagueName: 'GT3 Masters',
},
],
};
mockService.getLeagueSchedule.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSchedule('league-1', {});
expect(result).toEqual(mockResult);
expect(mockService.getLeagueSchedule).toHaveBeenCalledWith('league-1', {});
});
it('should return empty schedule for league with no races', async () => {
const mockResult = {
seasonId: 'season-1',
published: false,
races: [],
};
mockService.getLeagueSchedule.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSchedule('league-1', {});
expect(result).toEqual(mockResult);
expect(result.races).toHaveLength(0);
expect(result.published).toBe(false);
});
it('should handle schedule with specific season ID', async () => {
const mockResult = {
seasonId: 'season-2',
published: true,
races: [
{
id: 'race-10',
name: 'Silverstone Endurance',
date: '2024-08-01T14:00:00Z',
leagueName: 'GT3 Masters',
},
],
};
mockService.getLeagueSchedule.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSchedule('league-1', { seasonId: 'season-2' });
expect(result).toEqual(mockResult);
expect(mockService.getLeagueSchedule).toHaveBeenCalledWith('league-1', { seasonId: 'season-2' });
});
it('should handle schedule with multiple races on same track', async () => {
const mockResult = {
seasonId: 'season-1',
published: true,
races: [
{
id: 'race-1',
name: 'Spa Endurance',
date: '2024-03-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-2',
name: 'Spa Sprint',
date: '2024-04-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
],
};
mockService.getLeagueSchedule.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSchedule('league-1', {});
expect(result).toEqual(mockResult);
expect(result.races).toHaveLength(2);
expect(result.races[0]?.name).toContain('Spa');
expect(result.races[1]?.name).toContain('Spa');
});
it('should handle schedule with different race names', async () => {
const mockResult = {
seasonId: 'season-1',
published: true,
races: [
{
id: 'race-1',
name: 'Spa Endurance',
date: '2024-01-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-2',
name: 'Monza Sprint',
date: '2024-02-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-3',
name: 'Nürburgring Endurance',
date: '2024-03-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-4',
name: 'Silverstone Sprint',
date: '2024-04-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
],
};
mockService.getLeagueSchedule.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSchedule('league-1', {});
expect(result).toEqual(mockResult);
expect(result.races).toHaveLength(4);
expect(result.races[0]?.name).toBe('Spa Endurance');
expect(result.races[1]?.name).toBe('Monza Sprint');
expect(result.races[2]?.name).toBe('Nürburgring Endurance');
expect(result.races[3]?.name).toBe('Silverstone Sprint');
});
it('should handle schedule with different dates', async () => {
const mockResult = {
seasonId: 'season-1',
published: true,
races: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-01-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-02-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-3',
name: 'Race 3',
date: '2024-03-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
],
};
mockService.getLeagueSchedule.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSchedule('league-1', {});
expect(result).toEqual(mockResult);
expect(result.races).toHaveLength(3);
expect(result.races[0]?.date).toBe('2024-01-15T14:00:00Z');
expect(result.races[1]?.date).toBe('2024-02-15T14:00:00Z');
expect(result.races[2]?.date).toBe('2024-03-15T14:00:00Z');
});
it('should handle schedule with league name variations', async () => {
const mockResult = {
seasonId: 'season-1',
published: true,
races: [
{
id: 'race-1',
name: 'Race 1',
date: '2024-03-15T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-2',
name: 'Race 2',
date: '2024-03-22T14:00:00Z',
leagueName: 'GT3 Masters',
},
{
id: 'race-3',
name: 'Race 3',
date: '2024-03-29T14:00:00Z',
leagueName: null,
},
],
};
mockService.getLeagueSchedule.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueSchedule('league-1', {});
expect(result).toEqual(mockResult);
expect(result.races).toHaveLength(3);
expect(result.races[0]?.leagueName).toBe('GT3 Masters');
expect(result.races[1]?.leagueName).toBe('GT3 Masters');
expect(result.races[2]?.leagueName).toBeNull();
});
});
});

View File

@@ -0,0 +1,388 @@
import { describe, expect, it, vi } from 'vitest';
import { LeagueController } from './LeagueController';
import { LeagueService } from './LeagueService';
describe('LeagueController - Standings Endpoints', () => {
let controller: LeagueController;
let mockService: ReturnType<typeof vi.mocked<LeagueService>>;
beforeEach(() => {
mockService = {
getLeagueStandings: vi.fn(),
} as never;
controller = new LeagueController(mockService);
});
describe('getLeagueStandings', () => {
it('should return league standings', async () => {
const mockResult = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'John Doe',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 1,
points: 150,
races: 12,
wins: 5,
podiums: 8,
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
iracingId: '67890',
name: 'Jane Smith',
country: 'UK',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 2,
points: 145,
races: 12,
wins: 4,
podiums: 7,
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
iracingId: '11111',
name: 'Bob Johnson',
country: 'CA',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 3,
points: 140,
races: 12,
wins: 3,
podiums: 6,
},
],
};
mockService.getLeagueStandings.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStandings('league-1');
expect(result).toEqual(mockResult);
expect(mockService.getLeagueStandings).toHaveBeenCalledWith('league-1');
});
it('should return empty standings for league with no races', async () => {
const mockResult = { standings: [] };
mockService.getLeagueStandings.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStandings('league-1');
expect(result).toEqual(mockResult);
expect(result.standings).toHaveLength(0);
});
it('should handle standings with single driver', async () => {
const mockResult = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'John Doe',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 1,
points: 100,
races: 10,
wins: 10,
podiums: 10,
},
],
};
mockService.getLeagueStandings.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStandings('league-1');
expect(result).toEqual(mockResult);
expect(result.standings).toHaveLength(1);
expect(result.standings[0]?.position).toBe(1);
expect(result.standings[0]?.points).toBe(100);
});
it('should handle standings with many drivers', async () => {
const mockResult = {
standings: Array.from({ length: 20 }, (_, i) => ({
driverId: `driver-${i + 1}`,
driver: {
id: `driver-${i + 1}`,
iracingId: `${10000 + i}`,
name: `Driver ${i + 1}`,
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
position: i + 1,
points: 100 - i,
races: 12,
wins: Math.max(0, 5 - i),
podiums: Math.max(0, 10 - i),
})),
};
mockService.getLeagueStandings.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStandings('league-1');
expect(result).toEqual(mockResult);
expect(result.standings).toHaveLength(20);
expect(result.standings[0]?.position).toBe(1);
expect(result.standings[19]?.position).toBe(20);
});
it('should handle standings with tied points', async () => {
const mockResult = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'John Doe',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 1,
points: 150,
races: 12,
wins: 5,
podiums: 8,
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
iracingId: '67890',
name: 'Jane Smith',
country: 'UK',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 2,
points: 150,
races: 12,
wins: 5,
podiums: 7,
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
iracingId: '11111',
name: 'Bob Johnson',
country: 'CA',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 3,
points: 145,
races: 12,
wins: 4,
podiums: 6,
},
],
};
mockService.getLeagueStandings.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStandings('league-1');
expect(result).toEqual(mockResult);
expect(result.standings).toHaveLength(3);
expect(result.standings[0]?.points).toBe(150);
expect(result.standings[1]?.points).toBe(150);
expect(result.standings[2]?.points).toBe(145);
});
it('should handle standings with varying race counts', async () => {
const mockResult = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'John Doe',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 1,
points: 150,
races: 12,
wins: 5,
podiums: 8,
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
iracingId: '67890',
name: 'Jane Smith',
country: 'UK',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 2,
points: 140,
races: 10,
wins: 4,
podiums: 6,
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
iracingId: '11111',
name: 'Bob Johnson',
country: 'CA',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 3,
points: 130,
races: 8,
wins: 3,
podiums: 5,
},
],
};
mockService.getLeagueStandings.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStandings('league-1');
expect(result).toEqual(mockResult);
expect(result.standings).toHaveLength(3);
expect(result.standings[0]?.races).toBe(12);
expect(result.standings[1]?.races).toBe(10);
expect(result.standings[2]?.races).toBe(8);
});
it('should handle standings with varying win counts', async () => {
const mockResult = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'John Doe',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 1,
points: 150,
races: 12,
wins: 10,
podiums: 12,
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
iracingId: '67890',
name: 'Jane Smith',
country: 'UK',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 2,
points: 140,
races: 12,
wins: 2,
podiums: 8,
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
iracingId: '11111',
name: 'Bob Johnson',
country: 'CA',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 3,
points: 130,
races: 12,
wins: 0,
podiums: 4,
},
],
};
mockService.getLeagueStandings.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStandings('league-1');
expect(result).toEqual(mockResult);
expect(result.standings).toHaveLength(3);
expect(result.standings[0]?.wins).toBe(10);
expect(result.standings[1]?.wins).toBe(2);
expect(result.standings[2]?.wins).toBe(0);
});
it('should handle standings with varying podium counts', async () => {
const mockResult = {
standings: [
{
driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'John Doe',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 1,
points: 150,
races: 12,
wins: 5,
podiums: 12,
},
{
driverId: 'driver-2',
driver: {
id: 'driver-2',
iracingId: '67890',
name: 'Jane Smith',
country: 'UK',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 2,
points: 140,
races: 12,
wins: 4,
podiums: 8,
},
{
driverId: 'driver-3',
driver: {
id: 'driver-3',
iracingId: '11111',
name: 'Bob Johnson',
country: 'CA',
joinedAt: '2024-01-01T00:00:00Z',
},
position: 3,
points: 130,
races: 12,
wins: 3,
podiums: 4,
},
],
};
mockService.getLeagueStandings.mockResolvedValue(mockResult as never);
const result = await controller.getLeagueStandings('league-1');
expect(result).toEqual(mockResult);
expect(result.standings).toHaveLength(3);
expect(result.standings[0]?.podiums).toBe(12);
expect(result.standings[1]?.podiums).toBe(8);
expect(result.standings[2]?.podiums).toBe(4);
});
});
});

View File

@@ -0,0 +1,257 @@
import { requestContextMiddleware } from '@adapters/http/RequestContext';
import { Result } from '@core/shared/domain/Result';
import { describe, expect, it, vi } from 'vitest';
import { LeagueService } from './LeagueService';
async function withUserId<T>(userId: string, fn: () => Promise<T>): Promise<T> {
const req = { user: { userId } };
const res = {};
return await new Promise<T>((resolve, reject) => {
requestContextMiddleware(req as never, res as never, () => {
fn().then(resolve, reject);
});
});
}
describe('LeagueService - All Endpoints', () => {
it('covers all league endpoint happy paths and error branches', async () => {
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
const ok = async () => Result.ok(undefined);
const err = async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never);
// Discovery use cases
const getAllLeaguesWithCapacityUseCase = { execute: vi.fn(async () => Result.ok({ leagues: [] })) };
const getAllLeaguesWithCapacityAndScoringUseCase = { execute: vi.fn(ok) };
const getTotalLeaguesUseCase = { execute: vi.fn(ok) };
// Detail use cases
const getLeagueOwnerSummaryUseCase = { execute: vi.fn(ok) };
const getLeagueSeasonsUseCase = { execute: vi.fn(ok) };
const getLeagueStatsUseCase = { execute: vi.fn(ok) };
const getLeagueMembershipsUseCase = { execute: vi.fn(ok) };
// Schedule use case
const getLeagueScheduleUseCase = { execute: vi.fn(ok) };
// Standings use case
const getLeagueStandingsUseCase = { execute: vi.fn(ok) };
// Other use cases (for completeness)
const getLeagueFullConfigUseCase = { execute: vi.fn(ok) };
const getLeagueScoringConfigUseCase = { execute: vi.fn(ok) };
const listLeagueScoringPresetsUseCase = { execute: vi.fn(ok) };
const joinLeagueUseCase = { execute: vi.fn(ok) };
const transferLeagueOwnershipUseCase = { execute: vi.fn(ok) };
const createLeagueWithSeasonAndScoringUseCase = { execute: vi.fn(ok) };
const getLeagueJoinRequestsUseCase = { execute: vi.fn(ok) };
const approveLeagueJoinRequestUseCase = { execute: vi.fn(ok) };
const rejectLeagueJoinRequestUseCase = { execute: vi.fn(ok) };
const removeLeagueMemberUseCase = { execute: vi.fn(ok) };
const updateLeagueMemberRoleUseCase = { execute: vi.fn(ok) };
const getLeagueProtestsUseCase = { execute: vi.fn(ok) };
const getLeagueAdminPermissionsUseCase = { execute: vi.fn(ok) };
const getLeagueWalletUseCase = { execute: vi.fn(ok) };
const withdrawFromLeagueWalletUseCase = { execute: vi.fn(ok) };
const getSeasonSponsorshipsUseCase = { execute: vi.fn(ok) };
const getLeagueRosterMembersUseCase = { execute: vi.fn(ok) };
const getLeagueRosterJoinRequestsUseCase = { execute: vi.fn(ok) };
// Schedule mutation use cases
const createLeagueSeasonScheduleRaceUseCase = { execute: vi.fn(ok) };
const updateLeagueSeasonScheduleRaceUseCase = { execute: vi.fn(ok) };
const deleteLeagueSeasonScheduleRaceUseCase = { execute: vi.fn(ok) };
const publishLeagueSeasonScheduleUseCase = { execute: vi.fn(ok) };
const unpublishLeagueSeasonScheduleUseCase = { execute: vi.fn(ok) };
// Presenters
const allLeaguesWithCapacityPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ leagues: [] })) };
const allLeaguesWithCapacityAndScoringPresenter = {
present: vi.fn(),
getViewModel: vi.fn(() => ({ leagues: [], totalCount: 0 })),
};
const leagueStandingsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ standings: [] })) };
const leagueProtestsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ protests: [] })) };
const seasonSponsorshipsPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ sponsorships: [] })) };
const leagueScoringPresetsPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ presets: [] })) };
const approveLeagueJoinRequestPresenter = {
present: vi.fn(),
getViewModel: vi.fn(() => ({ success: true }))
};
const createLeaguePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ id: 'l1' })) };
const getLeagueAdminPermissionsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ canManage: true })) };
const getLeagueMembershipsPresenter = {
reset: vi.fn(),
present: vi.fn(),
getViewModel: vi.fn(() => ({ memberships: { members: [] } })),
};
const getLeagueRosterMembersPresenter = {
reset: vi.fn(),
present: vi.fn(),
getViewModel: vi.fn(() => ([])),
};
const getLeagueRosterJoinRequestsPresenter = {
reset: vi.fn(),
present: vi.fn(),
getViewModel: vi.fn(() => ([])),
};
const getLeagueOwnerSummaryPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ driver: { id: 'd1', iracingId: '12345', name: 'Driver', country: 'US', joinedAt: '2024-01-01T00:00:00Z' }, rating: 1500, rank: 10 })) };
const getLeagueSeasonsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ([])) };
const joinLeaguePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
const leagueSchedulePresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ({ seasonId: 'season-1', published: false, races: [] })) };
const leagueStatsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ totalMembers: 0, totalRaces: 0, averageRating: 0 })) };
const rejectLeagueJoinRequestPresenter = {
present: vi.fn(),
getViewModel: vi.fn(() => ({ success: true }))
};
const removeLeagueMemberPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
const totalLeaguesPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ totalLeagues: 0 })) };
const transferLeagueOwnershipPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
const updateLeagueMemberRolePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
const leagueConfigPresenter = {
present: vi.fn(),
getViewModel: vi.fn(() => ({ form: {} }))
};
const leagueScoringConfigPresenter = {
present: vi.fn(),
getViewModel: vi.fn(() => ({ config: {} }))
};
const getLeagueWalletPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ balance: 0 })) };
const withdrawFromLeagueWalletPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) };
const leagueJoinRequestsPresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ({ joinRequests: [] })) };
const createLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ raceId: 'race-1' })) };
const updateLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) };
const deleteLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) };
const publishLeagueSeasonSchedulePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true, published: true })) };
const unpublishLeagueSeasonSchedulePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true, published: false })) };
const service = new (LeagueService as unknown as { new (...args: never[]): LeagueService })(
getAllLeaguesWithCapacityUseCase as never,
getAllLeaguesWithCapacityAndScoringUseCase as never,
getLeagueStandingsUseCase as never,
getLeagueStatsUseCase as never,
getLeagueFullConfigUseCase as never,
getLeagueScoringConfigUseCase as never,
listLeagueScoringPresetsUseCase as never,
joinLeagueUseCase as never,
transferLeagueOwnershipUseCase as never,
createLeagueWithSeasonAndScoringUseCase as never,
getTotalLeaguesUseCase as never,
getLeagueJoinRequestsUseCase as never,
approveLeagueJoinRequestUseCase as never,
rejectLeagueJoinRequestUseCase as never,
removeLeagueMemberUseCase as never,
updateLeagueMemberRoleUseCase as never,
getLeagueOwnerSummaryUseCase as never,
getLeagueProtestsUseCase as never,
getLeagueSeasonsUseCase as never,
getLeagueMembershipsUseCase as never,
getLeagueScheduleUseCase as never,
getLeagueAdminPermissionsUseCase as never,
getLeagueWalletUseCase as never,
withdrawFromLeagueWalletUseCase as never,
getSeasonSponsorshipsUseCase as never,
createLeagueSeasonScheduleRaceUseCase as never,
updateLeagueSeasonScheduleRaceUseCase as never,
deleteLeagueSeasonScheduleRaceUseCase as never,
publishLeagueSeasonScheduleUseCase as never,
unpublishLeagueSeasonScheduleUseCase as never,
logger as never,
allLeaguesWithCapacityPresenter as never,
allLeaguesWithCapacityAndScoringPresenter as never,
leagueStandingsPresenter as never,
leagueProtestsPresenter as never,
seasonSponsorshipsPresenter as never,
leagueScoringPresetsPresenter as never,
approveLeagueJoinRequestPresenter as never,
createLeaguePresenter as never,
getLeagueAdminPermissionsPresenter as never,
getLeagueMembershipsPresenter as never,
getLeagueOwnerSummaryPresenter as never,
getLeagueSeasonsPresenter as never,
joinLeaguePresenter as never,
leagueSchedulePresenter as never,
leagueStatsPresenter as never,
rejectLeagueJoinRequestPresenter as never,
removeLeagueMemberPresenter as never,
totalLeaguesPresenter as never,
transferLeagueOwnershipPresenter as never,
updateLeagueMemberRolePresenter as never,
leagueConfigPresenter as never,
leagueScoringConfigPresenter as never,
getLeagueWalletPresenter as never,
withdrawFromLeagueWalletPresenter as never,
leagueJoinRequestsPresenter as never,
createLeagueSeasonScheduleRacePresenter as never,
updateLeagueSeasonScheduleRacePresenter as never,
deleteLeagueSeasonScheduleRacePresenter as never,
publishLeagueSeasonSchedulePresenter as never,
unpublishLeagueSeasonSchedulePresenter as never,
// Roster admin read delegation (added for strict TDD)
getLeagueRosterMembersUseCase as never,
getLeagueRosterJoinRequestsUseCase as never,
getLeagueRosterMembersPresenter as never,
getLeagueRosterJoinRequestsPresenter as never,
);
// Discovery endpoints
await expect(service.getAllLeaguesWithCapacity()).resolves.toEqual({ leagues: [] });
await expect(service.getAllLeaguesWithCapacityAndScoring()).resolves.toEqual({ leagues: [], totalCount: 0 });
await expect(service.getTotalLeagues()).resolves.toEqual({ totalLeagues: 0 });
// Detail endpoints
await expect(service.getLeagueOwnerSummary({ leagueId: 'l1' } as never)).resolves.toEqual({ driver: { id: 'd1', iracingId: '12345', name: 'Driver', country: 'US', joinedAt: '2024-01-01T00:00:00Z' }, rating: 1500, rank: 10 });
await expect(service.getLeagueSeasons({ leagueId: 'l1' } as never)).resolves.toEqual([]);
await expect(service.getLeagueStats('l1')).resolves.toEqual({ totalMembers: 0, totalRaces: 0, averageRating: 0 });
await expect(service.getLeagueMemberships('l1')).resolves.toEqual({ members: [] });
// Schedule endpoint
await expect(service.getLeagueSchedule('l1')).resolves.toEqual({ seasonId: 'season-1', published: false, races: [] });
expect(getLeagueScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' });
getLeagueScheduleUseCase.execute.mockClear();
await expect(service.getLeagueSchedule('l1', { seasonId: 'season-x' } as never)).resolves.toEqual({
seasonId: 'season-1',
published: false,
races: [],
});
expect(getLeagueScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1', seasonId: 'season-x' });
// Standings endpoint
await expect(service.getLeagueStandings('l1')).resolves.toEqual({ standings: [] });
// Error branches: use case returns error result
getAllLeaguesWithCapacityUseCase.execute.mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never));
await expect(service.getAllLeaguesWithCapacity()).rejects.toThrow('REPOSITORY_ERROR');
getLeagueFullConfigUseCase.execute.mockResolvedValueOnce(
Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never)
);
await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as never)).resolves.toBeNull();
getLeagueScoringConfigUseCase.execute.mockImplementationOnce(async () => {
throw new Error('boom');
});
await expect(service.getLeagueScoringConfig('l1')).resolves.toBeNull();
// Cover non-Error throw branches for logger.error wrapping
getLeagueFullConfigUseCase.execute.mockResolvedValueOnce(
Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never)
);
await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as never)).resolves.toBeNull();
getLeagueScoringConfigUseCase.execute.mockImplementationOnce(async () => {
throw 'boom';
});
await expect(service.getLeagueScoringConfig('l1')).resolves.toBeNull();
// keep lint happy (ensures err() used)
await err();
});
});