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'); }); }); });