Files
gridpilot.gg/core/identity/domain/services/RatingUpdateService.test.ts
2026-01-16 19:46:49 +01:00

304 lines
11 KiB
TypeScript

import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import { RatingEvent } from '../entities/RatingEvent';
import type { RatingEventRepository } from '../repositories/RatingEventRepository';
import type { UserRatingRepository } from '../repositories/UserRatingRepository';
import { RatingDelta } from '../value-objects/RatingDelta';
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
import { RatingEventId } from '../value-objects/RatingEventId';
import { UserRating } from '../value-objects/UserRating';
import { RatingUpdateService } from './RatingUpdateService';
describe('RatingUpdateService - Slice 7 Evolution', () => {
let service: RatingUpdateService;
let userRatingRepository: { findByUserId: Mock; save: Mock };
let ratingEventRepository: { save: Mock; getAllByUserId: Mock };
beforeEach(() => {
userRatingRepository = {
findByUserId: vi.fn(),
save: vi.fn(),
};
ratingEventRepository = {
save: vi.fn(),
getAllByUserId: vi.fn(),
};
service = new RatingUpdateService(
userRatingRepository as unknown as UserRatingRepository,
ratingEventRepository as unknown as RatingEventRepository
);
});
describe('recordRaceRatingEvents - Ledger-based approach', () => {
it('should record race events and update snapshots', async () => {
const raceId = 'race-123';
const raceResults = [
{
userId: 'driver-1',
startPos: 5,
finishPos: 2,
incidents: 1,
status: 'finished' as const,
},
{
userId: 'driver-2',
startPos: 3,
finishPos: 1,
incidents: 0,
status: 'finished' as const,
},
];
// Mock repositories
ratingEventRepository.save.mockImplementation((event: RatingEvent) => Promise.resolve(event));
ratingEventRepository.getAllByUserId.mockImplementation((userId: string) => {
// Return mock events based on user
if (userId === 'driver-1') {
return Promise.resolve([
RatingEvent.create({
id: RatingEventId.generate(),
userId: 'driver-1',
dimension: RatingDimensionKey.create('driving'),
delta: RatingDelta.create(15),
weight: 1,
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'race', id: raceId },
reason: { code: 'DRIVING_FINISH_STRENGTH_GAIN', summary: 'Test', details: {} },
visibility: { public: true, redactedFields: [] },
version: 1,
}),
]);
}
return Promise.resolve([
RatingEvent.create({
id: RatingEventId.generate(),
userId: 'driver-2',
dimension: RatingDimensionKey.create('driving'),
delta: RatingDelta.create(20),
weight: 1,
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'race', id: raceId },
reason: { code: 'DRIVING_FINISH_STRENGTH_GAIN', summary: 'Test', details: {} },
visibility: { public: true, redactedFields: [] },
version: 1,
}),
]);
});
userRatingRepository.save.mockImplementation((rating: UserRating) => Promise.resolve(rating));
const result = await service.recordRaceRatingEvents(raceId, raceResults);
expect(result.success).toBe(true);
expect(result.eventsCreated).toBeGreaterThan(0);
expect(result.driversUpdated).toEqual(['driver-1', 'driver-2']);
// Verify events were saved
expect(ratingEventRepository.save).toHaveBeenCalled();
// Verify snapshots were recomputed
expect(ratingEventRepository.getAllByUserId).toHaveBeenCalledWith('driver-1');
expect(ratingEventRepository.getAllByUserId).toHaveBeenCalledWith('driver-2');
expect(userRatingRepository.save).toHaveBeenCalledTimes(2);
});
it('should handle empty race results gracefully', async () => {
const result = await service.recordRaceRatingEvents('race-123', []);
expect(result.success).toBe(true);
expect(result.eventsCreated).toBe(0);
expect(result.driversUpdated).toEqual([]);
});
it('should return failure on repository errors', async () => {
ratingEventRepository.save.mockRejectedValue(new Error('Database error'));
const result = await service.recordRaceRatingEvents('race-123', [
{
userId: 'driver-1',
startPos: 5,
finishPos: 2,
incidents: 1,
status: 'finished' as const,
},
]);
expect(result.success).toBe(false);
expect(result.eventsCreated).toBe(0);
expect(result.driversUpdated).toEqual([]);
});
it('should handle DNF status correctly', async () => {
const raceResults = [
{
userId: 'driver-1',
startPos: 5,
finishPos: 10,
incidents: 2,
status: 'dnf' as const,
},
];
ratingEventRepository.save.mockImplementation((event: RatingEvent) => Promise.resolve(event));
ratingEventRepository.getAllByUserId.mockResolvedValue([]);
userRatingRepository.save.mockImplementation((rating: UserRating) => Promise.resolve(rating));
const result = await service.recordRaceRatingEvents('race-123', raceResults);
expect(result.success).toBe(true);
expect(result.eventsCreated).toBeGreaterThan(0);
// Verify DNF penalty event was created
const savedEvents = ratingEventRepository.save.mock.calls.map(call => call[0]);
const hasDnfPenalty = savedEvents.some((event: RatingEvent) =>
event.reason.code === 'DRIVING_DNF_PENALTY'
);
expect(hasDnfPenalty).toBe(true);
});
});
describe('updateDriverRatingsAfterRace - Backward compatibility', () => {
it('should delegate to new ledger-based approach', async () => {
const driverResults = [
{
driverId: 'driver-1',
position: 2,
totalDrivers: 10,
incidents: 1,
startPosition: 5,
},
];
// Mock the new method to succeed
const recordSpy = vi.spyOn(service, 'recordRaceRatingEvents').mockResolvedValue({
success: true,
eventsCreated: 2,
driversUpdated: ['driver-1'],
});
await service.updateDriverRatingsAfterRace(driverResults);
expect(recordSpy).toHaveBeenCalled();
recordSpy.mockRestore();
});
it('should throw error when ledger approach fails', async () => {
const driverResults = [
{
driverId: 'driver-1',
position: 2,
totalDrivers: 10,
incidents: 1,
startPosition: 5,
},
];
// Mock the new method to fail
const recordSpy = vi.spyOn(service, 'recordRaceRatingEvents').mockResolvedValue({
success: false,
eventsCreated: 0,
driversUpdated: [],
});
await expect(service.updateDriverRatingsAfterRace(driverResults)).rejects.toThrow(
'Failed to update ratings via event system'
);
recordSpy.mockRestore();
});
});
describe('updateTrustScore - Ledger-based', () => {
it('should create trust event and update snapshot', async () => {
const userId = 'user-1';
const trustChange = 10;
ratingEventRepository.save.mockImplementation((event: RatingEvent) => Promise.resolve(event));
ratingEventRepository.getAllByUserId.mockResolvedValue([
RatingEvent.create({
id: RatingEventId.generate(),
userId: userId,
dimension: RatingDimensionKey.create('adminTrust'),
delta: RatingDelta.create(10),
weight: 1,
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'manualAdjustment', id: 'trust-test' },
reason: { code: 'TRUST_BONUS', summary: 'Test', details: {} },
visibility: { public: true, redactedFields: [] },
version: 1,
}),
]);
userRatingRepository.save.mockImplementation((rating: UserRating) => Promise.resolve(rating));
await service.updateTrustScore(userId, trustChange);
expect(ratingEventRepository.save).toHaveBeenCalled();
expect(userRatingRepository.save).toHaveBeenCalled();
});
it('should handle negative trust changes', async () => {
const userId = 'user-1';
const trustChange = -5;
ratingEventRepository.save.mockImplementation((event: RatingEvent) => Promise.resolve(event));
ratingEventRepository.getAllByUserId.mockResolvedValue([]);
userRatingRepository.save.mockImplementation((rating: UserRating) => Promise.resolve(rating));
await service.updateTrustScore(userId, trustChange);
const savedEvent = ratingEventRepository.save.mock.calls[0][0];
expect(savedEvent.reason.code).toBe('TRUST_PENALTY');
expect(savedEvent.delta.value).toBe(-5);
});
});
describe('updateStewardRating - Ledger-based', () => {
it('should create steward event and update snapshot', async () => {
const stewardId = 'steward-1';
const ratingChange = 8;
ratingEventRepository.save.mockImplementation((event: RatingEvent) => Promise.resolve(event));
ratingEventRepository.getAllByUserId.mockResolvedValue([
RatingEvent.create({
id: RatingEventId.generate(),
userId: stewardId,
dimension: RatingDimensionKey.create('adminTrust'),
delta: RatingDelta.create(8),
weight: 1,
occurredAt: new Date(),
createdAt: new Date(),
source: { type: 'manualAdjustment', id: 'steward-test' },
reason: { code: 'STEWARD_BONUS', summary: 'Test', details: {} },
visibility: { public: true, redactedFields: [] },
version: 1,
}),
]);
userRatingRepository.save.mockImplementation((rating: UserRating) => Promise.resolve(rating));
await service.updateStewardRating(stewardId, ratingChange);
expect(ratingEventRepository.save).toHaveBeenCalled();
expect(userRatingRepository.save).toHaveBeenCalled();
});
it('should handle negative steward rating changes', async () => {
const stewardId = 'steward-1';
const ratingChange = -3;
ratingEventRepository.save.mockImplementation((event: RatingEvent) => Promise.resolve(event));
ratingEventRepository.getAllByUserId.mockResolvedValue([]);
userRatingRepository.save.mockImplementation((rating: UserRating) => Promise.resolve(rating));
await service.updateStewardRating(stewardId, ratingChange);
const savedEvent = ratingEventRepository.save.mock.calls[0][0];
expect(savedEvent.reason.code).toBe('STEWARD_PENALTY');
expect(savedEvent.delta.value).toBe(-3);
});
});
});