268 lines
9.5 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |