177 lines
7.3 KiB
TypeScript
177 lines
7.3 KiB
TypeScript
/**
|
|
* Tests for GetUserRatingsSummaryQuery
|
|
*/
|
|
|
|
import { describe, expect, it, beforeEach, vi, type Mock } from 'vitest';
|
|
import { GetUserRatingsSummaryQuery, GetUserRatingsSummaryQueryHandler } from './GetUserRatingsSummaryQuery';
|
|
import { UserRating } from '../../domain/value-objects/UserRating';
|
|
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
|
|
import { GameKey } from '../../domain/value-objects/GameKey';
|
|
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
|
|
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
|
|
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
|
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
|
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
|
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
|
|
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
|
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
|
|
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
|
|
|
import { UserId } from '../../domain/value-objects/UserId';
|
|
|
|
describe('GetUserRatingsSummaryQuery', () => {
|
|
let mockUserRatingRepo: { findByUserId: Mock };
|
|
let mockExternalRatingRepo: { findByUserId: Mock };
|
|
let mockRatingEventRepo: { getAllByUserId: Mock };
|
|
let handler: GetUserRatingsSummaryQueryHandler;
|
|
|
|
beforeEach(() => {
|
|
mockUserRatingRepo = {
|
|
findByUserId: vi.fn(),
|
|
};
|
|
mockExternalRatingRepo = {
|
|
findByUserId: vi.fn(),
|
|
};
|
|
mockRatingEventRepo = {
|
|
getAllByUserId: vi.fn(),
|
|
};
|
|
|
|
handler = new GetUserRatingsSummaryQueryHandler(
|
|
mockUserRatingRepo as unknown as UserRatingRepository,
|
|
mockExternalRatingRepo as unknown as ExternalGameRatingRepository,
|
|
mockRatingEventRepo as unknown as RatingEventRepository
|
|
);
|
|
});
|
|
|
|
describe('execute', () => {
|
|
it('should return summary with platform and external ratings', async () => {
|
|
const userId = 'user-123';
|
|
|
|
// Mock user rating
|
|
const userRating = UserRating.create(userId);
|
|
mockUserRatingRepo.findByUserId.mockResolvedValue(userRating);
|
|
|
|
// Mock external ratings
|
|
const gameKey = GameKey.create('iracing');
|
|
const profile = ExternalGameRatingProfile.create({
|
|
userId: UserId.fromString(userId),
|
|
gameKey,
|
|
ratings: new Map([
|
|
['iRating', ExternalRating.create(gameKey, 'iRating', 2200)],
|
|
['safetyRating', ExternalRating.create(gameKey, 'safetyRating', 4.5)],
|
|
]),
|
|
provenance: ExternalRatingProvenance.create({ source: 'iRacing API', lastSyncedAt: new Date() }),
|
|
});
|
|
mockExternalRatingRepo.findByUserId.mockResolvedValue([profile]);
|
|
|
|
// Mock rating events
|
|
const event = RatingEvent.create({
|
|
id: RatingEventId.generate(),
|
|
userId,
|
|
dimension: RatingDimensionKey.create('driving'),
|
|
delta: RatingDelta.create(5),
|
|
occurredAt: new Date('2024-01-01'),
|
|
createdAt: new Date('2024-01-01'),
|
|
source: { type: 'race', id: 'race-123' },
|
|
reason: { code: 'RACE_FINISH', summary: 'Good race', details: {} },
|
|
visibility: { public: true, redactedFields: [] },
|
|
version: 1,
|
|
});
|
|
mockRatingEventRepo.getAllByUserId.mockResolvedValue([event]);
|
|
|
|
const query: GetUserRatingsSummaryQuery = { userId };
|
|
const result = await handler.execute(query);
|
|
|
|
expect(result.userId).toBe(userId);
|
|
expect(result.platform.driving.value).toBe(50); // Default
|
|
expect(result.platform.overallReputation).toBe(50);
|
|
expect(result.external.iracing?.iRating).toBe(2200);
|
|
expect(result.external.iracing?.safetyRating).toBe(4.5);
|
|
expect(result.lastRatingEventAt).toBe('2024-01-01T00:00:00.000Z');
|
|
});
|
|
|
|
it('should handle missing user rating gracefully', async () => {
|
|
const userId = 'user-123';
|
|
|
|
mockUserRatingRepo.findByUserId.mockResolvedValue(null);
|
|
mockExternalRatingRepo.findByUserId.mockResolvedValue([]);
|
|
mockRatingEventRepo.getAllByUserId.mockResolvedValue([]);
|
|
|
|
const query: GetUserRatingsSummaryQuery = { userId };
|
|
const result = await handler.execute(query);
|
|
|
|
expect(result.userId).toBe(userId);
|
|
expect(result.platform.driving.value).toBe(0);
|
|
expect(result.platform.overallReputation).toBe(0);
|
|
expect(result.external).toEqual({});
|
|
expect(result.lastRatingEventAt).toBeUndefined();
|
|
});
|
|
|
|
it('should handle multiple external game profiles', async () => {
|
|
const userId = 'user-123';
|
|
|
|
mockUserRatingRepo.findByUserId.mockResolvedValue(UserRating.create(userId));
|
|
|
|
// Multiple game profiles
|
|
const iracingProfile = ExternalGameRatingProfile.create({
|
|
userId: UserId.fromString(userId),
|
|
gameKey: GameKey.create('iracing'),
|
|
ratings: new Map([
|
|
['iRating', ExternalRating.create(GameKey.create('iracing'), 'iRating', 2200)],
|
|
]),
|
|
provenance: ExternalRatingProvenance.create({ source: 'iRacing API', lastSyncedAt: new Date() }),
|
|
});
|
|
|
|
const assettoProfile = ExternalGameRatingProfile.create({
|
|
userId: UserId.fromString(userId),
|
|
gameKey: GameKey.create('assetto'),
|
|
ratings: new Map([
|
|
['rating', ExternalRating.create(GameKey.create('assetto'), 'rating', 85)],
|
|
]),
|
|
provenance: ExternalRatingProvenance.create({ source: 'Assetto API', lastSyncedAt: new Date() }),
|
|
});
|
|
|
|
mockExternalRatingRepo.findByUserId.mockResolvedValue([iracingProfile, assettoProfile]);
|
|
mockRatingEventRepo.getAllByUserId.mockResolvedValue([]);
|
|
|
|
const query: GetUserRatingsSummaryQuery = { userId };
|
|
const result = await handler.execute(query);
|
|
|
|
expect(result.external.iracing.iRating).toBe(2200);
|
|
expect(result.external.assetto.rating).toBe(85);
|
|
});
|
|
|
|
it('should handle empty external ratings', async () => {
|
|
const userId = 'user-123';
|
|
|
|
mockUserRatingRepo.findByUserId.mockResolvedValue(UserRating.create(userId));
|
|
mockExternalRatingRepo.findByUserId.mockResolvedValue([]);
|
|
mockRatingEventRepo.getAllByUserId.mockResolvedValue([]);
|
|
|
|
const query: GetUserRatingsSummaryQuery = { userId };
|
|
const result = await handler.execute(query);
|
|
|
|
expect(result.external).toEqual({});
|
|
});
|
|
|
|
it('should use current date for timestamps when no user rating exists', async () => {
|
|
const userId = 'user-123';
|
|
|
|
mockUserRatingRepo.findByUserId.mockResolvedValue(null);
|
|
mockExternalRatingRepo.findByUserId.mockResolvedValue([]);
|
|
mockRatingEventRepo.getAllByUserId.mockResolvedValue([]);
|
|
|
|
const beforeQuery = new Date();
|
|
const query: GetUserRatingsSummaryQuery = { userId };
|
|
const result = await handler.execute(query);
|
|
const afterQuery = new Date();
|
|
|
|
// Should have valid ISO date strings
|
|
expect(new Date(result.createdAt).getTime()).toBeGreaterThanOrEqual(beforeQuery.getTime());
|
|
expect(new Date(result.createdAt).getTime()).toBeLessThanOrEqual(afterQuery.getTime());
|
|
expect(new Date(result.updatedAt).getTime()).toBeGreaterThanOrEqual(beforeQuery.getTime());
|
|
expect(new Date(result.updatedAt).getTime()).toBeLessThanOrEqual(afterQuery.getTime());
|
|
});
|
|
});
|
|
});
|