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,80 @@
import { describe, it, expect, vi } from 'vitest';
import { TeamJoinService } from './TeamJoinService';
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
describe('TeamJoinService', () => {
let service: TeamJoinService;
let mockApiClient: TeamsApiClient;
beforeEach(() => {
mockApiClient = {
getJoinRequests: vi.fn(),
} as any;
service = new TeamJoinService(mockApiClient);
});
describe('getJoinRequests', () => {
it('should return view models for join requests', async () => {
const mockDto = {
requests: [
{
id: 'request-1',
teamId: 'team-1',
driverId: 'driver-1',
requestedAt: '2023-01-01T00:00:00Z',
message: 'Please accept me',
},
{
id: 'request-2',
teamId: 'team-1',
driverId: 'driver-2',
requestedAt: '2023-01-02T00:00:00Z',
},
],
};
mockApiClient.getJoinRequests = vi.fn().mockResolvedValue(mockDto);
const result = await service.getJoinRequests('team-1', 'user-1', true);
expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1');
expect(result).toHaveLength(2);
expect(result[0].id).toBe('request-1');
expect(result[0].canApprove).toBe(true);
expect(result[1].id).toBe('request-2');
expect(result[1].canApprove).toBe(true);
});
it('should pass correct parameters to view model constructor', async () => {
const mockDto = {
requests: [
{
id: 'request-1',
teamId: 'team-1',
driverId: 'driver-1',
requestedAt: '2023-01-01T00:00:00Z',
},
],
};
mockApiClient.getJoinRequests = vi.fn().mockResolvedValue(mockDto);
const result = await service.getJoinRequests('team-1', 'user-1', false);
expect(result[0].canApprove).toBe(false);
});
});
describe('approveJoinRequest', () => {
it('should throw not implemented error', async () => {
await expect(service.approveJoinRequest()).rejects.toThrow('Not implemented: API endpoint for approving join requests');
});
});
describe('rejectJoinRequest', () => {
it('should throw not implemented error', async () => {
await expect(service.rejectJoinRequest()).rejects.toThrow('Not implemented: API endpoint for rejecting join requests');
});
});
});

View File

@@ -1,12 +1,9 @@
import { TeamJoinRequestViewModel, type TeamJoinRequestDTO } from '@/lib/view-models/TeamJoinRequestViewModel';
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
import { TeamJoinRequestViewModel } from '../../view-models';
type TeamJoinRequestDTO = {
id: string;
teamId: string;
driverId: string;
requestedAt: string;
message?: string;
// TODO: Create generated DTO when API spec is available
type TeamJoinRequestsDto = {
requests: TeamJoinRequestDTO[];
};
/**
@@ -24,14 +21,14 @@ export class TeamJoinService {
* Get team join requests with view model transformation
*/
async getJoinRequests(teamId: string, currentUserId: string, isOwner: boolean): Promise<TeamJoinRequestViewModel[]> {
const dto = await this.apiClient.getJoinRequests(teamId);
const dto = await this.apiClient.getJoinRequests(teamId) as TeamJoinRequestsDto;
return dto.requests.map((r: TeamJoinRequestDTO) => new TeamJoinRequestViewModel(r, currentUserId, isOwner));
}
/**
* Approve a team join request
*/
async approveJoinRequest(teamId: string, requestId: string): Promise<void> {
async approveJoinRequest(): Promise<void> {
// TODO: implement API call when endpoint is available
throw new Error('Not implemented: API endpoint for approving join requests');
}
@@ -39,7 +36,7 @@ export class TeamJoinService {
/**
* Reject a team join request
*/
async rejectJoinRequest(teamId: string, requestId: string): Promise<void> {
async rejectJoinRequest(): Promise<void> {
// TODO: implement API call when endpoint is available
throw new Error('Not implemented: API endpoint for rejecting join requests');
}

View File

@@ -0,0 +1,205 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { TeamService } from './TeamService';
import { TeamsApiClient } from '../../api/teams/TeamsApiClient';
import { TeamSummaryViewModel } from '../../view-models/TeamSummaryViewModel';
import { TeamDetailsViewModel } from '../../view-models/TeamDetailsViewModel';
import { TeamMemberViewModel } from '../../view-models/TeamMemberViewModel';
describe('TeamService', () => {
let mockApiClient: Mocked<TeamsApiClient>;
let service: TeamService;
beforeEach(() => {
mockApiClient = {
getAll: vi.fn(),
getDetails: vi.fn(),
getMembers: vi.fn(),
create: vi.fn(),
update: vi.fn(),
getDriverTeam: vi.fn(),
} as Mocked<TeamsApiClient>;
service = new TeamService(mockApiClient);
});
describe('getAllTeams', () => {
it('should call apiClient.getAll and return array of TeamSummaryViewModel', async () => {
const mockDto = {
teams: [
{
id: 'team-1',
name: 'Test Team',
logoUrl: 'https://example.com/logo.png',
memberCount: 5,
rating: 1500,
},
],
};
mockApiClient.getAll.mockResolvedValue(mockDto);
const result = await service.getAllTeams();
expect(mockApiClient.getAll).toHaveBeenCalled();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(TeamSummaryViewModel);
expect(result[0].id).toBe('team-1');
expect(result[0].name).toBe('Test Team');
});
it('should throw error when apiClient.getAll fails', async () => {
const error = new Error('API call failed');
mockApiClient.getAll.mockRejectedValue(error);
await expect(service.getAllTeams()).rejects.toThrow('API call failed');
});
});
describe('getTeamDetails', () => {
it('should call apiClient.getDetails and return TeamDetailsViewModel when data exists', async () => {
const mockDto = {
id: 'team-1',
name: 'Test Team',
description: 'A test team',
logoUrl: 'https://example.com/logo.png',
memberCount: 5,
ownerId: 'owner-1',
members: [],
};
mockApiClient.getDetails.mockResolvedValue(mockDto);
const result = await service.getTeamDetails('team-1', 'user-1');
expect(mockApiClient.getDetails).toHaveBeenCalledWith('team-1');
expect(result).toBeInstanceOf(TeamDetailsViewModel);
expect(result?.id).toBe('team-1');
expect(result?.name).toBe('Test Team');
});
it('should return null when apiClient.getDetails returns null', async () => {
mockApiClient.getDetails.mockResolvedValue(null);
const result = await service.getTeamDetails('team-1', 'user-1');
expect(mockApiClient.getDetails).toHaveBeenCalledWith('team-1');
expect(result).toBeNull();
});
it('should throw error when apiClient.getDetails fails', async () => {
const error = new Error('API call failed');
mockApiClient.getDetails.mockRejectedValue(error);
await expect(service.getTeamDetails('team-1', 'user-1')).rejects.toThrow('API call failed');
});
});
describe('getTeamMembers', () => {
it('should call apiClient.getMembers and return array of TeamMemberViewModel', async () => {
const mockDto = {
members: [
{
driverId: 'driver-1',
driver: { id: 'driver-1', name: 'Driver One', avatarUrl: 'avatar.png', iracingId: '123', rating: 1400 },
role: 'member',
joinedAt: '2023-01-01T00:00:00Z',
},
],
};
mockApiClient.getMembers.mockResolvedValue(mockDto);
const result = await service.getTeamMembers('team-1', 'user-1', 'owner-1');
expect(mockApiClient.getMembers).toHaveBeenCalledWith('team-1');
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(TeamMemberViewModel);
expect(result[0].driverId).toBe('driver-1');
expect(result[0].role).toBe('member');
});
it('should throw error when apiClient.getMembers fails', async () => {
const error = new Error('API call failed');
mockApiClient.getMembers.mockRejectedValue(error);
await expect(service.getTeamMembers('team-1', 'user-1', 'owner-1')).rejects.toThrow('API call failed');
});
});
describe('createTeam', () => {
it('should call apiClient.create and return the result', async () => {
const input = { name: 'New Team', tag: 'NT', description: 'A new team' };
const mockOutput = { id: 'team-123', success: true };
mockApiClient.create.mockResolvedValue(mockOutput);
const result = await service.createTeam(input);
expect(mockApiClient.create).toHaveBeenCalledWith(input);
expect(result).toEqual(mockOutput);
});
it('should throw error when apiClient.create fails', async () => {
const input = { name: 'New Team', tag: 'NT' };
const error = new Error('API call failed');
mockApiClient.create.mockRejectedValue(error);
await expect(service.createTeam(input)).rejects.toThrow('API call failed');
});
});
describe('updateTeam', () => {
it('should call apiClient.update and return the result', async () => {
const input = { name: 'Updated Team', description: 'Updated description' };
const mockOutput = { success: true };
mockApiClient.update.mockResolvedValue(mockOutput);
const result = await service.updateTeam('team-1', input);
expect(mockApiClient.update).toHaveBeenCalledWith('team-1', input);
expect(result).toEqual(mockOutput);
});
it('should throw error when apiClient.update fails', async () => {
const input = { name: 'Updated Team' };
const error = new Error('API call failed');
mockApiClient.update.mockRejectedValue(error);
await expect(service.updateTeam('team-1', input)).rejects.toThrow('API call failed');
});
});
describe('getDriverTeam', () => {
it('should call apiClient.getDriverTeam and return the result', async () => {
const mockOutput = { teamId: 'team-1', teamName: 'Test Team', role: 'member' };
mockApiClient.getDriverTeam.mockResolvedValue(mockOutput);
const result = await service.getDriverTeam('driver-1');
expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith('driver-1');
expect(result).toEqual(mockOutput);
});
it('should return null when apiClient.getDriverTeam returns null', async () => {
mockApiClient.getDriverTeam.mockResolvedValue(null);
const result = await service.getDriverTeam('driver-1');
expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith('driver-1');
expect(result).toBeNull();
});
it('should throw error when apiClient.getDriverTeam fails', async () => {
const error = new Error('API call failed');
mockApiClient.getDriverTeam.mockRejectedValue(error);
await expect(service.getDriverTeam('driver-1')).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,14 +1,20 @@
import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { CreateTeamViewModel } from '@/lib/view-models/CreateTeamViewModel';
import { UpdateTeamViewModel } from '@/lib/view-models/UpdateTeamViewModel';
import { DriverTeamViewModel } from '@/lib/view-models/DriverTeamViewModel';
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
import { TeamSummaryViewModel, TeamDetailsViewModel, TeamMemberViewModel } from '../../view-models';
// TODO: Move these types to apps/website/lib/types/generated when available
type DriverDTO = { id: string; name: string; avatarUrl?: string; iracingId?: string; rating?: number };
type CreateTeamInputDto = { name: string; tag: string; description?: string };
type CreateTeamOutputDto = { id: string; success: boolean };
type UpdateTeamInputDto = { name?: string; tag?: string; description?: string };
type UpdateTeamOutputDto = { success: boolean };
type DriverTeamDto = { teamId: string; teamName: string; role: string };
type TeamSummaryDTO = { id: string; name: string; logoUrl?: string; memberCount: number; rating: number };
type TeamMemberDTO = { driverId: string; driver?: any; role: string; joinedAt: string };
type TeamMemberDTO = { driverId: string; driver?: DriverDTO; role: string; joinedAt: string };
/**
* Team Service
@@ -49,23 +55,26 @@ export class TeamService {
}
/**
* Create a new team
* Create a new team with view model transformation
*/
async createTeam(input: CreateTeamInputDto): Promise<CreateTeamOutputDto> {
return await this.apiClient.create(input);
async createTeam(input: CreateTeamInputDto): Promise<CreateTeamViewModel> {
const dto = await this.apiClient.create(input);
return new CreateTeamViewModel(dto);
}
/**
* Update team
* Update team with view model transformation
*/
async updateTeam(teamId: string, input: UpdateTeamInputDto): Promise<UpdateTeamOutputDto> {
return await this.apiClient.update(teamId, input);
async updateTeam(teamId: string, input: UpdateTeamInputDto): Promise<UpdateTeamViewModel> {
const dto = await this.apiClient.update(teamId, input);
return new UpdateTeamViewModel(dto);
}
/**
* Get driver's team
* Get driver's team with view model transformation
*/
async getDriverTeam(driverId: string): Promise<DriverTeamDto | null> {
return await this.apiClient.getDriverTeam(driverId);
async getDriverTeam(driverId: string): Promise<DriverTeamViewModel | null> {
const dto = await this.apiClient.getDriverTeam(driverId);
return dto ? new DriverTeamViewModel(dto) : null;
}
}