refactor racing use cases

This commit is contained in:
2025-12-21 00:43:42 +01:00
parent e9d6f90bb2
commit c12656d671
308 changed files with 14401 additions and 7419 deletions

View File

@@ -1,34 +1,38 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { GetRaceDetailUseCase } from './GetRaceDetailUseCase';
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import {
GetRaceDetailUseCase,
type GetRaceDetailInput,
type GetRaceDetailResult,
type GetRaceDetailErrorCode,
} from './GetRaceDetailUseCase';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
import type { IImageServicePort } from '../ports/IImageServicePort';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
describe('GetRaceDetailUseCase', () => {
let useCase: GetRaceDetailUseCase;
let raceRepository: { findById: Mock };
let leagueRepository: { findById: Mock };
let driverRepository: { findById: Mock };
let raceRegistrationRepository: { getRegisteredDrivers: Mock };
let raceRegistrationRepository: { findByRaceId: Mock };
let resultRepository: { findByRaceId: Mock };
let leagueMembershipRepository: { getMembership: Mock };
let driverRatingProvider: { getRating: Mock; getRatings: Mock };
let imageService: { getDriverAvatar: Mock; getTeamLogo: Mock; getLeagueCover: Mock; getLeagueLogo: Mock };
let output: UseCaseOutputPort<GetRaceDetailResult> & { present: Mock };
beforeEach(() => {
raceRepository = { findById: vi.fn() };
leagueRepository = { findById: vi.fn() };
driverRepository = { findById: vi.fn() };
raceRegistrationRepository = { getRegisteredDrivers: vi.fn() };
raceRegistrationRepository = { findByRaceId: vi.fn() };
resultRepository = { findByRaceId: vi.fn() };
leagueMembershipRepository = { getMembership: vi.fn() };
driverRatingProvider = { getRating: vi.fn(), getRatings: vi.fn() };
imageService = { getDriverAvatar: vi.fn(), getTeamLogo: vi.fn(), getLeagueCover: vi.fn(), getLeagueLogo: vi.fn() };
output = { present: vi.fn() } as UseCaseOutputPort<GetRaceDetailResult> & { present: Mock };
useCase = new GetRaceDetailUseCase(
raceRepository as unknown as IRaceRepository,
leagueRepository as unknown as ILeagueRepository,
@@ -36,12 +40,11 @@ describe('GetRaceDetailUseCase', () => {
raceRegistrationRepository as unknown as IRaceRegistrationRepository,
resultRepository as unknown as IResultRepository,
leagueMembershipRepository as unknown as ILeagueMembershipRepository,
driverRatingProvider as DriverRatingProvider,
imageService as IImageServicePort,
output,
);
});
it('should return race detail when race exists', async () => {
it('should present race detail when race exists', async () => {
const raceId = 'race-1';
const driverId = 'driver-1';
const race = {
@@ -62,9 +65,11 @@ describe('GetRaceDetailUseCase', () => {
description: 'Description',
settings: { maxDrivers: 20, qualifyingFormat: 'ladder' },
};
const registeredDriverIds = ['driver-1', 'driver-2'];
const registrations = [
{ driverId: { toString: () => 'driver-1' } },
{ driverId: { toString: () => 'driver-2' } },
];
const membership = { status: 'active' as const };
const ratings = new Map([['driver-1', 1600], ['driver-2', 1400]]);
const drivers = [
{ id: 'driver-1', name: 'Driver 1', country: 'US' },
{ id: 'driver-2', name: 'Driver 2', country: 'UK' },
@@ -72,46 +77,41 @@ describe('GetRaceDetailUseCase', () => {
raceRepository.findById.mockResolvedValue(race);
leagueRepository.findById.mockResolvedValue(league);
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(registeredDriverIds);
raceRegistrationRepository.findByRaceId.mockResolvedValue(registrations);
leagueMembershipRepository.getMembership.mockResolvedValue(membership);
driverRatingProvider.getRatings.mockReturnValue(ratings);
driverRepository.findById.mockImplementation((id) => Promise.resolve(drivers.find(d => d.id === id) || null));
imageService.getDriverAvatar.mockImplementation((id) => `avatar-${id}`);
driverRepository.findById.mockImplementation((id: string) =>
Promise.resolve(drivers.find(d => d.id === id) || null),
);
resultRepository.findByRaceId.mockResolvedValue([]);
const result = await useCase.execute({ raceId, driverId });
const input: GetRaceDetailInput = { raceId, driverId };
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const viewModel = result.unwrap();
expect(viewModel.race).toEqual({
id: raceId,
leagueId: 'league-1',
track: 'Track 1',
car: 'Car 1',
scheduledAt: '2023-01-01T10:00:00.000Z',
sessionType: 'race',
status: 'scheduled',
strengthOfField: 1500,
registeredCount: 10,
maxParticipants: 20,
});
expect(viewModel.league).toEqual({
id: 'league-1',
name: 'League 1',
description: 'Description',
settings: { maxDrivers: 20, qualifyingFormat: 'ladder' },
});
expect(viewModel.entryList).toHaveLength(2);
expect(viewModel.registration).toEqual({ isUserRegistered: true, canRegister: false });
expect(viewModel.userResult).toBeNull();
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetRaceDetailResult;
expect(presented.race).toEqual(race);
expect(presented.league).toEqual(league);
expect(presented.registrations).toEqual(registrations);
expect(presented.drivers).toHaveLength(2);
expect(presented.isUserRegistered).toBe(true);
expect(presented.canRegister).toBe(true);
expect(presented.userResult).toBeNull();
});
it('should return error when race not found', async () => {
raceRepository.findById.mockResolvedValue(null);
const result = await useCase.execute({ raceId: 'race-1', driverId: 'driver-1' });
const input: GetRaceDetailInput = { raceId: 'race-1', driverId: 'driver-1' };
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
expect(result.error).toEqual({ code: 'RACE_NOT_FOUND' });
const err = result.unwrapErr() as ApplicationErrorCode<GetRaceDetailErrorCode, { message: string }>;
expect(err.code).toBe('RACE_NOT_FOUND');
expect(err.details?.message).toBe('Race not found');
expect(output.present).not.toHaveBeenCalled();
});
it('should include user result when race is completed', async () => {
@@ -126,37 +126,43 @@ describe('GetRaceDetailUseCase', () => {
sessionType: 'race' as const,
status: 'completed' as const,
};
const results = [{
driverId: 'driver-1',
position: 2,
startPosition: 1,
incidents: 0,
fastestLap: 120,
getPositionChange: () => -1,
isPodium: () => true,
isClean: () => true,
}];
const registrations: Array<{ driverId: { toString: () => string } }> = [];
const userDomainResult = {
driverId: { toString: () => driverId },
} as unknown as { driverId: { toString: () => string } };
raceRepository.findById.mockResolvedValue(race);
leagueRepository.findById.mockResolvedValue(null);
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue([]);
raceRegistrationRepository.findByRaceId.mockResolvedValue(registrations);
leagueMembershipRepository.getMembership.mockResolvedValue(null);
driverRatingProvider.getRatings.mockReturnValue(new Map());
resultRepository.findByRaceId.mockResolvedValue(results);
driverRepository.findById.mockResolvedValue(null);
resultRepository.findByRaceId.mockResolvedValue([userDomainResult]);
const result = await useCase.execute({ raceId, driverId });
const input: GetRaceDetailInput = { raceId, driverId };
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const viewModel = result.unwrap();
expect(viewModel.userResult).toEqual({
position: 2,
startPosition: 1,
incidents: 0,
fastestLap: 120,
positionChange: -1,
isPodium: true,
isClean: true,
ratingChange: 61, // based on calculateRatingChange
});
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetRaceDetailResult;
expect(presented.userResult).toBe(userDomainResult);
expect(presented.race).toEqual(race);
expect(presented.league).toBeNull();
expect(presented.registrations).toEqual(registrations);
});
it('should wrap repository errors', async () => {
const error = new Error('db down');
raceRepository.findById.mockRejectedValue(error);
const input: GetRaceDetailInput = { raceId: 'race-1', driverId: 'driver-1' };
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const err = result.unwrapErr() as ApplicationErrorCode<GetRaceDetailErrorCode, { message: string }>;
expect(err.code).toBe('REPOSITORY_ERROR');
expect(err.details.message).toBe('db down');
expect(output.present).not.toHaveBeenCalled();
});
});