core tests
This commit is contained in:
354
core/rating/application/use-cases/CalculateRatingUseCase.test.ts
Normal file
354
core/rating/application/use-cases/CalculateRatingUseCase.test.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Unit tests for CalculateRatingUseCase
|
||||
*
|
||||
* Tests business logic and orchestration using mocked ports.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CalculateRatingUseCase } from './CalculateRatingUseCase';
|
||||
import { Driver } from '../../../racing/domain/entities/Driver';
|
||||
import { Race } from '../../../racing/domain/entities/Race';
|
||||
import { Result } from '../../../racing/domain/entities/result/Result';
|
||||
import { Rating } from '../../domain/Rating';
|
||||
import { RatingCalculatedEvent } from '../../domain/events/RatingCalculatedEvent';
|
||||
|
||||
// Mock repositories and publisher
|
||||
const mockDriverRepository = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
|
||||
const mockRaceRepository = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
|
||||
const mockResultRepository = {
|
||||
findByRaceId: vi.fn(),
|
||||
};
|
||||
|
||||
const mockRatingRepository = {
|
||||
save: vi.fn(),
|
||||
};
|
||||
|
||||
const mockEventPublisher = {
|
||||
publish: vi.fn(),
|
||||
};
|
||||
|
||||
describe('CalculateRatingUseCase', () => {
|
||||
let useCase: CalculateRatingUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useCase = new CalculateRatingUseCase({
|
||||
driverRepository: mockDriverRepository as any,
|
||||
raceRepository: mockRaceRepository as any,
|
||||
resultRepository: mockResultRepository as any,
|
||||
ratingRepository: mockRatingRepository as any,
|
||||
eventPublisher: mockEventPublisher as any,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 1: Driver missing', () => {
|
||||
it('should return error when driver is not found', async () => {
|
||||
// Given
|
||||
mockDriverRepository.findById.mockResolvedValue(null);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({
|
||||
driverId: 'driver-123',
|
||||
raceId: 'race-456',
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toBe('Driver not found');
|
||||
expect(mockRatingRepository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 2: Race missing', () => {
|
||||
it('should return error when race is not found', async () => {
|
||||
// Given
|
||||
const mockDriver = Driver.create({
|
||||
id: 'driver-123',
|
||||
iracingId: 'iracing-123',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
});
|
||||
mockDriverRepository.findById.mockResolvedValue(mockDriver);
|
||||
mockRaceRepository.findById.mockResolvedValue(null);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({
|
||||
driverId: 'driver-123',
|
||||
raceId: 'race-456',
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toBe('Race not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 3: No results', () => {
|
||||
it('should return error when no results found for race', async () => {
|
||||
// Given
|
||||
const mockDriver = Driver.create({
|
||||
id: 'driver-123',
|
||||
iracingId: 'iracing-123',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
});
|
||||
const mockRace = Race.create({
|
||||
id: 'race-456',
|
||||
leagueId: 'league-789',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Test Track',
|
||||
car: 'Test Car',
|
||||
});
|
||||
mockDriverRepository.findById.mockResolvedValue(mockDriver);
|
||||
mockRaceRepository.findById.mockResolvedValue(mockRace);
|
||||
mockResultRepository.findByRaceId.mockResolvedValue([]);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({
|
||||
driverId: 'driver-123',
|
||||
raceId: 'race-456',
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toBe('No results found for race');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 4: Driver not present in results', () => {
|
||||
it('should return error when driver is not in race results', async () => {
|
||||
// Given
|
||||
const mockDriver = Driver.create({
|
||||
id: 'driver-123',
|
||||
iracingId: 'iracing-123',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
});
|
||||
const mockRace = Race.create({
|
||||
id: 'race-456',
|
||||
leagueId: 'league-789',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Test Track',
|
||||
car: 'Test Car',
|
||||
});
|
||||
const otherResult = Result.create({
|
||||
id: 'result-1',
|
||||
raceId: 'race-456',
|
||||
driverId: 'driver-456',
|
||||
position: 1,
|
||||
fastestLap: 60000,
|
||||
incidents: 0,
|
||||
startPosition: 1,
|
||||
points: 25,
|
||||
});
|
||||
mockDriverRepository.findById.mockResolvedValue(mockDriver);
|
||||
mockRaceRepository.findById.mockResolvedValue(mockRace);
|
||||
mockResultRepository.findByRaceId.mockResolvedValue([otherResult]);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({
|
||||
driverId: 'driver-123',
|
||||
raceId: 'race-456',
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toBe('Driver not found in race results');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 5: Publishes event after save', () => {
|
||||
it('should call ratingRepository.save before eventPublisher.publish', async () => {
|
||||
// Given
|
||||
const mockDriver = Driver.create({
|
||||
id: 'driver-123',
|
||||
iracingId: 'iracing-123',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
});
|
||||
const mockRace = Race.create({
|
||||
id: 'race-456',
|
||||
leagueId: 'league-789',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Test Track',
|
||||
car: 'Test Car',
|
||||
});
|
||||
const mockResult = Result.create({
|
||||
id: 'result-1',
|
||||
raceId: 'race-456',
|
||||
driverId: 'driver-123',
|
||||
position: 1,
|
||||
fastestLap: 60000,
|
||||
incidents: 0,
|
||||
startPosition: 1,
|
||||
points: 25,
|
||||
});
|
||||
mockDriverRepository.findById.mockResolvedValue(mockDriver);
|
||||
mockRaceRepository.findById.mockResolvedValue(mockRace);
|
||||
mockResultRepository.findByRaceId.mockResolvedValue([mockResult]);
|
||||
mockRatingRepository.save.mockResolvedValue(undefined);
|
||||
mockEventPublisher.publish.mockResolvedValue(undefined);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({
|
||||
driverId: 'driver-123',
|
||||
raceId: 'race-456',
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(mockRatingRepository.save).toHaveBeenCalledTimes(1);
|
||||
expect(mockEventPublisher.publish).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Verify call order: save should be called before publish
|
||||
const saveCallOrder = mockRatingRepository.save.mock.invocationCallOrder[0];
|
||||
const publishCallOrder = mockEventPublisher.publish.mock.invocationCallOrder[0];
|
||||
expect(saveCallOrder).toBeLessThan(publishCallOrder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 6: Component boundaries for cleanDriving', () => {
|
||||
it('should return cleanDriving = 100 when incidents = 0', async () => {
|
||||
// Given
|
||||
const mockDriver = Driver.create({
|
||||
id: 'driver-123',
|
||||
iracingId: 'iracing-123',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
});
|
||||
const mockRace = Race.create({
|
||||
id: 'race-456',
|
||||
leagueId: 'league-789',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Test Track',
|
||||
car: 'Test Car',
|
||||
});
|
||||
const mockResult = Result.create({
|
||||
id: 'result-1',
|
||||
raceId: 'race-456',
|
||||
driverId: 'driver-123',
|
||||
position: 1,
|
||||
fastestLap: 60000,
|
||||
incidents: 0,
|
||||
startPosition: 1,
|
||||
points: 25,
|
||||
});
|
||||
mockDriverRepository.findById.mockResolvedValue(mockDriver);
|
||||
mockRaceRepository.findById.mockResolvedValue(mockRace);
|
||||
mockResultRepository.findByRaceId.mockResolvedValue([mockResult]);
|
||||
mockRatingRepository.save.mockResolvedValue(undefined);
|
||||
mockEventPublisher.publish.mockResolvedValue(undefined);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({
|
||||
driverId: 'driver-123',
|
||||
raceId: 'race-456',
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.isOk()).toBe(true);
|
||||
const rating = result.unwrap();
|
||||
expect(rating.components.cleanDriving).toBe(100);
|
||||
});
|
||||
|
||||
it('should return cleanDriving = 20 when incidents >= 5', async () => {
|
||||
// Given
|
||||
const mockDriver = Driver.create({
|
||||
id: 'driver-123',
|
||||
iracingId: 'iracing-123',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
});
|
||||
const mockRace = Race.create({
|
||||
id: 'race-456',
|
||||
leagueId: 'league-789',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Test Track',
|
||||
car: 'Test Car',
|
||||
});
|
||||
const mockResult = Result.create({
|
||||
id: 'result-1',
|
||||
raceId: 'race-456',
|
||||
driverId: 'driver-123',
|
||||
position: 1,
|
||||
fastestLap: 60000,
|
||||
incidents: 5,
|
||||
startPosition: 1,
|
||||
points: 25,
|
||||
});
|
||||
mockDriverRepository.findById.mockResolvedValue(mockDriver);
|
||||
mockRaceRepository.findById.mockResolvedValue(mockRace);
|
||||
mockResultRepository.findByRaceId.mockResolvedValue([mockResult]);
|
||||
mockRatingRepository.save.mockResolvedValue(undefined);
|
||||
mockEventPublisher.publish.mockResolvedValue(undefined);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({
|
||||
driverId: 'driver-123',
|
||||
raceId: 'race-456',
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.isOk()).toBe(true);
|
||||
const rating = result.unwrap();
|
||||
expect(rating.components.cleanDriving).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scenario 7: Time-dependent output', () => {
|
||||
it('should produce deterministic timestamp when time is frozen', async () => {
|
||||
// Given
|
||||
const frozenTime = new Date('2024-01-01T12:00:00.000Z');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(frozenTime);
|
||||
|
||||
const mockDriver = Driver.create({
|
||||
id: 'driver-123',
|
||||
iracingId: 'iracing-123',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
});
|
||||
const mockRace = Race.create({
|
||||
id: 'race-456',
|
||||
leagueId: 'league-789',
|
||||
scheduledAt: new Date(),
|
||||
track: 'Test Track',
|
||||
car: 'Test Car',
|
||||
});
|
||||
const mockResult = Result.create({
|
||||
id: 'result-1',
|
||||
raceId: 'race-456',
|
||||
driverId: 'driver-123',
|
||||
position: 1,
|
||||
fastestLap: 60000,
|
||||
incidents: 0,
|
||||
startPosition: 1,
|
||||
points: 25,
|
||||
});
|
||||
mockDriverRepository.findById.mockResolvedValue(mockDriver);
|
||||
mockRaceRepository.findById.mockResolvedValue(mockRace);
|
||||
mockResultRepository.findByRaceId.mockResolvedValue([mockResult]);
|
||||
mockRatingRepository.save.mockResolvedValue(undefined);
|
||||
mockEventPublisher.publish.mockResolvedValue(undefined);
|
||||
|
||||
// When
|
||||
const result = await useCase.execute({
|
||||
driverId: 'driver-123',
|
||||
raceId: 'race-456',
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.isOk()).toBe(true);
|
||||
const rating = result.unwrap();
|
||||
expect(rating.timestamp).toEqual(frozenTime);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user