292 lines
9.8 KiB
TypeScript
292 lines
9.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { DriverRegistrationService } from './DriverRegistrationService';
|
|
import type { DriversApiClient } from '../../api/drivers/DriversApiClient';
|
|
import type { DriverRegistrationStatusPresenter } from '../../presenters/DriverRegistrationStatusPresenter';
|
|
import type { DriverRegistrationStatusDto } from '../../dtos';
|
|
import type { DriverRegistrationStatusViewModel } from '../../view-models';
|
|
|
|
describe('DriverRegistrationService', () => {
|
|
let service: DriverRegistrationService;
|
|
let mockApiClient: DriversApiClient;
|
|
let mockStatusPresenter: DriverRegistrationStatusPresenter;
|
|
|
|
beforeEach(() => {
|
|
mockApiClient = {
|
|
getRegistrationStatus: vi.fn(),
|
|
} as unknown as DriversApiClient;
|
|
|
|
mockStatusPresenter = {
|
|
present: vi.fn(),
|
|
} as unknown as DriverRegistrationStatusPresenter;
|
|
|
|
service = new DriverRegistrationService(
|
|
mockApiClient,
|
|
mockStatusPresenter
|
|
);
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('should create instance with injected dependencies', () => {
|
|
expect(service).toBeInstanceOf(DriverRegistrationService);
|
|
});
|
|
});
|
|
|
|
describe('getDriverRegistrationStatus', () => {
|
|
it('should fetch registration status from API and transform via presenter', async () => {
|
|
// Arrange
|
|
const driverId = 'driver-123';
|
|
const raceId = 'race-456';
|
|
|
|
const mockDto: DriverRegistrationStatusDto = {
|
|
isRegistered: true,
|
|
raceId: 'race-456',
|
|
driverId: 'driver-123',
|
|
};
|
|
|
|
const mockViewModel = {
|
|
isRegistered: true,
|
|
raceId: 'race-456',
|
|
driverId: 'driver-123',
|
|
statusMessage: 'Registered for this race',
|
|
statusColor: 'green',
|
|
statusBadgeVariant: 'success',
|
|
registrationButtonText: 'Withdraw',
|
|
canRegister: false,
|
|
} as DriverRegistrationStatusViewModel;
|
|
|
|
vi.mocked(mockApiClient.getRegistrationStatus).mockResolvedValue(mockDto);
|
|
vi.mocked(mockStatusPresenter.present).mockReturnValue(mockViewModel);
|
|
|
|
// Act
|
|
const result = await service.getDriverRegistrationStatus(driverId, raceId);
|
|
|
|
// Assert
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
|
|
expect(mockStatusPresenter.present).toHaveBeenCalledWith(mockDto);
|
|
expect(result).toEqual(mockViewModel);
|
|
});
|
|
|
|
it('should handle unregistered driver status', async () => {
|
|
// Arrange
|
|
const driverId = 'driver-789';
|
|
const raceId = 'race-101';
|
|
|
|
const mockDto: DriverRegistrationStatusDto = {
|
|
isRegistered: false,
|
|
raceId: 'race-101',
|
|
driverId: 'driver-789',
|
|
};
|
|
|
|
const mockViewModel = {
|
|
isRegistered: false,
|
|
raceId: 'race-101',
|
|
driverId: 'driver-789',
|
|
statusMessage: 'Not registered',
|
|
statusColor: 'red',
|
|
statusBadgeVariant: 'warning',
|
|
registrationButtonText: 'Register',
|
|
canRegister: true,
|
|
} as DriverRegistrationStatusViewModel;
|
|
|
|
vi.mocked(mockApiClient.getRegistrationStatus).mockResolvedValue(mockDto);
|
|
vi.mocked(mockStatusPresenter.present).mockReturnValue(mockViewModel);
|
|
|
|
// Act
|
|
const result = await service.getDriverRegistrationStatus(driverId, raceId);
|
|
|
|
// Assert
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
|
|
expect(mockStatusPresenter.present).toHaveBeenCalledWith(mockDto);
|
|
expect(result).toEqual(mockViewModel);
|
|
expect(result.canRegister).toBe(true);
|
|
});
|
|
|
|
it('should propagate errors from API client', async () => {
|
|
// Arrange
|
|
const driverId = 'driver-123';
|
|
const raceId = 'race-456';
|
|
const error = new Error('Failed to fetch registration status');
|
|
|
|
vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(error);
|
|
|
|
// Act & Assert
|
|
await expect(service.getDriverRegistrationStatus(driverId, raceId))
|
|
.rejects.toThrow('Failed to fetch registration status');
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
|
|
expect(mockStatusPresenter.present).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle network errors gracefully', async () => {
|
|
// Arrange
|
|
const driverId = 'driver-123';
|
|
const raceId = 'race-456';
|
|
const networkError = new Error('Network request failed');
|
|
|
|
vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(networkError);
|
|
|
|
// Act & Assert
|
|
await expect(service.getDriverRegistrationStatus(driverId, raceId))
|
|
.rejects.toThrow('Network request failed');
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
|
|
});
|
|
|
|
it('should handle API errors with proper error propagation', async () => {
|
|
// Arrange
|
|
const driverId = 'driver-123';
|
|
const raceId = 'race-456';
|
|
const apiError = new Error('Race not found');
|
|
|
|
vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(apiError);
|
|
|
|
// Act & Assert
|
|
await expect(service.getDriverRegistrationStatus(driverId, raceId))
|
|
.rejects.toThrow('Race not found');
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
|
|
expect(mockStatusPresenter.present).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle multiple consecutive calls correctly', async () => {
|
|
// Arrange
|
|
const driverId = 'driver-123';
|
|
const raceId1 = 'race-456';
|
|
const raceId2 = 'race-789';
|
|
|
|
const mockDto1: DriverRegistrationStatusDto = {
|
|
isRegistered: true,
|
|
raceId: raceId1,
|
|
driverId,
|
|
};
|
|
|
|
const mockDto2: DriverRegistrationStatusDto = {
|
|
isRegistered: false,
|
|
raceId: raceId2,
|
|
driverId,
|
|
};
|
|
|
|
const mockViewModel1 = {
|
|
isRegistered: true,
|
|
raceId: raceId1,
|
|
driverId,
|
|
} as DriverRegistrationStatusViewModel;
|
|
|
|
const mockViewModel2 = {
|
|
isRegistered: false,
|
|
raceId: raceId2,
|
|
driverId,
|
|
} as DriverRegistrationStatusViewModel;
|
|
|
|
vi.mocked(mockApiClient.getRegistrationStatus)
|
|
.mockResolvedValueOnce(mockDto1)
|
|
.mockResolvedValueOnce(mockDto2);
|
|
|
|
vi.mocked(mockStatusPresenter.present)
|
|
.mockReturnValueOnce(mockViewModel1)
|
|
.mockReturnValueOnce(mockViewModel2);
|
|
|
|
// Act
|
|
const result1 = await service.getDriverRegistrationStatus(driverId, raceId1);
|
|
const result2 = await service.getDriverRegistrationStatus(driverId, raceId2);
|
|
|
|
// Assert
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledTimes(2);
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenNthCalledWith(1, driverId, raceId1);
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenNthCalledWith(2, driverId, raceId2);
|
|
expect(result1.isRegistered).toBe(true);
|
|
expect(result2.isRegistered).toBe(false);
|
|
});
|
|
|
|
it('should handle different driver IDs for same race', async () => {
|
|
// Arrange
|
|
const driverId1 = 'driver-123';
|
|
const driverId2 = 'driver-456';
|
|
const raceId = 'race-789';
|
|
|
|
const mockDto1: DriverRegistrationStatusDto = {
|
|
isRegistered: true,
|
|
raceId,
|
|
driverId: driverId1,
|
|
};
|
|
|
|
const mockDto2: DriverRegistrationStatusDto = {
|
|
isRegistered: false,
|
|
raceId,
|
|
driverId: driverId2,
|
|
};
|
|
|
|
const mockViewModel1 = {
|
|
isRegistered: true,
|
|
raceId,
|
|
driverId: driverId1,
|
|
} as DriverRegistrationStatusViewModel;
|
|
|
|
const mockViewModel2 = {
|
|
isRegistered: false,
|
|
raceId,
|
|
driverId: driverId2,
|
|
} as DriverRegistrationStatusViewModel;
|
|
|
|
vi.mocked(mockApiClient.getRegistrationStatus)
|
|
.mockResolvedValueOnce(mockDto1)
|
|
.mockResolvedValueOnce(mockDto2);
|
|
|
|
vi.mocked(mockStatusPresenter.present)
|
|
.mockReturnValueOnce(mockViewModel1)
|
|
.mockReturnValueOnce(mockViewModel2);
|
|
|
|
// Act
|
|
const result1 = await service.getDriverRegistrationStatus(driverId1, raceId);
|
|
const result2 = await service.getDriverRegistrationStatus(driverId2, raceId);
|
|
|
|
// Assert
|
|
expect(result1.driverId).toBe(driverId1);
|
|
expect(result1.isRegistered).toBe(true);
|
|
expect(result2.driverId).toBe(driverId2);
|
|
expect(result2.isRegistered).toBe(false);
|
|
});
|
|
|
|
it('should handle unauthorized access errors', async () => {
|
|
// Arrange
|
|
const driverId = 'driver-123';
|
|
const raceId = 'race-456';
|
|
const authError = new Error('Unauthorized: Driver not found');
|
|
|
|
vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(authError);
|
|
|
|
// Act & Assert
|
|
await expect(service.getDriverRegistrationStatus(driverId, raceId))
|
|
.rejects.toThrow('Unauthorized: Driver not found');
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
|
|
expect(mockStatusPresenter.present).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should call presenter only after successful API response', async () => {
|
|
// Arrange
|
|
const driverId = 'driver-123';
|
|
const raceId = 'race-456';
|
|
|
|
const mockDto: DriverRegistrationStatusDto = {
|
|
isRegistered: true,
|
|
raceId: 'race-456',
|
|
driverId: 'driver-123',
|
|
};
|
|
|
|
const mockViewModel = {
|
|
isRegistered: true,
|
|
raceId: 'race-456',
|
|
driverId: 'driver-123',
|
|
} as DriverRegistrationStatusViewModel;
|
|
|
|
vi.mocked(mockApiClient.getRegistrationStatus).mockResolvedValue(mockDto);
|
|
vi.mocked(mockStatusPresenter.present).mockReturnValue(mockViewModel);
|
|
|
|
// Act
|
|
await service.getDriverRegistrationStatus(driverId, raceId);
|
|
|
|
// Assert - verify call order
|
|
expect(mockApiClient.getRegistrationStatus).toHaveBeenCalled();
|
|
expect(mockStatusPresenter.present).toHaveBeenCalledAfter(
|
|
mockApiClient.getRegistrationStatus as any
|
|
);
|
|
});
|
|
});
|
|
}); |