import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile'; import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository'; import { ExternalRating } from '../../domain/value-objects/ExternalRating'; import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance'; import { GameKey } from '../../domain/value-objects/GameKey'; import { UserId } from '../../domain/value-objects/UserId'; import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto'; import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase'; describe('UpsertExternalGameRatingUseCase', () => { let useCase: UpsertExternalGameRatingUseCase; let mockRepository: { findByUserIdAndGameKey: Mock; findByUserId: Mock; findByGameKey: Mock; save: Mock; saveMany: Mock; delete: Mock; exists: Mock; }; beforeEach(() => { mockRepository = { findByUserIdAndGameKey: vi.fn(), findByUserId: vi.fn(), findByGameKey: vi.fn(), save: vi.fn(), saveMany: vi.fn(), delete: vi.fn(), exists: vi.fn(), } as unknown as { findByUserIdAndGameKey: Mock; findByUserId: Mock; findByGameKey: Mock; save: Mock; saveMany: Mock; delete: Mock; exists: Mock; }; useCase = new UpsertExternalGameRatingUseCase(mockRepository as unknown as ExternalGameRatingRepository); }); 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.mockResolvedValue(null); mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => 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.mockResolvedValue(existingProfile); mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => 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.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.mockResolvedValue(null); mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => 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.mockResolvedValue(null); mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => 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, }); } });