view models

This commit is contained in:
2025-12-18 13:48:35 +01:00
parent cc2553876a
commit 91adbb9c83
71 changed files with 3119 additions and 359 deletions

View File

@@ -0,0 +1,111 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { LeagueMembershipService } from './LeagueMembershipService';
import { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { LeagueMemberViewModel } from '../../view-models';
import type { LeagueMemberDTO } from '../../types/generated';
describe('LeagueMembershipService', () => {
let mockApiClient: Mocked<LeaguesApiClient>;
let service: LeagueMembershipService;
beforeEach(() => {
mockApiClient = {
getMemberships: vi.fn(),
removeMember: vi.fn(),
} as Mocked<LeaguesApiClient>;
service = new LeagueMembershipService(mockApiClient);
});
describe('getLeagueMemberships', () => {
it('should call apiClient.getMemberships and return array of LeagueMemberViewModel', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
members: [
{ driverId: 'driver-1' },
{ driverId: 'driver-2' },
] as LeagueMemberDTO[],
};
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
expect(result).toHaveLength(2);
expect(result[0]).toBeInstanceOf(LeagueMemberViewModel);
expect(result[0].driverId).toBe('driver-1');
expect(result[0].currentUserId).toBe(currentUserId);
expect(result[1]).toBeInstanceOf(LeagueMemberViewModel);
expect(result[1].driverId).toBe('driver-2');
expect(result[1].currentUserId).toBe(currentUserId);
});
it('should handle empty members array', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
members: [] as LeagueMemberDTO[],
};
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
expect(result).toHaveLength(0);
});
it('should throw error when apiClient.getMemberships fails', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const error = new Error('API call failed');
mockApiClient.getMemberships.mockRejectedValue(error);
await expect(service.getLeagueMemberships(leagueId, currentUserId)).rejects.toThrow('API call failed');
});
});
describe('removeMember', () => {
it('should call apiClient.removeMember and return the result', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const mockResult = { success: true };
mockApiClient.removeMember.mockResolvedValue(mockResult);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
expect(result).toEqual(mockResult);
});
it('should handle unsuccessful removal', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const mockResult = { success: false };
mockApiClient.removeMember.mockResolvedValue(mockResult);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
expect(result.success).toBe(false);
});
it('should throw error when apiClient.removeMember fails', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const error = new Error('API call failed');
mockApiClient.removeMember.mockRejectedValue(error);
await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,6 +1,11 @@
import { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
import { LeagueMemberViewModel } from '@/lib/view-models/LeagueMemberViewModel';
import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { LeagueMemberViewModel } from '../../view-models';
import type { LeagueMemberDTO } from '../../types/generated';
// TODO: Move to generated types when available
type LeagueMembershipsDTO = {
members: LeagueMemberDTO[];
};
/**
* League Membership Service
@@ -17,7 +22,7 @@ export class LeagueMembershipService {
* Get league memberships with view model transformation
*/
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMemberViewModel[]> {
const dto = await this.apiClient.getMemberships(leagueId);
const dto: LeagueMembershipsDTO = await this.apiClient.getMemberships(leagueId);
return dto.members.map((member: LeagueMemberDTO) => new LeagueMemberViewModel(member, currentUserId));
}

View File

@@ -0,0 +1,299 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { LeagueService } from './LeagueService';
import { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { LeagueSummaryViewModel, LeagueStandingsViewModel, LeagueStatsViewModel, LeagueScheduleViewModel, LeagueMembershipsViewModel, CreateLeagueViewModel, RemoveMemberViewModel, LeagueMemberViewModel } from '../../view-models';
import type { LeagueWithCapacityDTO, CreateLeagueInputDTO, CreateLeagueOutputDTO, RemoveLeagueMemberOutputDTO, LeagueMemberDTO } from '../../types/generated';
describe('LeagueService', () => {
let mockApiClient: Mocked<LeaguesApiClient>;
let service: LeagueService;
beforeEach(() => {
mockApiClient = {
getAllWithCapacity: vi.fn(),
getStandings: vi.fn(),
getTotal: vi.fn(),
getSchedule: vi.fn(),
getMemberships: vi.fn(),
create: vi.fn(),
removeMember: vi.fn(),
} as Mocked<LeaguesApiClient>;
service = new LeagueService(mockApiClient);
});
describe('getAllLeagues', () => {
it('should call apiClient.getAllWithCapacity and return array of LeagueSummaryViewModel', async () => {
const mockDto = {
leagues: [
{ id: 'league-1', name: 'League One' },
{ id: 'league-2', name: 'League Two' },
] as LeagueWithCapacityDTO[],
};
mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto);
const result = await service.getAllLeagues();
expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled();
expect(result).toHaveLength(2);
expect(result[0]).toBeInstanceOf(LeagueSummaryViewModel);
expect(result[0].id).toBe('league-1');
expect(result[1]).toBeInstanceOf(LeagueSummaryViewModel);
expect(result[1].id).toBe('league-2');
});
it('should handle empty leagues array', async () => {
const mockDto = {
leagues: [] as LeagueWithCapacityDTO[],
};
mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto);
const result = await service.getAllLeagues();
expect(result).toHaveLength(0);
});
it('should throw error when apiClient.getAllWithCapacity fails', async () => {
const error = new Error('API call failed');
mockApiClient.getAllWithCapacity.mockRejectedValue(error);
await expect(service.getAllLeagues()).rejects.toThrow('API call failed');
});
});
describe('getLeagueStandings', () => {
it('should call apiClient.getStandings and return LeagueStandingsViewModel', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
id: leagueId,
name: 'Test League',
};
mockApiClient.getStandings.mockResolvedValue(mockDto);
const result = await service.getLeagueStandings(leagueId, currentUserId);
expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueStandingsViewModel);
});
it('should throw error when apiClient.getStandings fails', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const error = new Error('API call failed');
mockApiClient.getStandings.mockRejectedValue(error);
await expect(service.getLeagueStandings(leagueId, currentUserId)).rejects.toThrow('API call failed');
});
});
describe('getLeagueStats', () => {
it('should call apiClient.getTotal and return LeagueStatsViewModel', async () => {
const mockDto = { totalLeagues: 42 };
mockApiClient.getTotal.mockResolvedValue(mockDto);
const result = await service.getLeagueStats();
expect(mockApiClient.getTotal).toHaveBeenCalled();
expect(result).toBeInstanceOf(LeagueStatsViewModel);
expect(result.totalLeagues).toBe(42);
});
it('should throw error when apiClient.getTotal fails', async () => {
const error = new Error('API call failed');
mockApiClient.getTotal.mockRejectedValue(error);
await expect(service.getLeagueStats()).rejects.toThrow('API call failed');
});
});
describe('getLeagueSchedule', () => {
it('should call apiClient.getSchedule and return LeagueScheduleViewModel', async () => {
const leagueId = 'league-123';
const mockDto = { races: [{ id: 'race-1' }, { id: 'race-2' }] };
mockApiClient.getSchedule.mockResolvedValue(mockDto);
const result = await service.getLeagueSchedule(leagueId);
expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueScheduleViewModel);
expect(result.races).toEqual(mockDto.races);
});
it('should handle empty races array', async () => {
const leagueId = 'league-123';
const mockDto = { races: [] };
mockApiClient.getSchedule.mockResolvedValue(mockDto);
const result = await service.getLeagueSchedule(leagueId);
expect(result.races).toEqual([]);
expect(result.hasRaces).toBe(false);
});
it('should throw error when apiClient.getSchedule fails', async () => {
const leagueId = 'league-123';
const error = new Error('API call failed');
mockApiClient.getSchedule.mockRejectedValue(error);
await expect(service.getLeagueSchedule(leagueId)).rejects.toThrow('API call failed');
});
});
describe('getLeagueMemberships', () => {
it('should call apiClient.getMemberships and return LeagueMembershipsViewModel', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
memberships: [
{ driverId: 'driver-1' },
{ driverId: 'driver-2' },
] as LeagueMemberDTO[],
};
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueMembershipsViewModel);
expect(result.memberships).toHaveLength(2);
expect(result.memberships[0]).toBeInstanceOf(LeagueMemberViewModel);
expect(result.memberships[0].driverId).toBe('driver-1');
});
it('should handle empty memberships array', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
memberships: [] as LeagueMemberDTO[],
};
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
expect(result.memberships).toHaveLength(0);
expect(result.hasMembers).toBe(false);
});
it('should throw error when apiClient.getMemberships fails', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const error = new Error('API call failed');
mockApiClient.getMemberships.mockRejectedValue(error);
await expect(service.getLeagueMemberships(leagueId, currentUserId)).rejects.toThrow('API call failed');
});
});
describe('createLeague', () => {
it('should call apiClient.create and return CreateLeagueViewModel', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
};
const mockDto: CreateLeagueOutputDTO = {
leagueId: 'new-league-id',
success: true,
};
mockApiClient.create.mockResolvedValue(mockDto);
const result = await service.createLeague(input);
expect(mockApiClient.create).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(CreateLeagueViewModel);
expect(result.leagueId).toBe('new-league-id');
expect(result.success).toBe(true);
});
it('should handle unsuccessful creation', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
};
const mockDto: CreateLeagueOutputDTO = {
leagueId: '',
success: false,
};
mockApiClient.create.mockResolvedValue(mockDto);
const result = await service.createLeague(input);
expect(result.success).toBe(false);
expect(result.successMessage).toBe('Failed to create league.');
});
it('should throw error when apiClient.create fails', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
};
const error = new Error('API call failed');
mockApiClient.create.mockRejectedValue(error);
await expect(service.createLeague(input)).rejects.toThrow('API call failed');
});
});
describe('removeMember', () => {
it('should call apiClient.removeMember and return RemoveMemberViewModel', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const mockDto: RemoveLeagueMemberOutputDTO = { success: true };
mockApiClient.removeMember.mockResolvedValue(mockDto);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
expect(result).toBeInstanceOf(RemoveMemberViewModel);
expect(result.success).toBe(true);
});
it('should handle unsuccessful removal', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const mockDto: RemoveLeagueMemberOutputDTO = { success: false };
mockApiClient.removeMember.mockResolvedValue(mockDto);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
expect(result.success).toBe(false);
expect(result.successMessage).toBe('Failed to remove member.');
});
it('should throw error when apiClient.removeMember fails', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const error = new Error('API call failed');
mockApiClient.removeMember.mockRejectedValue(error);
await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,11 +1,14 @@
import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { LeagueSummaryViewModel, LeagueStandingsViewModel } from '../../view-models';
import type { CreateLeagueInputDTO, CreateLeagueOutputDTO, LeagueWithCapacityDTO } from '../../types/generated';
import { LeaguesApiClient } from "@/lib/api/leagues/LeaguesApiClient";
import { CreateLeagueInputDTO } from "@/lib/types/generated/CreateLeagueInputDTO";
import { LeagueWithCapacityDTO } from "@/lib/types/generated/LeagueWithCapacityDTO";
import { CreateLeagueViewModel } from "@/lib/view-models/CreateLeagueViewModel";
import { LeagueMembershipsViewModel } from "@/lib/view-models/LeagueMembershipsViewModel";
import { LeagueScheduleViewModel } from "@/lib/view-models/LeagueScheduleViewModel";
import { LeagueStandingsViewModel } from "@/lib/view-models/LeagueStandingsViewModel";
import { LeagueStatsViewModel } from "@/lib/view-models/LeagueStatsViewModel";
import { LeagueSummaryViewModel } from "@/lib/view-models/LeagueSummaryViewModel";
import { RemoveMemberViewModel } from "@/lib/view-models/RemoveMemberViewModel";
// TODO: Move these types to apps/website/lib/types/generated when available
type LeagueStatsDto = { totalLeagues: number };
type LeagueScheduleDto = { races: Array<unknown> };
type LeagueMembershipsDto = { memberships: Array<unknown> };
/**
* League Service
@@ -43,35 +46,40 @@ export class LeagueService {
/**
* Get league statistics
*/
async getLeagueStats(): Promise<LeagueStatsDto> {
return await this.apiClient.getTotal();
async getLeagueStats(): Promise<LeagueStatsViewModel> {
const dto = await this.apiClient.getTotal();
return new LeagueStatsViewModel(dto);
}
/**
* Get league schedule
*/
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleDto> {
return await this.apiClient.getSchedule(leagueId);
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleViewModel> {
const dto = await this.apiClient.getSchedule(leagueId);
return new LeagueScheduleViewModel(dto);
}
/**
* Get league memberships
*/
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDto> {
return await this.apiClient.getMemberships(leagueId);
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMembershipsViewModel> {
const dto = await this.apiClient.getMemberships(leagueId);
return new LeagueMembershipsViewModel(dto, currentUserId);
}
/**
* Create a new league
*/
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueOutputDTO> {
return await this.apiClient.create(input);
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueViewModel> {
const dto = await this.apiClient.create(input);
return new CreateLeagueViewModel(dto);
}
/**
* Remove a member from league
*/
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> {
return await this.apiClient.removeMember(leagueId, performerDriverId, targetDriverId);
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<RemoveMemberViewModel> {
const dto = await this.apiClient.removeMember(leagueId, performerDriverId, targetDriverId);
return new RemoveMemberViewModel(dto);
}
}