Files
gridpilot.gg/tests/bdd/race-event-performance-summary.test.ts
2025-12-16 11:52:26 +01:00

284 lines
9.8 KiB
TypeScript

import { describe, it, beforeEach, expect, vi } from 'vitest';
import { Session } from '@core/racing/domain/entities/Session';
import { RaceEvent } from '@core/racing/domain/entities/RaceEvent';
import { SessionType } from '@core/racing/domain/value-objects/SessionType';
import { MainRaceCompletedEvent } from '@core/racing/domain/events/MainRaceCompleted';
import { RaceEventStewardingClosedEvent } from '@core/racing/domain/events/RaceEventStewardingClosed';
import { SendPerformanceSummaryUseCase } from '@core/racing/application/use-cases/SendPerformanceSummaryUseCase';
import { SendFinalResultsUseCase } from '@core/racing/application/use-cases/SendFinalResultsUseCase';
import { CloseRaceEventStewardingUseCase } from '@core/racing/application/use-cases/CloseRaceEventStewardingUseCase';
import { InMemoryRaceEventRepository } from '@core/racing/infrastructure/repositories/InMemoryRaceEventRepository';
import { InMemorySessionRepository } from '@core/racing/infrastructure/repositories/InMemorySessionRepository';
// Mock notification service
const mockNotificationService = {
sendNotification: vi.fn(),
};
// Test data builders
const createTestSession = (overrides: Partial<{
id: string;
raceEventId: string;
sessionType: SessionType;
status: 'scheduled' | 'running' | 'completed';
scheduledAt: Date;
}> = {}) => {
return Session.create({
id: overrides.id ?? 'session-1',
raceEventId: overrides.raceEventId ?? 'race-event-1',
scheduledAt: overrides.scheduledAt ?? new Date(),
track: 'Monza',
car: 'F1 Car',
sessionType: overrides.sessionType ?? SessionType.main(),
status: overrides.status ?? 'scheduled',
});
};
const createTestRaceEvent = (overrides: Partial<{
id: string;
seasonId: string;
leagueId: string;
name: string;
sessions: Session[];
status: 'scheduled' | 'in_progress' | 'awaiting_stewarding' | 'closed';
stewardingClosesAt: Date;
}> = {}) => {
const sessions = overrides.sessions ?? [
createTestSession({ id: 'practice-1', sessionType: SessionType.practice() }),
createTestSession({ id: 'qualifying-1', sessionType: SessionType.qualifying() }),
createTestSession({ id: 'main-1', sessionType: SessionType.main() }),
];
return RaceEvent.create({
id: overrides.id ?? 'race-event-1',
seasonId: overrides.seasonId ?? 'season-1',
leagueId: overrides.leagueId ?? 'league-1',
name: overrides.name ?? 'Monza Grand Prix',
sessions,
status: overrides.status ?? 'scheduled',
stewardingClosesAt: overrides.stewardingClosesAt,
});
};
describe('Race Event Performance Summary Notifications', () => {
let raceEventRepository: InMemoryRaceEventRepository;
let sessionRepository: InMemorySessionRepository;
let sendPerformanceSummaryUseCase: SendPerformanceSummaryUseCase;
let sendFinalResultsUseCase: SendFinalResultsUseCase;
let closeStewardingUseCase: CloseRaceEventStewardingUseCase;
beforeEach(() => {
raceEventRepository = new InMemoryRaceEventRepository();
sessionRepository = new InMemorySessionRepository();
sendPerformanceSummaryUseCase = new SendPerformanceSummaryUseCase(
mockNotificationService as any,
raceEventRepository as any,
{} as any // Mock result repository
);
sendFinalResultsUseCase = new SendFinalResultsUseCase(
mockNotificationService as any,
raceEventRepository as any,
{} as any // Mock result repository
);
closeStewardingUseCase = new CloseRaceEventStewardingUseCase(
raceEventRepository as any,
{} as any // Mock domain event publisher
);
vi.clearAllMocks();
});
describe('Performance Summary After Main Race Completion', () => {
it('should send performance summary notification when main race completes', async () => {
// Given
const raceEvent = createTestRaceEvent();
await raceEventRepository.create(raceEvent);
const mainRaceCompletedEvent = new MainRaceCompletedEvent({
raceEventId: raceEvent.id,
sessionId: 'main-1',
leagueId: raceEvent.leagueId,
seasonId: raceEvent.seasonId,
completedAt: new Date(),
driverIds: ['driver-1', 'driver-2'],
});
// When
await sendPerformanceSummaryUseCase.execute(mainRaceCompletedEvent);
// Then
expect(mockNotificationService.sendNotification).toHaveBeenCalledTimes(2);
expect(mockNotificationService.sendNotification).toHaveBeenCalledWith(
expect.objectContaining({
recipientId: 'driver-1',
type: 'race_performance_summary',
urgency: 'modal',
title: expect.stringContaining('Race Complete'),
})
);
});
it('should calculate provisional rating changes correctly', async () => {
// Given
const raceEvent = createTestRaceEvent();
await raceEventRepository.create(raceEvent);
// Mock result repository to return position data
const mockResultRepository = {
findByRaceId: vi.fn().mockResolvedValue([
{ driverId: 'driver-1', position: 1, incidents: 0, getPositionChange: () => 0 },
]),
};
const useCase = new SendPerformanceSummaryUseCase(
mockNotificationService as any,
raceEventRepository as any,
mockResultRepository as any
);
const event = new MainRaceCompletedEvent({
raceEventId: raceEvent.id,
sessionId: 'main-1',
leagueId: raceEvent.leagueId,
seasonId: raceEvent.seasonId,
completedAt: new Date(),
driverIds: ['driver-1'],
});
// When
await useCase.execute(event);
// Then
expect(mockNotificationService.sendNotification).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
provisionalRatingChange: 25, // P1 with 0 incidents = +25
}),
})
);
});
});
describe('Final Results After Stewarding Closes', () => {
it('should send final results notification when stewarding closes', async () => {
// Given
const raceEvent = createTestRaceEvent({ status: 'awaiting_stewarding' });
await raceEventRepository.create(raceEvent);
const stewardingClosedEvent = new RaceEventStewardingClosedEvent({
raceEventId: raceEvent.id,
leagueId: raceEvent.leagueId,
seasonId: raceEvent.seasonId,
closedAt: new Date(),
driverIds: ['driver-1', 'driver-2'],
hadPenaltiesApplied: false,
});
// When
await sendFinalResultsUseCase.execute(stewardingClosedEvent);
// Then
expect(mockNotificationService.sendNotification).toHaveBeenCalledTimes(2);
expect(mockNotificationService.sendNotification).toHaveBeenCalledWith(
expect.objectContaining({
recipientId: 'driver-1',
type: 'race_final_results',
urgency: 'modal',
title: expect.stringContaining('Final Results'),
})
);
});
});
describe('Stewarding Window Management', () => {
it('should close expired stewarding windows', async () => {
// Given
const pastDate = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago
const raceEvent = createTestRaceEvent({
status: 'awaiting_stewarding',
stewardingClosesAt: pastDate,
});
await raceEventRepository.create(raceEvent);
// When
await closeStewardingUseCase.execute({});
// Then
const updatedEvent = await raceEventRepository.findById(raceEvent.id);
expect(updatedEvent?.status).toBe('closed');
});
it('should not close unexpired stewarding windows', async () => {
// Given
const futureDate = new Date(Date.now() + 25 * 60 * 60 * 1000); // 25 hours from now
const raceEvent = createTestRaceEvent({
status: 'awaiting_stewarding',
stewardingClosesAt: futureDate,
});
await raceEventRepository.create(raceEvent);
// When
await closeStewardingUseCase.execute({});
// Then
const updatedEvent = await raceEventRepository.findById(raceEvent.id);
expect(updatedEvent?.status).toBe('awaiting_stewarding');
});
});
describe('Race Event Lifecycle', () => {
it('should transition from scheduled to in_progress when sessions start', () => {
// Given
const raceEvent = createTestRaceEvent({ status: 'scheduled' });
// When
const startedEvent = raceEvent.start();
// Then
expect(startedEvent.status).toBe('in_progress');
});
it('should transition to awaiting_stewarding when main race completes', () => {
// Given
const raceEvent = createTestRaceEvent({ status: 'in_progress' });
// When
const completedEvent = raceEvent.completeMainRace();
// Then
expect(completedEvent.status).toBe('awaiting_stewarding');
});
it('should transition to closed when stewarding closes', () => {
// Given
const raceEvent = createTestRaceEvent({ status: 'awaiting_stewarding' });
// When
const closedEvent = raceEvent.closeStewarding();
// Then
expect(closedEvent.status).toBe('closed');
});
});
describe('Session Type Behavior', () => {
it('should identify main race sessions correctly', () => {
// Given
const mainSession = createTestSession({ sessionType: SessionType.main() });
const practiceSession = createTestSession({ sessionType: SessionType.practice() });
// Then
expect(mainSession.countsForPoints()).toBe(true);
expect(practiceSession.countsForPoints()).toBe(false);
});
it('should identify qualifying sessions correctly', () => {
// Given
const qualiSession = createTestSession({ sessionType: SessionType.qualifying() });
const mainSession = createTestSession({ sessionType: SessionType.main() });
// Then
expect(qualiSession.determinesGrid()).toBe(true);
expect(mainSession.determinesGrid()).toBe(false);
});
});
});