377 lines
12 KiB
TypeScript
377 lines
12 KiB
TypeScript
import { RecordTeamRaceRatingEventsUseCase } from './RecordTeamRaceRatingEventsUseCase';
|
|
import { TeamRaceResultsProvider } from '../ports/TeamRaceResultsProvider';
|
|
import { TeamRatingEventRepository } from '@core/racing/domain/repositories/TeamRatingEventRepository';
|
|
import { TeamRatingRepository } from '@core/racing/domain/repositories/TeamRatingRepository';
|
|
import { AppendTeamRatingEventsUseCase } from './AppendTeamRatingEventsUseCase';
|
|
import { TeamDrivingRaceFactsDto } from '@core/racing/domain/services/TeamDrivingRatingEventFactory';
|
|
import { TeamRatingEvent } from '@core/racing/domain/entities/TeamRatingEvent';
|
|
import { TeamRatingEventId } from '@core/racing/domain/value-objects/TeamRatingEventId';
|
|
import { TeamRating } from '../../domain/value-objects/TeamRating';
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
|
// Mock repositories
|
|
class MockTeamRaceResultsProvider implements TeamRaceResultsProvider {
|
|
private results: TeamDrivingRaceFactsDto | null = null;
|
|
|
|
async getTeamRaceResults(): Promise<TeamDrivingRaceFactsDto | null> {
|
|
return this.results;
|
|
}
|
|
|
|
setResults(results: TeamDrivingRaceFactsDto | null) {
|
|
this.results = results;
|
|
}
|
|
}
|
|
|
|
class MockTeamRatingEventRepository implements TeamRatingEventRepository {
|
|
private events: TeamRatingEvent[] = [];
|
|
|
|
async save(event: TeamRatingEvent): Promise<TeamRatingEvent> {
|
|
this.events.push(event);
|
|
return event;
|
|
}
|
|
|
|
async findByTeamId(teamId: string): Promise<TeamRatingEvent[]> {
|
|
return this.events.filter(e => e.teamId === teamId);
|
|
}
|
|
|
|
async findByIds(ids: TeamRatingEventId[]): Promise<TeamRatingEvent[]> {
|
|
return this.events.filter(e => ids.some(id => id.equals(e.id)));
|
|
}
|
|
|
|
async getAllByTeamId(teamId: string): Promise<TeamRatingEvent[]> {
|
|
return this.events.filter(e => e.teamId === teamId);
|
|
}
|
|
|
|
async findEventsPaginated(teamId: string): Promise<PaginatedResult<TeamRatingEvent>> {
|
|
const events = await this.getAllByTeamId(teamId);
|
|
return {
|
|
items: events,
|
|
total: events.length,
|
|
limit: 10,
|
|
offset: 0,
|
|
hasMore: false,
|
|
};
|
|
}
|
|
|
|
clear() {
|
|
this.events = [];
|
|
}
|
|
}
|
|
|
|
class MockTeamRatingRepository implements TeamRatingRepository {
|
|
private snapshots: Map<string, TeamRating> = new Map();
|
|
|
|
async findByTeamId(teamId: string): Promise<TeamRating | null> {
|
|
return this.snapshots.get(teamId) || null;
|
|
}
|
|
|
|
async save(snapshot: TeamRating): Promise<TeamRating> {
|
|
this.snapshots.set(snapshot.teamId, snapshot);
|
|
return snapshot;
|
|
}
|
|
|
|
clear() {
|
|
this.snapshots.clear();
|
|
}
|
|
}
|
|
|
|
describe('RecordTeamRaceRatingEventsUseCase', () => {
|
|
let useCase: RecordTeamRaceRatingEventsUseCase;
|
|
let mockResultsProvider: MockTeamRaceResultsProvider;
|
|
let mockEventRepo: MockTeamRatingEventRepository;
|
|
let mockRatingRepo: MockTeamRatingRepository;
|
|
let appendUseCase: AppendTeamRatingEventsUseCase;
|
|
|
|
beforeEach(() => {
|
|
mockResultsProvider = new MockTeamRaceResultsProvider();
|
|
mockEventRepo = new MockTeamRatingEventRepository();
|
|
mockRatingRepo = new MockTeamRatingRepository();
|
|
appendUseCase = new AppendTeamRatingEventsUseCase(mockEventRepo, mockRatingRepo);
|
|
useCase = new RecordTeamRaceRatingEventsUseCase(
|
|
mockResultsProvider,
|
|
mockEventRepo,
|
|
mockRatingRepo,
|
|
appendUseCase
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
mockEventRepo.clear();
|
|
mockRatingRepo.clear();
|
|
});
|
|
|
|
describe('execute', () => {
|
|
it('should return error when race results not found', async () => {
|
|
mockResultsProvider.setResults(null);
|
|
|
|
const result = await useCase.execute({ raceId: 'race-123' });
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Team race results not found');
|
|
expect(result.eventsCreated).toBe(0);
|
|
expect(result.teamsUpdated).toEqual([]);
|
|
});
|
|
|
|
it('should return success with no events when results are empty', async () => {
|
|
mockResultsProvider.setResults({
|
|
raceId: 'race-123',
|
|
teamId: 'team-123',
|
|
results: [],
|
|
});
|
|
|
|
const result = await useCase.execute({ raceId: 'race-123' });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.eventsCreated).toBe(0);
|
|
expect(result.teamsUpdated).toEqual([]);
|
|
expect(result.errors).toEqual([]);
|
|
});
|
|
|
|
it('should create events for single team and update snapshot', async () => {
|
|
const raceResults: TeamDrivingRaceFactsDto = {
|
|
raceId: 'race-123',
|
|
teamId: 'team-123',
|
|
results: [
|
|
{
|
|
teamId: 'team-123',
|
|
position: 1,
|
|
incidents: 0,
|
|
status: 'finished',
|
|
fieldSize: 3,
|
|
strengthOfField: 55,
|
|
},
|
|
],
|
|
};
|
|
|
|
mockResultsProvider.setResults(raceResults);
|
|
|
|
const result = await useCase.execute({ raceId: 'race-123' });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.eventsCreated).toBeGreaterThan(0);
|
|
expect(result.teamsUpdated).toContain('team-123');
|
|
expect(result.errors).toEqual([]);
|
|
|
|
// Verify events were saved
|
|
const savedEvents = await mockEventRepo.getAllByTeamId('team-123');
|
|
expect(savedEvents.length).toBeGreaterThan(0);
|
|
|
|
// Verify snapshot was updated
|
|
const snapshot = await mockRatingRepo.findByTeamId('team-123');
|
|
expect(snapshot).toBeDefined();
|
|
expect(snapshot.teamId).toBe('team-123');
|
|
});
|
|
|
|
it('should create events for multiple teams', async () => {
|
|
const raceResults: TeamDrivingRaceFactsDto = {
|
|
raceId: 'race-123',
|
|
teamId: 'team-123',
|
|
results: [
|
|
{
|
|
teamId: 'team-123',
|
|
position: 1,
|
|
incidents: 0,
|
|
status: 'finished',
|
|
fieldSize: 3,
|
|
strengthOfField: 55,
|
|
},
|
|
{
|
|
teamId: 'team-456',
|
|
position: 2,
|
|
incidents: 1,
|
|
status: 'finished',
|
|
fieldSize: 3,
|
|
strengthOfField: 55,
|
|
},
|
|
{
|
|
teamId: 'team-789',
|
|
position: 3,
|
|
incidents: 0,
|
|
status: 'dnf',
|
|
fieldSize: 3,
|
|
strengthOfField: 55,
|
|
},
|
|
],
|
|
};
|
|
|
|
mockResultsProvider.setResults(raceResults);
|
|
|
|
const result = await useCase.execute({ raceId: 'race-123' });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.eventsCreated).toBeGreaterThan(0);
|
|
expect(result.teamsUpdated).toContain('team-123');
|
|
expect(result.teamsUpdated).toContain('team-456');
|
|
expect(result.teamsUpdated).toContain('team-789');
|
|
expect(result.errors).toEqual([]);
|
|
|
|
// Verify all team snapshots were updated
|
|
const snapshot1 = await mockRatingRepo.findByTeamId('team-123');
|
|
const snapshot2 = await mockRatingRepo.findByTeamId('team-456');
|
|
const snapshot3 = await mockRatingRepo.findByTeamId('team-789');
|
|
|
|
expect(snapshot1).toBeDefined();
|
|
expect(snapshot2).toBeDefined();
|
|
expect(snapshot3).toBeDefined();
|
|
});
|
|
|
|
it('should handle optional ratings in results', async () => {
|
|
const raceResults: TeamDrivingRaceFactsDto = {
|
|
raceId: 'race-123',
|
|
teamId: 'team-123',
|
|
results: [
|
|
{
|
|
teamId: 'team-123',
|
|
position: 1,
|
|
incidents: 0,
|
|
status: 'finished',
|
|
fieldSize: 3,
|
|
strengthOfField: 65,
|
|
pace: 85,
|
|
consistency: 80,
|
|
teamwork: 90,
|
|
sportsmanship: 95,
|
|
},
|
|
],
|
|
};
|
|
|
|
mockResultsProvider.setResults(raceResults);
|
|
|
|
const result = await useCase.execute({ raceId: 'race-123' });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.eventsCreated).toBeGreaterThan(5); // Should have many events
|
|
expect(result.teamsUpdated).toContain('team-123');
|
|
|
|
// Verify events include optional rating events
|
|
const savedEvents = await mockEventRepo.getAllByTeamId('team-123');
|
|
const paceEvent = savedEvents.find(e => e.reason.code === 'RACE_PACE');
|
|
const consistencyEvent = savedEvents.find(e => e.reason.code === 'RACE_CONSISTENCY');
|
|
const teamworkEvent = savedEvents.find(e => e.reason.code === 'RACE_TEAMWORK');
|
|
const sportsmanshipEvent = savedEvents.find(e => e.reason.code === 'RACE_SPORTSMANSHIP');
|
|
|
|
expect(paceEvent).toBeDefined();
|
|
expect(consistencyEvent).toBeDefined();
|
|
expect(teamworkEvent).toBeDefined();
|
|
expect(sportsmanshipEvent).toBeDefined();
|
|
});
|
|
|
|
it('should handle partial failures gracefully', async () => {
|
|
const raceResults: TeamDrivingRaceFactsDto = {
|
|
raceId: 'race-123',
|
|
teamId: 'team-123',
|
|
results: [
|
|
{
|
|
teamId: 'team-123',
|
|
position: 1,
|
|
incidents: 0,
|
|
status: 'finished',
|
|
fieldSize: 2,
|
|
strengthOfField: 55,
|
|
},
|
|
{
|
|
teamId: 'team-456',
|
|
position: 2,
|
|
incidents: 0,
|
|
status: 'finished',
|
|
fieldSize: 2,
|
|
strengthOfField: 55,
|
|
},
|
|
],
|
|
};
|
|
|
|
mockResultsProvider.setResults(raceResults);
|
|
|
|
// Mock the append use case to fail for team-456
|
|
const originalExecute = appendUseCase.execute.bind(appendUseCase);
|
|
appendUseCase.execute = async (events) => {
|
|
if (events.length > 0 && events[0] && events[0].teamId === 'team-456') {
|
|
throw new Error('Simulated failure for team-456');
|
|
}
|
|
return originalExecute(events);
|
|
};
|
|
|
|
const result = await useCase.execute({ raceId: 'race-123' });
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.teamsUpdated).toContain('team-123');
|
|
expect(result.teamsUpdated).not.toContain('team-456');
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
expect(result.errors[0]).toContain('team-456');
|
|
});
|
|
|
|
it('should handle repository errors', async () => {
|
|
const raceResults: TeamDrivingRaceFactsDto = {
|
|
raceId: 'race-123',
|
|
teamId: 'team-123',
|
|
results: [
|
|
{
|
|
teamId: 'team-123',
|
|
position: 1,
|
|
incidents: 0,
|
|
status: 'finished',
|
|
fieldSize: 1,
|
|
strengthOfField: 55,
|
|
},
|
|
],
|
|
};
|
|
|
|
mockResultsProvider.setResults(raceResults);
|
|
|
|
// Mock repository to throw error
|
|
mockEventRepo.save = async () => {
|
|
throw new Error('Repository error');
|
|
};
|
|
|
|
const result = await useCase.execute({ raceId: 'race-123' });
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
expect(result.errors[0]).toContain('Repository error');
|
|
expect(result.eventsCreated).toBe(0);
|
|
expect(result.teamsUpdated).toEqual([]);
|
|
});
|
|
|
|
it('should handle empty results array', async () => {
|
|
mockResultsProvider.setResults({
|
|
raceId: 'race-123',
|
|
teamId: 'team-123',
|
|
results: [],
|
|
});
|
|
|
|
const result = await useCase.execute({ raceId: 'race-123' });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.eventsCreated).toBe(0);
|
|
expect(result.teamsUpdated).toEqual([]);
|
|
expect(result.errors).toEqual([]);
|
|
});
|
|
|
|
it('should handle race with minimal events generated', async () => {
|
|
// Race where teams have some impact (DNS creates penalty event)
|
|
const raceResults: TeamDrivingRaceFactsDto = {
|
|
raceId: 'race-123',
|
|
teamId: 'team-123',
|
|
results: [
|
|
{
|
|
teamId: 'team-123',
|
|
position: 1,
|
|
incidents: 0,
|
|
status: 'dns',
|
|
fieldSize: 1,
|
|
strengthOfField: 50,
|
|
},
|
|
],
|
|
};
|
|
|
|
mockResultsProvider.setResults(raceResults);
|
|
|
|
const result = await useCase.execute({ raceId: 'race-123' });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.eventsCreated).toBeGreaterThan(0); // DNS creates penalty event
|
|
expect(result.teamsUpdated).toContain('team-123');
|
|
expect(result.errors).toEqual([]);
|
|
});
|
|
});
|
|
}); |