import { beforeEach, describe, expect, it } from 'vitest'; import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile'; import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository'; import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto'; import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase'; // Mock repository for integration test class MockExternalGameRatingRepository { private profiles = new Map(); private getKey(userId: string, gameKey: string): string { return `${userId}|${gameKey}`; } async findByUserIdAndGameKey(userId: string, gameKey: string): Promise { return this.profiles.get(this.getKey(userId, gameKey)) || null; } async findByUserId(userId: string): Promise { return Array.from(this.profiles.values()).filter((p: ExternalGameRatingProfile) => p.userId.toString() === userId); } async findByGameKey(gameKey: string): Promise { return Array.from(this.profiles.values()).filter((p: ExternalGameRatingProfile) => p.gameKey.toString() === gameKey); } async save(profile: ExternalGameRatingProfile): Promise { const key = this.getKey(profile.userId.toString(), profile.gameKey.toString()); this.profiles.set(key, profile); return profile; } async delete(userId: string, gameKey: string): Promise { return this.profiles.delete(this.getKey(userId, gameKey)); } async exists(userId: string, gameKey: string): Promise { return this.profiles.has(this.getKey(userId, gameKey)); } clear(): void { this.profiles.clear(); } } /** * Integration test for UpsertExternalGameRatingUseCase * Tests the full flow from use case to repository */ describe('UpsertExternalGameRatingUseCase - Integration', () => { let useCase: UpsertExternalGameRatingUseCase; let repository: MockExternalGameRatingRepository; beforeEach(() => { repository = new MockExternalGameRatingRepository(); useCase = new UpsertExternalGameRatingUseCase(repository as unknown as ExternalGameRatingRepository); }); describe('Full upsert flow', () => { it('should create and then update a profile', async () => { // Step 1: Create new profile const createInput: 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: false, }, }; const createResult = await useCase.execute(createInput); expect(createResult.success).toBe(true); expect(createResult.action).toBe('created'); expect(createResult.profile.ratingCount).toBe(2); expect(createResult.profile.verified).toBe(false); // Verify it was saved const savedProfile = await repository.findByUserIdAndGameKey('user-123', 'iracing'); expect(savedProfile).not.toBeNull(); expect(savedProfile?.ratings.size).toBe(2); // Step 2: Update the profile const updateInput: UpsertExternalGameRatingInput = { userId: 'user-123', gameKey: 'iracing', ratings: [ { type: 'safety', value: 90.0 }, { type: 'skill', value: 95.0 }, { type: 'consistency', value: 88.0 }, ], provenance: { source: 'iracing', lastSyncedAt: '2024-01-02T00:00:00Z', verified: true, }, }; const updateResult = await useCase.execute(updateInput); expect(updateResult.success).toBe(true); expect(updateResult.action).toBe('updated'); expect(updateResult.profile.ratingCount).toBe(3); expect(updateResult.profile.verified).toBe(true); // Verify the update was persisted const updatedProfile = await repository.findByUserIdAndGameKey('user-123', 'iracing'); expect(updatedProfile).not.toBeNull(); expect(updatedProfile?.ratings.size).toBe(3); expect(updatedProfile?.getRatingByType('safety')?.value).toBe(90.0); expect(updatedProfile?.getRatingByType('consistency')).toBeDefined(); expect(updatedProfile?.provenance.verified).toBe(true); }); it('should handle multiple users and games', async () => { // Create profiles for different users/games const inputs: UpsertExternalGameRatingInput[] = [ { userId: 'user-1', gameKey: 'iracing', ratings: [{ type: 'safety', value: 80.0 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z' }, }, { userId: 'user-1', gameKey: 'assetto', ratings: [{ type: 'safety', value: 75.0 }], provenance: { source: 'assetto', lastSyncedAt: '2024-01-01T00:00:00Z' }, }, { userId: 'user-2', gameKey: 'iracing', ratings: [{ type: 'safety', value: 85.0 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z' }, }, ]; for (const input of inputs) { const result = await useCase.execute(input); expect(result.success).toBe(true); } // Verify user-1 has 2 profiles const user1Profiles = await repository.findByUserId('user-1'); expect(user1Profiles).toHaveLength(2); // Verify iracing has 2 profiles const iracingProfiles = await repository.findByGameKey('iracing'); expect(iracingProfiles).toHaveLength(2); // Verify specific profile const specific = await repository.findByUserIdAndGameKey('user-1', 'assetto'); expect(specific?.getRatingByType('safety')?.value).toBe(75.0); }); it('should handle concurrent updates to same profile', async () => { // Initial profile const input1: UpsertExternalGameRatingInput = { userId: 'user-1', gameKey: 'iracing', ratings: [{ type: 'safety', value: 80.0 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z' }, }; await useCase.execute(input1); // Update 1 const input2: UpsertExternalGameRatingInput = { userId: 'user-1', gameKey: 'iracing', ratings: [{ type: 'safety', value: 85.0 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-02T00:00:00Z' }, }; await useCase.execute(input2); // Update 2 (should overwrite) const input3: UpsertExternalGameRatingInput = { userId: 'user-1', gameKey: 'iracing', ratings: [{ type: 'safety', value: 90.0 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-03T00:00:00Z' }, }; await useCase.execute(input3); // Verify final state const profile = await repository.findByUserIdAndGameKey('user-1', 'iracing'); expect(profile?.getRatingByType('safety')?.value).toBe(90.0); expect(profile?.provenance.lastSyncedAt).toEqual(new Date('2024-01-03T00:00:00Z')); }); it('should handle complex rating updates', async () => { // Initial with 2 ratings const input1: UpsertExternalGameRatingInput = { userId: 'user-1', gameKey: 'iracing', ratings: [ { type: 'safety', value: 80.0 }, { type: 'skill', value: 75.0 }, ], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z' }, }; await useCase.execute(input1); // Update with different set const input2: UpsertExternalGameRatingInput = { userId: 'user-1', gameKey: 'iracing', ratings: [ { type: 'safety', value: 85.0 }, // Updated { type: 'consistency', value: 88.0 }, // New // skill removed ], provenance: { source: 'iracing', lastSyncedAt: '2024-01-02T00:00:00Z' }, }; const result = await useCase.execute(input2); expect(result.profile.ratingCount).toBe(2); expect(result.profile.ratingTypes).toEqual(['safety', 'consistency']); expect(result.profile.ratingTypes).not.toContain('skill'); const profile = await repository.findByUserIdAndGameKey('user-1', 'iracing'); expect(profile?.ratings.size).toBe(2); expect(profile?.getRatingByType('safety')?.value).toBe(85.0); expect(profile?.getRatingByType('consistency')?.value).toBe(88.0); expect(profile?.getRatingByType('skill')).toBeUndefined(); }); }); describe('Repository method integration', () => { it('should work with repository methods directly', async () => { // Create via use case const input: UpsertExternalGameRatingInput = { userId: 'user-1', gameKey: 'iracing', ratings: [{ type: 'safety', value: 80.0 }], provenance: { source: 'iracing', lastSyncedAt: '2024-01-01T00:00:00Z' }, }; await useCase.execute(input); // Test repository methods const exists = await repository.exists('user-1', 'iracing'); expect(exists).toBe(true); const allForUser = await repository.findByUserId('user-1'); expect(allForUser).toHaveLength(1); const allForGame = await repository.findByGameKey('iracing'); expect(allForGame).toHaveLength(1); // Delete const deleted = await repository.delete('user-1', 'iracing'); expect(deleted).toBe(true); const existsAfterDelete = await repository.exists('user-1', 'iracing'); expect(existsAfterDelete).toBe(false); }); }); });