view models
This commit is contained in:
@@ -1,254 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { TeamJoinService } from './TeamJoinService';
|
||||
import { TeamsApiClient } from '../../api/teams/TeamsApiClient';
|
||||
import { TeamJoinRequestPresenter } from '../../presenters/TeamJoinRequestPresenter';
|
||||
import type { TeamJoinRequestsDto, TeamJoinRequestItemDto } from '../../dtos';
|
||||
import type { TeamJoinRequestViewModel } from '../../view-models';
|
||||
|
||||
describe('TeamJoinService', () => {
|
||||
let mockApiClient: TeamsApiClient;
|
||||
let mockPresenter: TeamJoinRequestPresenter;
|
||||
let service: TeamJoinService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getJoinRequests: vi.fn(),
|
||||
} as unknown as TeamsApiClient;
|
||||
|
||||
mockPresenter = {
|
||||
present: vi.fn(),
|
||||
} as unknown as TeamJoinRequestPresenter;
|
||||
|
||||
service = new TeamJoinService(mockApiClient, mockPresenter);
|
||||
});
|
||||
|
||||
describe('getJoinRequests', () => {
|
||||
it('should fetch join requests from API and transform via presenter', async () => {
|
||||
// Arrange
|
||||
const mockRequestDto: TeamJoinRequestItemDto = {
|
||||
id: 'request-1',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: '2025-12-17T20:00:00Z',
|
||||
message: 'Please let me join',
|
||||
};
|
||||
|
||||
const mockDto: TeamJoinRequestsDto = {
|
||||
requests: [mockRequestDto],
|
||||
};
|
||||
|
||||
const mockViewModel: TeamJoinRequestViewModel = {
|
||||
id: 'request-1',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: '2025-12-17T20:00:00Z',
|
||||
message: 'Please let me join',
|
||||
canApprove: true,
|
||||
formattedRequestedAt: '12/17/2025, 9:00:00 PM',
|
||||
status: 'Pending',
|
||||
statusColor: 'yellow',
|
||||
approveButtonText: 'Approve',
|
||||
rejectButtonText: 'Reject',
|
||||
} as unknown as TeamJoinRequestViewModel;
|
||||
|
||||
vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto);
|
||||
vi.mocked(mockPresenter.present).mockReturnValue(mockViewModel);
|
||||
|
||||
// Act
|
||||
const result = await service.getJoinRequests('team-1', 'driver-owner', true);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1');
|
||||
expect(mockApiClient.getJoinRequests).toHaveBeenCalledTimes(1);
|
||||
expect(mockPresenter.present).toHaveBeenCalledWith(mockRequestDto, 'driver-owner', true);
|
||||
expect(mockPresenter.present).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual([mockViewModel]);
|
||||
});
|
||||
|
||||
it('should handle multiple join requests', async () => {
|
||||
// Arrange
|
||||
const mockRequestDto1: TeamJoinRequestItemDto = {
|
||||
id: 'request-1',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: '2025-12-17T20:00:00Z',
|
||||
};
|
||||
|
||||
const mockRequestDto2: TeamJoinRequestItemDto = {
|
||||
id: 'request-2',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-2',
|
||||
requestedAt: '2025-12-17T21:00:00Z',
|
||||
message: 'I want to join',
|
||||
};
|
||||
|
||||
const mockDto: TeamJoinRequestsDto = {
|
||||
requests: [mockRequestDto1, mockRequestDto2],
|
||||
};
|
||||
|
||||
const mockViewModel1 = { id: 'request-1' } as TeamJoinRequestViewModel;
|
||||
const mockViewModel2 = { id: 'request-2' } as TeamJoinRequestViewModel;
|
||||
|
||||
vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto);
|
||||
vi.mocked(mockPresenter.present)
|
||||
.mockReturnValueOnce(mockViewModel1)
|
||||
.mockReturnValueOnce(mockViewModel2);
|
||||
|
||||
// Act
|
||||
const result = await service.getJoinRequests('team-1', 'driver-owner', true);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1');
|
||||
expect(mockPresenter.present).toHaveBeenCalledTimes(2);
|
||||
expect(mockPresenter.present).toHaveBeenNthCalledWith(1, mockRequestDto1, 'driver-owner', true);
|
||||
expect(mockPresenter.present).toHaveBeenNthCalledWith(2, mockRequestDto2, 'driver-owner', true);
|
||||
expect(result).toEqual([mockViewModel1, mockViewModel2]);
|
||||
});
|
||||
|
||||
it('should handle empty join requests list', async () => {
|
||||
// Arrange
|
||||
const mockDto: TeamJoinRequestsDto = {
|
||||
requests: [],
|
||||
};
|
||||
|
||||
vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto);
|
||||
|
||||
// Act
|
||||
const result = await service.getJoinRequests('team-1', 'driver-owner', true);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1');
|
||||
expect(mockPresenter.present).not.toHaveBeenCalled();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should propagate API client errors', async () => {
|
||||
// Arrange
|
||||
const error = new Error('API Error: Team not found');
|
||||
vi.mocked(mockApiClient.getJoinRequests).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.getJoinRequests('invalid-team', 'driver-1', false)
|
||||
).rejects.toThrow('API Error: Team not found');
|
||||
|
||||
expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('invalid-team');
|
||||
expect(mockPresenter.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should propagate presenter errors', async () => {
|
||||
// Arrange
|
||||
const mockRequestDto: TeamJoinRequestItemDto = {
|
||||
id: 'request-1',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: '2025-12-17T20:00:00Z',
|
||||
};
|
||||
|
||||
const mockDto: TeamJoinRequestsDto = {
|
||||
requests: [mockRequestDto],
|
||||
};
|
||||
|
||||
const error = new Error('Presenter Error: Invalid DTO structure');
|
||||
vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto);
|
||||
vi.mocked(mockPresenter.present).mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.getJoinRequests('team-1', 'driver-1', false)
|
||||
).rejects.toThrow('Presenter Error: Invalid DTO structure');
|
||||
|
||||
expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1');
|
||||
expect(mockPresenter.present).toHaveBeenCalledWith(mockRequestDto, 'driver-1', false);
|
||||
});
|
||||
|
||||
it('should pass correct isOwner flag to presenter', async () => {
|
||||
// Arrange
|
||||
const mockRequestDto: TeamJoinRequestItemDto = {
|
||||
id: 'request-1',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: '2025-12-17T20:00:00Z',
|
||||
};
|
||||
|
||||
const mockDto: TeamJoinRequestsDto = {
|
||||
requests: [mockRequestDto],
|
||||
};
|
||||
|
||||
const mockViewModel = { id: 'request-1' } as TeamJoinRequestViewModel;
|
||||
|
||||
vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto);
|
||||
vi.mocked(mockPresenter.present).mockReturnValue(mockViewModel);
|
||||
|
||||
// Act - non-owner
|
||||
await service.getJoinRequests('team-1', 'driver-member', false);
|
||||
|
||||
// Assert
|
||||
expect(mockPresenter.present).toHaveBeenCalledWith(mockRequestDto, 'driver-member', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('approveJoinRequest', () => {
|
||||
it('should throw not implemented error', async () => {
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.approveJoinRequest('team-1', 'request-1')
|
||||
).rejects.toThrow('Not implemented: API endpoint for approving join requests');
|
||||
});
|
||||
|
||||
it('should propagate errors when API is implemented', async () => {
|
||||
// This test ensures error handling is in place for future implementation
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.approveJoinRequest('team-1', 'request-1')
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejectJoinRequest', () => {
|
||||
it('should throw not implemented error', async () => {
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.rejectJoinRequest('team-1', 'request-1')
|
||||
).rejects.toThrow('Not implemented: API endpoint for rejecting join requests');
|
||||
});
|
||||
|
||||
it('should propagate errors when API is implemented', async () => {
|
||||
// This test ensures error handling is in place for future implementation
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.rejectJoinRequest('team-1', 'request-1')
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Constructor Dependency Injection', () => {
|
||||
it('should require apiClient and teamJoinRequestPresenter', () => {
|
||||
// This test verifies the constructor signature
|
||||
expect(() => {
|
||||
new TeamJoinService(mockApiClient, mockPresenter);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should use injected dependencies', async () => {
|
||||
// Arrange
|
||||
const customApiClient = {
|
||||
getJoinRequests: vi.fn().mockResolvedValue({ requests: [] }),
|
||||
} as unknown as TeamsApiClient;
|
||||
|
||||
const customPresenter = {
|
||||
present: vi.fn().mockReturnValue({} as TeamJoinRequestViewModel),
|
||||
} as unknown as TeamJoinRequestPresenter;
|
||||
|
||||
const customService = new TeamJoinService(customApiClient, customPresenter);
|
||||
|
||||
// Act
|
||||
await customService.getJoinRequests('team-1', 'driver-1', true);
|
||||
|
||||
// Assert
|
||||
expect(customApiClient.getJoinRequests).toHaveBeenCalledWith('team-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,52 +1,46 @@
|
||||
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
|
||||
import type { TeamJoinRequestPresenter } from '../../presenters/TeamJoinRequestPresenter';
|
||||
import type { TeamJoinRequestViewModel } from '../../view-models';
|
||||
import { TeamJoinRequestViewModel } from '../../view-models';
|
||||
|
||||
type TeamJoinRequestDTO = {
|
||||
id: string;
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
requestedAt: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Team Join Service
|
||||
*
|
||||
* Orchestrates team join/leave operations by coordinating API calls and presentation logic.
|
||||
* Orchestrates team join/leave operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class TeamJoinService {
|
||||
constructor(
|
||||
private readonly apiClient: TeamsApiClient,
|
||||
private readonly teamJoinRequestPresenter: TeamJoinRequestPresenter
|
||||
private readonly apiClient: TeamsApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get team join requests with presentation transformation
|
||||
* Get team join requests with view model transformation
|
||||
*/
|
||||
async getJoinRequests(teamId: string, currentUserId: string, isOwner: boolean): Promise<TeamJoinRequestViewModel[]> {
|
||||
try {
|
||||
const dto = await this.apiClient.getJoinRequests(teamId);
|
||||
return dto.requests.map(r => this.teamJoinRequestPresenter.present(r, currentUserId, isOwner));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const dto = await this.apiClient.getJoinRequests(teamId);
|
||||
return dto.requests.map((r: TeamJoinRequestDTO) => new TeamJoinRequestViewModel(r, currentUserId, isOwner));
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a team join request
|
||||
*/
|
||||
async approveJoinRequest(teamId: string, requestId: string): Promise<void> {
|
||||
try {
|
||||
// TODO: implement API call when endpoint is available
|
||||
throw new Error('Not implemented: API endpoint for approving join requests');
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
// TODO: implement API call when endpoint is available
|
||||
throw new Error('Not implemented: API endpoint for approving join requests');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a team join request
|
||||
*/
|
||||
async rejectJoinRequest(teamId: string, requestId: string): Promise<void> {
|
||||
try {
|
||||
// TODO: implement API call when endpoint is available
|
||||
throw new Error('Not implemented: API endpoint for rejecting join requests');
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
// TODO: implement API call when endpoint is available
|
||||
throw new Error('Not implemented: API endpoint for rejecting join requests');
|
||||
}
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { TeamService } from './TeamService';
|
||||
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
|
||||
import type { TeamDetailsPresenter } from '../../presenters/TeamDetailsPresenter';
|
||||
import type { TeamListPresenter } from '../../presenters/TeamListPresenter';
|
||||
import type { TeamMembersPresenter } from '../../presenters/TeamMembersPresenter';
|
||||
import type {
|
||||
AllTeamsDto,
|
||||
TeamDetailsDto,
|
||||
TeamMembersDto,
|
||||
CreateTeamInputDto,
|
||||
CreateTeamOutputDto,
|
||||
UpdateTeamInputDto,
|
||||
UpdateTeamOutputDto,
|
||||
DriverTeamDto,
|
||||
} from '../../dtos';
|
||||
import type { TeamSummaryViewModel, TeamDetailsViewModel, TeamMemberViewModel } from '../../view-models';
|
||||
|
||||
describe('TeamService', () => {
|
||||
let service: TeamService;
|
||||
let mockApiClient: TeamsApiClient;
|
||||
let mockTeamListPresenter: TeamListPresenter;
|
||||
let mockTeamDetailsPresenter: TeamDetailsPresenter;
|
||||
let mockTeamMembersPresenter: TeamMembersPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getAll: vi.fn(),
|
||||
getDetails: vi.fn(),
|
||||
getMembers: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
getDriverTeam: vi.fn(),
|
||||
} as unknown as TeamsApiClient;
|
||||
|
||||
mockTeamListPresenter = {
|
||||
present: vi.fn(),
|
||||
} as unknown as TeamListPresenter;
|
||||
|
||||
mockTeamDetailsPresenter = {
|
||||
present: vi.fn(),
|
||||
} as unknown as TeamDetailsPresenter;
|
||||
|
||||
mockTeamMembersPresenter = {
|
||||
present: vi.fn(),
|
||||
} as unknown as TeamMembersPresenter;
|
||||
|
||||
service = new TeamService(
|
||||
mockApiClient,
|
||||
mockTeamListPresenter,
|
||||
mockTeamDetailsPresenter,
|
||||
mockTeamMembersPresenter
|
||||
);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create instance with injected dependencies', () => {
|
||||
expect(service).toBeInstanceOf(TeamService);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllTeams', () => {
|
||||
it('should fetch all teams from API and transform via presenter', async () => {
|
||||
// Arrange
|
||||
const mockDto: AllTeamsDto = {
|
||||
teams: [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Team Alpha',
|
||||
logoUrl: 'https://example.com/logo1.png',
|
||||
memberCount: 5,
|
||||
rating: 2500,
|
||||
},
|
||||
{
|
||||
id: 'team-2',
|
||||
name: 'Team Beta',
|
||||
logoUrl: 'https://example.com/logo2.png',
|
||||
memberCount: 3,
|
||||
rating: 2300,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockViewModels: TeamSummaryViewModel[] = [
|
||||
{
|
||||
id: 'team-1',
|
||||
name: 'Team Alpha',
|
||||
logoUrl: 'https://example.com/logo1.png',
|
||||
memberCount: 5,
|
||||
rating: 2500,
|
||||
} as TeamSummaryViewModel,
|
||||
{
|
||||
id: 'team-2',
|
||||
name: 'Team Beta',
|
||||
logoUrl: 'https://example.com/logo2.png',
|
||||
memberCount: 3,
|
||||
rating: 2300,
|
||||
} as TeamSummaryViewModel,
|
||||
];
|
||||
|
||||
vi.mocked(mockApiClient.getAll).mockResolvedValue(mockDto);
|
||||
vi.mocked(mockTeamListPresenter.present).mockReturnValue(mockViewModels);
|
||||
|
||||
// Act
|
||||
const result = await service.getAllTeams();
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getAll).toHaveBeenCalled();
|
||||
expect(mockTeamListPresenter.present).toHaveBeenCalledWith(mockDto);
|
||||
expect(result).toEqual(mockViewModels);
|
||||
});
|
||||
|
||||
it('should handle empty teams list', async () => {
|
||||
// Arrange
|
||||
const mockDto: AllTeamsDto = {
|
||||
teams: [],
|
||||
};
|
||||
|
||||
const mockViewModels: TeamSummaryViewModel[] = [];
|
||||
|
||||
vi.mocked(mockApiClient.getAll).mockResolvedValue(mockDto);
|
||||
vi.mocked(mockTeamListPresenter.present).mockReturnValue(mockViewModels);
|
||||
|
||||
// Act
|
||||
const result = await service.getAllTeams();
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getAll).toHaveBeenCalled();
|
||||
expect(mockTeamListPresenter.present).toHaveBeenCalledWith(mockDto);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should propagate errors from API client', async () => {
|
||||
// Arrange
|
||||
const error = new Error('Failed to fetch teams');
|
||||
vi.mocked(mockApiClient.getAll).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.getAllTeams()).rejects.toThrow('Failed to fetch teams');
|
||||
expect(mockApiClient.getAll).toHaveBeenCalled();
|
||||
expect(mockTeamListPresenter.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamDetails', () => {
|
||||
it('should fetch team details and transform via presenter', async () => {
|
||||
// Arrange
|
||||
const teamId = 'team-123';
|
||||
const currentUserId = 'user-456';
|
||||
|
||||
const mockDto: TeamDetailsDto = {
|
||||
id: teamId,
|
||||
name: 'Team Alpha',
|
||||
description: 'A competitive racing team',
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
memberCount: 5,
|
||||
ownerId: 'user-789',
|
||||
};
|
||||
|
||||
const mockViewModel: TeamDetailsViewModel = {
|
||||
id: teamId,
|
||||
name: 'Team Alpha',
|
||||
description: 'A competitive racing team',
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
memberCount: 5,
|
||||
ownerId: 'user-789',
|
||||
members: [],
|
||||
} as TeamDetailsViewModel;
|
||||
|
||||
vi.mocked(mockApiClient.getDetails).mockResolvedValue(mockDto);
|
||||
vi.mocked(mockTeamDetailsPresenter.present).mockReturnValue(mockViewModel);
|
||||
|
||||
// Act
|
||||
const result = await service.getTeamDetails(teamId, currentUserId);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getDetails).toHaveBeenCalledWith(teamId);
|
||||
expect(mockTeamDetailsPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId);
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
|
||||
it('should return null when team is not found', async () => {
|
||||
// Arrange
|
||||
const teamId = 'non-existent';
|
||||
const currentUserId = 'user-456';
|
||||
|
||||
vi.mocked(mockApiClient.getDetails).mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const result = await service.getTeamDetails(teamId, currentUserId);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getDetails).toHaveBeenCalledWith(teamId);
|
||||
expect(mockTeamDetailsPresenter.present).not.toHaveBeenCalled();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should propagate errors from API client', async () => {
|
||||
// Arrange
|
||||
const teamId = 'team-123';
|
||||
const currentUserId = 'user-456';
|
||||
const error = new Error('Failed to fetch team details');
|
||||
vi.mocked(mockApiClient.getDetails).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.getTeamDetails(teamId, currentUserId)).rejects.toThrow('Failed to fetch team details');
|
||||
expect(mockApiClient.getDetails).toHaveBeenCalledWith(teamId);
|
||||
expect(mockTeamDetailsPresenter.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamMembers', () => {
|
||||
it('should fetch team members and transform via presenter', async () => {
|
||||
// Arrange
|
||||
const teamId = 'team-123';
|
||||
const currentUserId = 'user-456';
|
||||
const teamOwnerId = 'user-789';
|
||||
|
||||
const mockDto: TeamMembersDto = {
|
||||
members: [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
role: 'owner',
|
||||
joinedAt: '2024-01-01',
|
||||
},
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
role: 'member',
|
||||
joinedAt: '2024-01-02',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockViewModels: TeamMemberViewModel[] = [
|
||||
{
|
||||
driverId: 'driver-1',
|
||||
role: 'owner',
|
||||
joinedAt: '2024-01-01',
|
||||
} as TeamMemberViewModel,
|
||||
{
|
||||
driverId: 'driver-2',
|
||||
role: 'member',
|
||||
joinedAt: '2024-01-02',
|
||||
} as TeamMemberViewModel,
|
||||
];
|
||||
|
||||
vi.mocked(mockApiClient.getMembers).mockResolvedValue(mockDto);
|
||||
vi.mocked(mockTeamMembersPresenter.present).mockReturnValue(mockViewModels);
|
||||
|
||||
// Act
|
||||
const result = await service.getTeamMembers(teamId, currentUserId, teamOwnerId);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getMembers).toHaveBeenCalledWith(teamId);
|
||||
expect(mockTeamMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId, teamOwnerId);
|
||||
expect(result).toEqual(mockViewModels);
|
||||
});
|
||||
|
||||
it('should handle empty members list', async () => {
|
||||
// Arrange
|
||||
const teamId = 'team-123';
|
||||
const currentUserId = 'user-456';
|
||||
const teamOwnerId = 'user-789';
|
||||
|
||||
const mockDto: TeamMembersDto = {
|
||||
members: [],
|
||||
};
|
||||
|
||||
const mockViewModels: TeamMemberViewModel[] = [];
|
||||
|
||||
vi.mocked(mockApiClient.getMembers).mockResolvedValue(mockDto);
|
||||
vi.mocked(mockTeamMembersPresenter.present).mockReturnValue(mockViewModels);
|
||||
|
||||
// Act
|
||||
const result = await service.getTeamMembers(teamId, currentUserId, teamOwnerId);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getMembers).toHaveBeenCalledWith(teamId);
|
||||
expect(mockTeamMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId, teamOwnerId);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should propagate errors from API client', async () => {
|
||||
// Arrange
|
||||
const teamId = 'team-123';
|
||||
const currentUserId = 'user-456';
|
||||
const teamOwnerId = 'user-789';
|
||||
const error = new Error('Failed to fetch team members');
|
||||
vi.mocked(mockApiClient.getMembers).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.getTeamMembers(teamId, currentUserId, teamOwnerId)).rejects.toThrow('Failed to fetch team members');
|
||||
expect(mockApiClient.getMembers).toHaveBeenCalledWith(teamId);
|
||||
expect(mockTeamMembersPresenter.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTeam', () => {
|
||||
it('should create a new team', async () => {
|
||||
// Arrange
|
||||
const input: CreateTeamInputDto = {
|
||||
name: 'New Team',
|
||||
description: 'A new racing team',
|
||||
};
|
||||
|
||||
const mockOutput: CreateTeamOutputDto = {
|
||||
id: 'team-new',
|
||||
name: 'New Team',
|
||||
success: true,
|
||||
};
|
||||
|
||||
vi.mocked(mockApiClient.create).mockResolvedValue(mockOutput);
|
||||
|
||||
// Act
|
||||
const result = await service.createTeam(input);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.create).toHaveBeenCalledWith(input);
|
||||
expect(result).toEqual(mockOutput);
|
||||
});
|
||||
|
||||
it('should propagate errors from API client', async () => {
|
||||
// Arrange
|
||||
const input: CreateTeamInputDto = {
|
||||
name: 'New Team',
|
||||
description: 'A new racing team',
|
||||
};
|
||||
const error = new Error('Failed to create team');
|
||||
vi.mocked(mockApiClient.create).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.createTeam(input)).rejects.toThrow('Failed to create team');
|
||||
expect(mockApiClient.create).toHaveBeenCalledWith(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTeam', () => {
|
||||
it('should update team details', async () => {
|
||||
// Arrange
|
||||
const teamId = 'team-123';
|
||||
const input: UpdateTeamInputDto = {
|
||||
name: 'Updated Team Name',
|
||||
description: 'Updated description',
|
||||
};
|
||||
|
||||
const mockOutput: UpdateTeamOutputDto = {
|
||||
id: teamId,
|
||||
success: true,
|
||||
};
|
||||
|
||||
vi.mocked(mockApiClient.update).mockResolvedValue(mockOutput);
|
||||
|
||||
// Act
|
||||
const result = await service.updateTeam(teamId, input);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.update).toHaveBeenCalledWith(teamId, input);
|
||||
expect(result).toEqual(mockOutput);
|
||||
});
|
||||
|
||||
it('should propagate errors from API client', async () => {
|
||||
// Arrange
|
||||
const teamId = 'team-123';
|
||||
const input: UpdateTeamInputDto = {
|
||||
name: 'Updated Team Name',
|
||||
};
|
||||
const error = new Error('Failed to update team');
|
||||
vi.mocked(mockApiClient.update).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.updateTeam(teamId, input)).rejects.toThrow('Failed to update team');
|
||||
expect(mockApiClient.update).toHaveBeenCalledWith(teamId, input);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDriverTeam', () => {
|
||||
it('should fetch driver team', async () => {
|
||||
// Arrange
|
||||
const driverId = 'driver-123';
|
||||
|
||||
const mockDto: DriverTeamDto = {
|
||||
teamId: 'team-456',
|
||||
teamName: 'Team Alpha',
|
||||
role: 'member',
|
||||
};
|
||||
|
||||
vi.mocked(mockApiClient.getDriverTeam).mockResolvedValue(mockDto);
|
||||
|
||||
// Act
|
||||
const result = await service.getDriverTeam(driverId);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith(driverId);
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should return null when driver has no team', async () => {
|
||||
// Arrange
|
||||
const driverId = 'driver-123';
|
||||
|
||||
vi.mocked(mockApiClient.getDriverTeam).mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const result = await service.getDriverTeam(driverId);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith(driverId);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should propagate errors from API client', async () => {
|
||||
// Arrange
|
||||
const driverId = 'driver-123';
|
||||
const error = new Error('Failed to fetch driver team');
|
||||
vi.mocked(mockApiClient.getDriverTeam).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.getDriverTeam(driverId)).rejects.toThrow('Failed to fetch driver team');
|
||||
expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith(driverId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,49 +1,51 @@
|
||||
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
|
||||
import type { TeamDetailsPresenter } from '../../presenters/TeamDetailsPresenter';
|
||||
import type { TeamListPresenter } from '../../presenters/TeamListPresenter';
|
||||
import type { TeamMembersPresenter } from '../../presenters/TeamMembersPresenter';
|
||||
import type { TeamSummaryViewModel, TeamDetailsViewModel, TeamMemberViewModel } from '../../view-models';
|
||||
import type { CreateTeamInputDto, CreateTeamOutputDto, UpdateTeamInputDto, UpdateTeamOutputDto, DriverTeamDto } from '../../dtos';
|
||||
import { TeamSummaryViewModel, TeamDetailsViewModel, TeamMemberViewModel } from '../../view-models';
|
||||
|
||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
||||
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 };
|
||||
|
||||
/**
|
||||
* Team Service
|
||||
*
|
||||
* Orchestrates team operations by coordinating API calls and presentation logic.
|
||||
* Orchestrates team operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class TeamService {
|
||||
constructor(
|
||||
private readonly apiClient: TeamsApiClient,
|
||||
private readonly teamListPresenter: TeamListPresenter,
|
||||
private readonly teamDetailsPresenter: TeamDetailsPresenter,
|
||||
private readonly teamMembersPresenter: TeamMembersPresenter
|
||||
private readonly apiClient: TeamsApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get all teams with presentation transformation
|
||||
* Get all teams with view model transformation
|
||||
*/
|
||||
async getAllTeams(): Promise<TeamSummaryViewModel[]> {
|
||||
const dto = await this.apiClient.getAll();
|
||||
return this.teamListPresenter.present(dto);
|
||||
return dto.teams.map((team: TeamSummaryDTO) => new TeamSummaryViewModel(team));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team details with presentation transformation
|
||||
* Get team details with view model transformation
|
||||
*/
|
||||
async getTeamDetails(teamId: string, currentUserId: string): Promise<TeamDetailsViewModel | null> {
|
||||
const dto = await this.apiClient.getDetails(teamId);
|
||||
if (!dto) {
|
||||
return null;
|
||||
}
|
||||
return this.teamDetailsPresenter.present(dto, currentUserId);
|
||||
return new TeamDetailsViewModel(dto, currentUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team members with presentation transformation
|
||||
* Get team members with view model transformation
|
||||
*/
|
||||
async getTeamMembers(teamId: string, currentUserId: string, teamOwnerId: string): Promise<TeamMemberViewModel[]> {
|
||||
const dto = await this.apiClient.getMembers(teamId);
|
||||
return this.teamMembersPresenter.present(dto, currentUserId, teamOwnerId);
|
||||
return dto.members.map((member: TeamMemberDTO) => new TeamMemberViewModel(member, currentUserId, teamOwnerId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user