This commit is contained in:
2025-12-29 22:27:33 +01:00
parent 3f610c1cb6
commit 7a853d4e43
96 changed files with 14790 additions and 111 deletions

View File

@@ -10,6 +10,7 @@ import type { IResultRepository } from '../../domain/repositories/IResultReposit
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import { RatingUpdateService } from '@core/identity/domain/services/RatingUpdateService';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { IRaceResultsProvider } from '@core/identity/application/ports/IRaceResultsProvider';
describe('CompleteRaceUseCaseWithRatings', () => {
let useCase: CompleteRaceUseCaseWithRatings;
@@ -32,6 +33,11 @@ describe('CompleteRaceUseCaseWithRatings', () => {
};
let ratingUpdateService: {
updateDriverRatingsAfterRace: Mock;
recordRaceRatingEvents: Mock;
};
let raceResultsProvider: {
getRaceResults: Mock;
hasRaceResults: Mock;
};
let output: { present: Mock };
@@ -55,9 +61,15 @@ describe('CompleteRaceUseCaseWithRatings', () => {
};
ratingUpdateService = {
updateDriverRatingsAfterRace: vi.fn(),
recordRaceRatingEvents: vi.fn(),
};
raceResultsProvider = {
getRaceResults: vi.fn(),
hasRaceResults: vi.fn(),
};
output = { present: vi.fn() };
// Test without raceResultsProvider (backward compatible mode)
useCase = new CompleteRaceUseCaseWithRatings(
raceRepository as unknown as IRaceRepository,
raceRegistrationRepository as unknown as IRaceRegistrationRepository,
@@ -221,4 +233,185 @@ describe('CompleteRaceUseCaseWithRatings', () => {
expect(error.details?.message).toBe('DB error');
expect(output.present).not.toHaveBeenCalled();
});
// SLICE 7: New tests for ledger-based approach
describe('Ledger-based rating updates (Slice 7)', () => {
let useCaseWithLedger: CompleteRaceUseCaseWithRatings;
beforeEach(() => {
// Create use case with raceResultsProvider for ledger mode
useCaseWithLedger = new CompleteRaceUseCaseWithRatings(
raceRepository as unknown as IRaceRepository,
raceRegistrationRepository as unknown as IRaceRegistrationRepository,
resultRepository as unknown as IResultRepository,
standingRepository as unknown as IStandingRepository,
driverRatingProvider,
ratingUpdateService as unknown as RatingUpdateService,
output as unknown as UseCaseOutputPort<CompleteRaceWithRatingsResult>,
raceResultsProvider as unknown as IRaceResultsProvider,
);
});
it('completes race with ledger-based rating updates when raceResultsProvider is available', async () => {
const command: CompleteRaceWithRatingsInput = {
raceId: 'race-1',
};
const mockRace = {
id: 'race-1',
leagueId: 'league-1',
status: 'scheduled',
complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }),
};
raceRepository.findById.mockResolvedValue(mockRace);
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1', 'driver-2']);
driverRatingProvider.getRatings.mockReturnValue(
new Map([
['driver-1', 1600],
['driver-2', 1500],
]),
);
resultRepository.create.mockResolvedValue(undefined);
standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null);
standingRepository.save.mockResolvedValue(undefined);
// Mock ledger-based rating update
ratingUpdateService.recordRaceRatingEvents.mockResolvedValue({
success: true,
eventsCreated: 4,
driversUpdated: ['driver-1', 'driver-2'],
});
raceRepository.update.mockResolvedValue(undefined);
const result = await useCaseWithLedger.execute(command);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
// Verify ledger-based approach was used
expect(ratingUpdateService.recordRaceRatingEvents).toHaveBeenCalledWith(
'race-1',
expect.arrayContaining([
expect.objectContaining({
userId: 'driver-1',
startPos: expect.any(Number),
finishPos: expect.any(Number),
incidents: expect.any(Number),
status: 'finished',
}),
]),
);
// Verify legacy method was NOT called
expect(ratingUpdateService.updateDriverRatingsAfterRace).not.toHaveBeenCalled();
expect(raceRepository.update).toHaveBeenCalledWith({ id: 'race-1', status: 'completed' });
expect(output.present).toHaveBeenCalledWith({
raceId: 'race-1',
ratingsUpdatedForDriverIds: ['driver-1', 'driver-2'],
});
});
it('falls back to legacy approach when ledger update fails', async () => {
const command: CompleteRaceWithRatingsInput = {
raceId: 'race-1',
};
const mockRace = {
id: 'race-1',
leagueId: 'league-1',
status: 'scheduled',
complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }),
};
raceRepository.findById.mockResolvedValue(mockRace);
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']);
driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]]));
resultRepository.create.mockResolvedValue(undefined);
standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null);
standingRepository.save.mockResolvedValue(undefined);
// Mock ledger-based rating update failure
ratingUpdateService.recordRaceRatingEvents.mockResolvedValue({
success: false,
eventsCreated: 0,
driversUpdated: [],
});
// Legacy method should be called as fallback
ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined);
raceRepository.update.mockResolvedValue(undefined);
const result = await useCaseWithLedger.execute(command);
expect(result.isOk()).toBe(true);
// Verify both methods were called (ledger attempted, then fallback)
expect(ratingUpdateService.recordRaceRatingEvents).toHaveBeenCalled();
expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled();
});
it('handles ledger update errors gracefully', async () => {
const command: CompleteRaceWithRatingsInput = {
raceId: 'race-1',
};
const mockRace = {
id: 'race-1',
leagueId: 'league-1',
status: 'scheduled',
complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }),
};
raceRepository.findById.mockResolvedValue(mockRace);
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']);
driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]]));
resultRepository.create.mockResolvedValue(undefined);
standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null);
standingRepository.save.mockResolvedValue(undefined);
// Mock ledger-based rating update throwing error
ratingUpdateService.recordRaceRatingEvents.mockRejectedValue(new Error('Ledger error'));
// Legacy method should be called as fallback
ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined);
raceRepository.update.mockResolvedValue(undefined);
const result = await useCaseWithLedger.execute(command);
expect(result.isOk()).toBe(true);
// Verify fallback was used
expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled();
});
it('still works without raceResultsProvider (backward compatibility)', async () => {
const command: CompleteRaceWithRatingsInput = {
raceId: 'race-1',
};
const mockRace = {
id: 'race-1',
leagueId: 'league-1',
status: 'scheduled',
complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }),
};
raceRepository.findById.mockResolvedValue(mockRace);
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']);
driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]]));
resultRepository.create.mockResolvedValue(undefined);
standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null);
standingRepository.save.mockResolvedValue(undefined);
ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined);
raceRepository.update.mockResolvedValue(undefined);
// Use original useCase without raceResultsProvider
const result = await useCase.execute(command);
expect(result.isOk()).toBe(true);
// Verify only legacy method was called
expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled();
expect(ratingUpdateService.recordRaceRatingEvents).not.toHaveBeenCalled();
});
});
});