import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase'; import { IExternalGameRatingRepository } from '../../domain/repositories/IExternalGameRatingRepository'; import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile'; import { UserId } from '../../domain/value-objects/UserId'; import { GameKey } from '../../domain/value-objects/GameKey'; import { ExternalRating } from '../../domain/value-objects/ExternalRating'; import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance'; import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto'; import { vi, describe, it, expect, beforeEach } from 'vitest'; describe('UpsertExternalGameRatingUseCase', () => { let useCase: UpsertExternalGameRatingUseCase; let mockRepository: IExternalGameRatingRepository; beforeEach(() => { mockRepository = { findByUserIdAndGameKey: vi.fn(), findByUserId: vi.fn(), findByGameKey: vi.fn(), save: vi.fn(), saveMany: vi.fn(), delete: vi.fn(), exists: vi.fn(), } as any; useCase = new UpsertExternalGameRatingUseCase(mockRepository); }); describe('execute', () => { it('should create a new profile when it does not exist', async () => { const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [ { type: 'safety', value: 85.5 }, { type: 'skill', value: 92.0 }, ], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z', verified: true, }, }; (mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(null); (mockRepository.save as any).mockImplementation(async (profile: any) => profile); const result = await useCase.execute(input); expect(result.success).toBe(true); expect(result.action).toBe('created'); expect(result.profile.userId).toBe('user-123'); expect(result.profile.gameKey).toBe('iracing'); expect(result.profile.ratingCount).toBe(2); expect(result.profile.ratingTypes).toEqual(['safety', 'skill']); expect(result.profile.verified).toBe(true); expect(mockRepository.findByUserIdAndGameKey).toHaveBeenCalledWith('user-123', 'iracing'); expect(mockRepository.save).toHaveBeenCalledTimes(1); }); it('should update existing profile', async () => { const existingProfile = createTestProfile('user-123', 'iracing'); (mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(existingProfile); (mockRepository.save as any).mockImplementation(async (profile: any) => profile); const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [ { type: 'safety', value: 90.0 }, { type: 'newType', value: 88.0 }, ], provenance: { source: 'iracing', lastSyncedAt: '2024-01-02T00:00:00Z', verified: false, }, }; const result = await useCase.execute(input); expect(result.success).toBe(true); expect(result.action).toBe('updated'); expect(result.profile.ratingCount).toBe(2); expect(result.profile.ratingTypes).toEqual(['safety', 'newType']); expect(result.profile.verified).toBe(false); expect(mockRepository.findByUserIdAndGameKey).toHaveBeenCalledWith('user-123', 'iracing'); expect(mockRepository.save).toHaveBeenCalledTimes(1); }); it('should handle validation errors - missing userId', async () => { const input: UpsertExternalGameRatingInput = { userId: '', gameKey: 'iracing', ratings: [{ type: 'safety', value: 85.5 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z', }, }; const result = await useCase.execute(input); expect(result.success).toBe(false); expect(result.errors).toContain('User ID is required'); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('should handle validation errors - missing gameKey', async () => { const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: '', ratings: [{ type: 'safety', value: 85.5 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z', }, }; const result = await useCase.execute(input); expect(result.success).toBe(false); expect(result.errors).toContain('Game key is required'); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('should handle validation errors - empty ratings', async () => { const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z', }, }; const result = await useCase.execute(input); expect(result.success).toBe(false); expect(result.errors).toContain('Ratings are required'); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('should handle validation errors - invalid rating value', async () => { const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [ { type: 'safety', value: NaN }, ], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z', }, }; const result = await useCase.execute(input); expect(result.success).toBe(false); expect(result.errors).toContain('Rating value for type safety must be a valid number'); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('should handle validation errors - missing provenance source', async () => { const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [{ type: 'safety', value: 85.5 }], provenance: { source: '', lastSyncedAt: '2024-01-01T00:00:00Z', }, }; const result = await useCase.execute(input); expect(result.success).toBe(false); expect(result.errors).toContain('Provenance source is required'); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('should handle validation errors - invalid date', async () => { const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [{ type: 'safety', value: 85.5 }], provenance: { source: 'iracing', lastSyncedAt: 'invalid-date', }, }; const result = await useCase.execute(input); expect(result.success).toBe(false); expect(result.errors).toContain('Provenance lastSyncedAt must be a valid date'); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('should handle repository errors gracefully', async () => { const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [{ type: 'safety', value: 85.5 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z', }, }; (mockRepository.findByUserIdAndGameKey as any).mockRejectedValue(new Error('Database connection failed')); const result = await useCase.execute(input); expect(result.success).toBe(false); expect(result.errors).toContain('Database connection failed'); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('should default verified to false when not provided', async () => { const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [{ type: 'safety', value: 85.5 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z', // verified not provided }, }; (mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(null); (mockRepository.save as any).mockImplementation(async (profile: any) => profile); const result = await useCase.execute(input); expect(result.success).toBe(true); expect(result.profile.verified).toBe(false); }); it('should trim rating types', async () => { const input: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [ { type: ' safety ', value: 85.5 }, ], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z', }, }; (mockRepository.findByUserIdAndGameKey as any).mockResolvedValue(null); (mockRepository.save as any).mockImplementation(async (profile: any) => profile); const result = await useCase.execute(input); expect(result.success).toBe(true); expect(result.profile.ratingTypes).toEqual(['safety']); }); }); function createTestProfile(userId: string, gameKey: string): ExternalGameRatingProfile { const user = UserId.fromString(userId); const game = GameKey.create(gameKey); const ratings = new Map([ ['safety', ExternalRating.create(game, 'safety', 85.5)], ]); const provenance = ExternalRatingProvenance.create({ source: gameKey, lastSyncedAt: new Date('2024-01-01'), verified: false, }); return ExternalGameRatingProfile.create({ userId: user, gameKey: game, ratings, provenance, }); } });