Files
gridpilot.gg/core/identity/application/use-cases/RecordRaceRatingEventsUseCase.test.ts
2025-12-29 22:27:33 +01:00

360 lines
12 KiB
TypeScript

import { RecordRaceRatingEventsUseCase } from './RecordRaceRatingEventsUseCase';
import { IRaceResultsProvider, RaceResultsData } from '../ports/IRaceResultsProvider';
import { IRatingEventRepository } from '../../domain/repositories/IRatingEventRepository';
import { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository';
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
import { UserRating } from '../../domain/value-objects/UserRating';
import { RatingEvent } from '../../domain/entities/RatingEvent';
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
// Mock implementations
class MockRaceResultsProvider implements IRaceResultsProvider {
private results: RaceResultsData | null = null;
setResults(results: RaceResultsData | null) {
this.results = results;
}
async getRaceResults(raceId: string): Promise<RaceResultsData | null> {
return this.results;
}
async hasRaceResults(raceId: string): Promise<boolean> {
return this.results !== null;
}
}
class MockRatingEventRepository implements IRatingEventRepository {
private events: RatingEvent[] = [];
async save(event: RatingEvent): Promise<RatingEvent> {
this.events.push(event);
return event;
}
async findByUserId(userId: string): Promise<RatingEvent[]> {
return this.events.filter(e => e.userId === userId);
}
async findByIds(ids: RatingEventId[]): Promise<RatingEvent[]> {
return this.events.filter(e => ids.some(id => id.equals(e.id)));
}
async getAllByUserId(userId: string): Promise<RatingEvent[]> {
return this.events.filter(e => e.userId === userId);
}
async findEventsPaginated(userId: string, options?: import('@core/identity/domain/repositories/IRatingEventRepository').PaginatedQueryOptions): Promise<import('@core/identity/domain/repositories/IRatingEventRepository').PaginatedResult<RatingEvent>> {
const allEvents = await this.findByUserId(userId);
// Apply filters
let filtered = allEvents;
if (options?.filter) {
const filter = options.filter;
if (filter.dimensions) {
filtered = filtered.filter(e => filter.dimensions!.includes(e.dimension.value));
}
if (filter.sourceTypes) {
filtered = filtered.filter(e => filter.sourceTypes!.includes(e.source.type));
}
if (filter.from) {
filtered = filtered.filter(e => e.occurredAt >= filter.from!);
}
if (filter.to) {
filtered = filtered.filter(e => e.occurredAt <= filter.to!);
}
if (filter.reasonCodes) {
filtered = filtered.filter(e => filter.reasonCodes!.includes(e.reason.code));
}
if (filter.visibility) {
filtered = filtered.filter(e => e.visibility.public === (filter.visibility === 'public'));
}
}
const total = filtered.length;
const limit = options?.limit ?? 10;
const offset = options?.offset ?? 0;
const items = filtered.slice(offset, offset + limit);
const hasMore = offset + limit < total;
const nextOffset = hasMore ? offset + limit : undefined;
const result: import('@core/identity/domain/repositories/IRatingEventRepository').PaginatedResult<RatingEvent> = {
items,
total,
limit,
offset,
hasMore
};
if (nextOffset !== undefined) {
result.nextOffset = nextOffset;
}
return result;
}
}
class MockUserRatingRepository implements IUserRatingRepository {
private ratings: Map<string, UserRating> = new Map();
async findByUserId(userId: string): Promise<UserRating | null> {
return this.ratings.get(userId) || null;
}
async save(userRating: UserRating): Promise<UserRating> {
this.ratings.set(userRating.userId, userRating);
return userRating;
}
// Helper for tests
setRating(userId: string, rating: UserRating) {
this.ratings.set(userId, rating);
}
}
describe('RecordRaceRatingEventsUseCase', () => {
let useCase: RecordRaceRatingEventsUseCase;
let mockRaceResultsProvider: MockRaceResultsProvider;
let mockRatingEventRepository: MockRatingEventRepository;
let mockUserRatingRepository: MockUserRatingRepository;
let appendRatingEventsUseCase: AppendRatingEventsUseCase;
beforeEach(() => {
mockRaceResultsProvider = new MockRaceResultsProvider();
mockRatingEventRepository = new MockRatingEventRepository();
mockUserRatingRepository = new MockUserRatingRepository();
appendRatingEventsUseCase = new AppendRatingEventsUseCase(
mockRatingEventRepository,
mockUserRatingRepository
);
useCase = new RecordRaceRatingEventsUseCase(
mockRaceResultsProvider,
mockRatingEventRepository,
mockUserRatingRepository,
appendRatingEventsUseCase
);
});
describe('execute', () => {
it('should return error when race results not found', async () => {
mockRaceResultsProvider.setResults(null);
const result = await useCase.execute({ raceId: 'race-123' });
expect(result.success).toBe(false);
expect(result.raceId).toBe('race-123');
expect(result.eventsCreated).toBe(0);
expect(result.driversUpdated).toEqual([]);
expect(result.errors).toContain('Race results not found');
});
it('should return error when no results in race', async () => {
mockRaceResultsProvider.setResults({
raceId: 'race-123',
results: [],
});
const result = await useCase.execute({ raceId: 'race-123' });
expect(result.success).toBe(false);
expect(result.eventsCreated).toBe(0);
expect(result.errors).toContain('No results found for race');
});
it('should process single driver with good performance', async () => {
mockRaceResultsProvider.setResults({
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 2,
incidents: 0,
status: 'finished',
sof: 2500,
},
],
});
// Set initial rating for user
const initialRating = UserRating.create('user-123');
mockUserRatingRepository.setRating('user-123', initialRating);
const result = await useCase.execute({ raceId: 'race-123' });
expect(result.success).toBe(true);
expect(result.raceId).toBe('race-123');
expect(result.eventsCreated).toBeGreaterThan(0);
expect(result.driversUpdated).toContain('user-123');
expect(result.errors).toEqual([]);
});
it('should process multiple drivers with mixed results', async () => {
mockRaceResultsProvider.setResults({
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 2,
incidents: 0,
status: 'finished',
sof: 2500,
},
{
userId: 'user-456',
startPos: 3,
finishPos: 8,
incidents: 2,
status: 'finished',
sof: 2500,
},
{
userId: 'user-789',
startPos: 5,
finishPos: 5,
incidents: 0,
status: 'dns',
sof: 2500,
},
],
});
// Set initial ratings
mockUserRatingRepository.setRating('user-123', UserRating.create('user-123'));
mockUserRatingRepository.setRating('user-456', UserRating.create('user-456'));
mockUserRatingRepository.setRating('user-789', UserRating.create('user-789'));
const result = await useCase.execute({ raceId: 'race-123' });
expect(result.success).toBe(true);
expect(result.eventsCreated).toBeGreaterThan(0);
expect(result.driversUpdated.length).toBe(3);
expect(result.driversUpdated).toContain('user-123');
expect(result.driversUpdated).toContain('user-456');
expect(result.driversUpdated).toContain('user-789');
expect(result.errors).toEqual([]);
});
it('should compute SoF if not provided', async () => {
mockRaceResultsProvider.setResults({
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 2,
incidents: 0,
status: 'finished',
// No sof
},
{
userId: 'user-456',
startPos: 3,
finishPos: 8,
incidents: 0,
status: 'finished',
// No sof
},
],
});
// Set ratings for SoF calculation
const rating1 = UserRating.create('user-123');
const rating2 = UserRating.create('user-456');
// Update driver ratings to specific values
mockUserRatingRepository.setRating('user-123', rating1.updateDriverRating(60));
mockUserRatingRepository.setRating('user-456', rating2.updateDriverRating(40));
const result = await useCase.execute({ raceId: 'race-123' });
expect(result.success).toBe(true);
expect(result.eventsCreated).toBeGreaterThan(0);
expect(result.driversUpdated.length).toBe(2);
});
it('should handle errors for individual drivers gracefully', async () => {
mockRaceResultsProvider.setResults({
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 2,
incidents: 0,
status: 'finished',
sof: 2500,
},
{
userId: 'user-456',
startPos: 3,
finishPos: 8,
incidents: 0,
status: 'finished',
sof: 2500,
},
],
});
// Only set rating for first user, second will fail
mockUserRatingRepository.setRating('user-123', UserRating.create('user-123'));
const result = await useCase.execute({ raceId: 'race-123' });
// Should still succeed overall but with errors
expect(result.raceId).toBe('race-123');
expect(result.driversUpdated).toContain('user-123');
expect(result.errors).toBeDefined();
expect(result.errors!.length).toBeGreaterThan(0);
});
it('should return success with no events when no valid events created', async () => {
// This would require a scenario where factory creates no events
// For now, we'll test with empty results
mockRaceResultsProvider.setResults({
raceId: 'race-123',
results: [],
});
const result = await useCase.execute({ raceId: 'race-123' });
expect(result.success).toBe(false); // No results
});
it('should handle repository errors', async () => {
mockRaceResultsProvider.setResults({
raceId: 'race-123',
results: [
{
userId: 'user-123',
startPos: 5,
finishPos: 2,
incidents: 0,
status: 'finished',
sof: 2500,
},
],
});
mockUserRatingRepository.setRating('user-123', UserRating.create('user-123'));
// Mock repository to throw error
const originalSave = mockRatingEventRepository.save;
mockRatingEventRepository.save = async () => {
throw new Error('Repository error');
};
const result = await useCase.execute({ raceId: 'race-123' });
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors!.length).toBeGreaterThan(0);
// Restore
mockRatingEventRepository.save = originalSave;
});
});
});