rating
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase';
|
||||
import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto';
|
||||
|
||||
// Mock repository for integration test
|
||||
class MockExternalGameRatingRepository {
|
||||
private profiles = new Map<string, any>();
|
||||
|
||||
private getKey(userId: string, gameKey: string): string {
|
||||
return `${userId}|${gameKey}`;
|
||||
}
|
||||
|
||||
async findByUserIdAndGameKey(userId: string, gameKey: string): Promise<any | null> {
|
||||
return this.profiles.get(this.getKey(userId, gameKey)) || null;
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<any[]> {
|
||||
return Array.from(this.profiles.values()).filter((p: any) => p.userId.toString() === userId);
|
||||
}
|
||||
|
||||
async findByGameKey(gameKey: string): Promise<any[]> {
|
||||
return Array.from(this.profiles.values()).filter((p: any) => p.gameKey.toString() === gameKey);
|
||||
}
|
||||
|
||||
async save(profile: any): Promise<any> {
|
||||
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 any);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user