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); }); }); });