Files
gridpilot.gg/core/identity/application/use-cases/UpsertExternalGameRatingUseCase.integration.test.ts
2026-01-16 19:46:49 +01:00

268 lines
9.5 KiB
TypeScript

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<string, ExternalGameRatingProfile>();
private getKey(userId: string, gameKey: string): string {
return `${userId}|${gameKey}`;
}
async findByUserIdAndGameKey(userId: string, gameKey: string): Promise<ExternalGameRatingProfile | null> {
return this.profiles.get(this.getKey(userId, gameKey)) || null;
}
async findByUserId(userId: string): Promise<ExternalGameRatingProfile[]> {
return Array.from(this.profiles.values()).filter((p: ExternalGameRatingProfile) => p.userId.toString() === userId);
}
async findByGameKey(gameKey: string): Promise<ExternalGameRatingProfile[]> {
return Array.from(this.profiles.values()).filter((p: ExternalGameRatingProfile) => p.gameKey.toString() === gameKey);
}
async save(profile: ExternalGameRatingProfile): Promise<ExternalGameRatingProfile> {
const key = this.getKey(profile.userId.toString(), profile.gameKey.toString());
this.profiles.set(key, profile);
return profile;
}
async delete(userId: string, gameKey: string): Promise<boolean> {
return this.profiles.delete(this.getKey(userId, gameKey));
}
async exists(userId: string, gameKey: string): Promise<boolean> {
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);
});
});
});