rating
This commit is contained in:
301
core/identity/domain/services/RatingUpdateService.test.ts
Normal file
301
core/identity/domain/services/RatingUpdateService.test.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { RatingUpdateService } from './RatingUpdateService';
|
||||
import type { IUserRatingRepository } from '../repositories/IUserRatingRepository';
|
||||
import type { IRatingEventRepository } from '../repositories/IRatingEventRepository';
|
||||
import { UserRating } from '../value-objects/UserRating';
|
||||
import { RatingEvent } from '../entities/RatingEvent';
|
||||
import { RatingEventId } from '../value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../value-objects/RatingDelta';
|
||||
|
||||
describe('RatingUpdateService - Slice 7 Evolution', () => {
|
||||
let service: RatingUpdateService;
|
||||
let userRatingRepository: any;
|
||||
let ratingEventRepository: any;
|
||||
|
||||
beforeEach(() => {
|
||||
userRatingRepository = {
|
||||
findByUserId: vi.fn(),
|
||||
save: vi.fn(),
|
||||
};
|
||||
|
||||
ratingEventRepository = {
|
||||
save: vi.fn(),
|
||||
getAllByUserId: vi.fn(),
|
||||
};
|
||||
|
||||
service = new RatingUpdateService(userRatingRepository, 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: any) =>
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user