This commit is contained in:
2025-12-16 21:05:01 +01:00
parent f61e3a4e5a
commit 7532c7ed6d
207 changed files with 7861 additions and 2606 deletions

View File

@@ -8,15 +8,13 @@ import type { IResultRepository } from '@core/racing/domain/repositories/IResult
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import type {
IRaceDetailPresenter,
RaceDetailViewModel,
} from '@core/racing/application/presenters/IRaceDetailPresenter';
import type { Logger } from '@core/shared/application';
import { Race } from '@core/racing/domain/entities/Race';
import { League } from '@core/racing/domain/entities/League';
import { Result } from '@core/racing/domain/entities/Result';
import { Driver } from '@core/racing/domain/entities/Driver';
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
import { GetRaceDetailUseCase } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import { CancelRaceUseCase } from '@core/racing/application/use-cases/CancelRaceUseCase';
@@ -154,11 +152,11 @@ class InMemoryDriverRepository implements IDriverRepository {
.filter((d): d is Driver => !!d);
}
async create(): Promise<any> {
async create(): Promise<Driver> {
throw new Error('Not needed for these tests');
}
async update(): Promise<any> {
async update(): Promise<Driver> {
throw new Error('Not needed for these tests');
}
@@ -371,21 +369,11 @@ class TestImageService implements IImageServicePort {
}
}
class FakeRaceDetailPresenter implements IRaceDetailPresenter {
viewModel: RaceDetailViewModel | null = null;
present(viewModel: RaceDetailViewModel): RaceDetailViewModel {
this.viewModel = viewModel;
return viewModel;
}
getViewModel(): RaceDetailViewModel | null {
return this.viewModel;
}
reset(): void {
this.viewModel = null;
}
class MockLogger implements Logger {
debug = vi.fn();
info = vi.fn();
warn = vi.fn();
error = vi.fn();
}
describe('GetRaceDetailUseCase', () => {
@@ -404,7 +392,7 @@ describe('GetRaceDetailUseCase', () => {
scheduledAt: new Date(Date.now() + 60 * 60 * 1000),
track: 'Test Track',
car: 'GT3',
sessionType: 'race',
sessionType: 'main',
status: 'scheduled',
});
@@ -439,7 +427,6 @@ describe('GetRaceDetailUseCase', () => {
ratingProvider.seed(otherDriverId, 1600);
const imageService = new TestImageService();
const presenter = new FakeRaceDetailPresenter();
const useCase = new GetRaceDetailUseCase(
raceRepo,
@@ -453,10 +440,10 @@ describe('GetRaceDetailUseCase', () => {
);
// When (execute the query for the current driver)
await useCase.execute({ raceId: race.id, driverId }, presenter);
const result = await useCase.execute({ raceId: race.id, driverId });
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
expect(result.isOk()).toBe(true);
const viewModel = result.unwrap();
// Then (verify race, league and registration flags)
expect(viewModel!.race?.id).toBe(race.id);
@@ -494,7 +481,7 @@ describe('GetRaceDetailUseCase', () => {
scheduledAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
track: 'Historic Circuit',
car: 'LMP2',
sessionType: 'race',
sessionType: 'main',
status: 'completed',
});
@@ -534,7 +521,6 @@ describe('GetRaceDetailUseCase', () => {
ratingProvider.seed(driverId, 2000);
const imageService = new TestImageService();
const presenter = new FakeRaceDetailPresenter();
const useCase = new GetRaceDetailUseCase(
raceRepo,
@@ -548,11 +534,11 @@ describe('GetRaceDetailUseCase', () => {
);
// When (executing the query for the completed race)
await useCase.execute({ raceId: race.id, driverId }, presenter);
const result = await useCase.execute({ raceId: race.id, driverId });
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
expect(viewModel!.userResult).not.toBeNull();
expect(result.isOk()).toBe(true);
const viewModel = result.unwrap();
expect(viewModel.userResult).not.toBeNull();
// Then (rating change uses the same formula as the legacy UI)
// For P1: baseChange = 25, positionBonus = (20 - 1) * 2 = 38, total = 63
@@ -574,7 +560,6 @@ describe('GetRaceDetailUseCase', () => {
const membershipRepo = new InMemoryLeagueMembershipRepository();
const ratingProvider = new TestDriverRatingProvider();
const imageService = new TestImageService();
const presenter = new FakeRaceDetailPresenter();
const useCase = new GetRaceDetailUseCase(
raceRepo,
@@ -588,13 +573,11 @@ describe('GetRaceDetailUseCase', () => {
);
// When
await useCase.execute({ raceId: 'missing-race', driverId: 'driver-x' }, presenter);
const result = await useCase.execute({ raceId: 'missing-race', driverId: 'driver-x' });
const viewModel = presenter.getViewModel();
// Then
expect(viewModel).not.toBeNull();
expect(viewModel!.race).toBeNull();
expect(viewModel!.error).toBe('Race not found');
expect(result.isErr()).toBe(true);
expect(result.unwrapErr()).toEqual({ code: 'RACE_NOT_FOUND' });
});
});
@@ -607,30 +590,35 @@ describe('CancelRaceUseCase', () => {
scheduledAt: new Date(Date.now() + 60 * 60 * 1000),
track: 'Cancel Circuit',
car: 'GT4',
sessionType: 'race',
sessionType: 'main',
status: 'scheduled',
});
const raceRepo = new InMemoryRaceRepository([race]);
const useCase = new CancelRaceUseCase(raceRepo);
const logger = new MockLogger();
const useCase = new CancelRaceUseCase(raceRepo, logger);
// When
await useCase.execute({ raceId: race.id });
const result = await useCase.execute({ raceId: race.id });
// Then (the stored race is now cancelled)
expect(result.isOk()).toBe(true);
const updated = raceRepo.getStored(race.id);
expect(updated).not.toBeNull();
expect(updated!.status).toBe('cancelled');
});
it('throws when trying to cancel a non-existent race', async () => {
it('returns error when trying to cancel a non-existent race', async () => {
// Given
const raceRepo = new InMemoryRaceRepository([]);
const useCase = new CancelRaceUseCase(raceRepo);
const logger = new MockLogger();
const useCase = new CancelRaceUseCase(raceRepo, logger);
// When / Then
await expect(
useCase.execute({ raceId: 'does-not-exist' }),
).rejects.toThrow('Race not found');
// When
const result = await useCase.execute({ raceId: 'does-not-exist' });
// Then
expect(result.isErr()).toBe(true);
expect(result.unwrapErr()).toEqual({ code: 'RACE_NOT_FOUND' });
});
});