rating
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { RatingEventOrmEntity } from '@adapters/identity/persistence/typeorm/entities/RatingEventOrmEntity';
|
||||
import { UserRatingOrmEntity } from '@adapters/identity/persistence/typeorm/entities/UserRatingOrmEntity';
|
||||
|
||||
import { TypeOrmRatingEventRepository } from '@adapters/identity/persistence/typeorm/repositories/TypeOrmRatingEventRepository';
|
||||
import { TypeOrmUserRatingRepository } from '@adapters/identity/persistence/typeorm/repositories/TypeOrmUserRatingRepository';
|
||||
|
||||
import { AppendRatingEventsUseCase } from '@core/identity/application/use-cases/AppendRatingEventsUseCase';
|
||||
import { RecomputeUserRatingSnapshotUseCase } from '@core/identity/application/use-cases/RecomputeUserRatingSnapshotUseCase';
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const describeIfDatabase = databaseUrl ? describe : describe.skip;
|
||||
|
||||
describeIfDatabase('TypeORM Identity Rating repositories (postgres slice)', () => {
|
||||
let dataSource: DataSource;
|
||||
let eventRepo: TypeOrmRatingEventRepository;
|
||||
let ratingRepo: TypeOrmUserRatingRepository;
|
||||
let appendUseCase: AppendRatingEventsUseCase;
|
||||
let recomputeUseCase: RecomputeUserRatingSnapshotUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (!databaseUrl) {
|
||||
throw new Error('DATABASE_URL is required to run postgres integration tests');
|
||||
}
|
||||
|
||||
dataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
url: databaseUrl,
|
||||
entities: [RatingEventOrmEntity, UserRatingOrmEntity],
|
||||
synchronize: true,
|
||||
});
|
||||
|
||||
await dataSource.initialize();
|
||||
|
||||
// Initialize repositories
|
||||
eventRepo = new TypeOrmRatingEventRepository(dataSource);
|
||||
ratingRepo = new TypeOrmUserRatingRepository(dataSource);
|
||||
|
||||
// Initialize use cases
|
||||
appendUseCase = new AppendRatingEventsUseCase(eventRepo, ratingRepo);
|
||||
recomputeUseCase = new RecomputeUserRatingSnapshotUseCase(eventRepo, ratingRepo);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (dataSource?.isInitialized) {
|
||||
// Clean up test data
|
||||
await dataSource.getRepository(RatingEventOrmEntity).clear();
|
||||
await dataSource.getRepository(UserRatingOrmEntity).clear();
|
||||
await dataSource.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('should complete full flow: append events -> persist -> recompute snapshot', async () => {
|
||||
const userId = `test-user-${Date.now()}`;
|
||||
|
||||
// Step 1: Append rating events from race results
|
||||
const appendResult = await appendUseCase.execute({
|
||||
userId,
|
||||
raceId: 'race-integration-test',
|
||||
raceResults: [
|
||||
{
|
||||
position: 3,
|
||||
totalDrivers: 10,
|
||||
startPosition: 5,
|
||||
incidents: 1,
|
||||
fieldStrength: 1500,
|
||||
status: 'finished',
|
||||
},
|
||||
{
|
||||
position: 1,
|
||||
totalDrivers: 10,
|
||||
startPosition: 2,
|
||||
incidents: 0,
|
||||
fieldStrength: 1500,
|
||||
status: 'finished',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Verify events were saved
|
||||
expect(appendResult.events.length).toBeGreaterThan(0);
|
||||
expect(appendResult.snapshotUpdated).toBe(true);
|
||||
|
||||
// Step 2: Verify events are in database
|
||||
const eventsInDb = await eventRepo.getAllByUserId(userId);
|
||||
expect(eventsInDb.length).toBeGreaterThan(0);
|
||||
expect(eventsInDb.length).toBe(appendResult.events.length);
|
||||
|
||||
// Step 3: Verify snapshot was created
|
||||
const snapshotFromDb = await ratingRepo.findByUserId(userId);
|
||||
expect(snapshotFromDb).not.toBeNull();
|
||||
expect(snapshotFromDb!.userId).toBe(userId);
|
||||
expect(snapshotFromDb!.driver.value).toBeGreaterThan(50); // Should have increased
|
||||
|
||||
// Step 4: Recompute snapshot manually
|
||||
const recomputeResult = await recomputeUseCase.execute({ userId });
|
||||
|
||||
// Verify recomputed snapshot
|
||||
expect(recomputeResult.snapshot.userId).toBe(userId);
|
||||
expect(recomputeResult.snapshot.driver.value).toBeGreaterThan(50);
|
||||
expect(recomputeResult.snapshot.driver.sampleSize).toBeGreaterThan(0);
|
||||
|
||||
// Step 5: Verify recomputed snapshot matches what's in DB
|
||||
const finalSnapshot = await ratingRepo.findByUserId(userId);
|
||||
expect(finalSnapshot!.driver.value).toBe(recomputeResult.snapshot.driver.value);
|
||||
});
|
||||
|
||||
it('should handle direct event creation and recompute', async () => {
|
||||
const userId = `test-user-direct-${Date.now()}`;
|
||||
|
||||
// Append direct events
|
||||
const appendResult = await appendUseCase.execute({
|
||||
userId,
|
||||
events: [
|
||||
{
|
||||
userId,
|
||||
dimension: 'driving',
|
||||
delta: 8,
|
||||
sourceType: 'race',
|
||||
sourceId: 'race-direct',
|
||||
reasonCode: 'DRIVING_FINISH_STRENGTH_GAIN',
|
||||
reasonSummary: 'Excellent finish',
|
||||
},
|
||||
{
|
||||
userId,
|
||||
dimension: 'driving',
|
||||
delta: -2,
|
||||
weight: 0.5,
|
||||
sourceType: 'penalty',
|
||||
sourceId: 'penalty-1',
|
||||
reasonCode: 'DRIVING_INCIDENTS_PENALTY',
|
||||
reasonSummary: 'Minor incident',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(appendResult.events).toHaveLength(2);
|
||||
expect(appendResult.snapshotUpdated).toBe(true);
|
||||
|
||||
// Verify events
|
||||
const events = await eventRepo.getAllByUserId(userId);
|
||||
expect(events).toHaveLength(2);
|
||||
|
||||
// Verify snapshot
|
||||
const snapshot = await ratingRepo.findByUserId(userId);
|
||||
expect(snapshot).not.toBeNull();
|
||||
expect(snapshot!.driver.value).toBeGreaterThan(50);
|
||||
expect(snapshot!.driver.sampleSize).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle empty race results gracefully', async () => {
|
||||
const userId = `test-user-empty-${Date.now()}`;
|
||||
|
||||
const result = await appendUseCase.execute({
|
||||
userId,
|
||||
raceId: 'race-empty',
|
||||
raceResults: [],
|
||||
});
|
||||
|
||||
expect(result.events).toHaveLength(0);
|
||||
expect(result.snapshotUpdated).toBe(false);
|
||||
|
||||
// No snapshot should exist
|
||||
const snapshot = await ratingRepo.findByUserId(userId);
|
||||
expect(snapshot).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle DNF/DNS/DSQ/AFK statuses', async () => {
|
||||
const userId = `test-user-status-${Date.now()}`;
|
||||
|
||||
const result = await appendUseCase.execute({
|
||||
userId,
|
||||
raceId: 'race-status-test',
|
||||
raceResults: [
|
||||
{ position: 5, totalDrivers: 10, startPosition: 3, incidents: 2, fieldStrength: 1500, status: 'dnf' },
|
||||
{ position: 5, totalDrivers: 10, startPosition: 3, incidents: 0, fieldStrength: 1500, status: 'dns' },
|
||||
{ position: 5, totalDrivers: 10, startPosition: 3, incidents: 5, fieldStrength: 1500, status: 'dsq' },
|
||||
{ position: 5, totalDrivers: 10, startPosition: 3, incidents: 1, fieldStrength: 1500, status: 'afk' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.events.length).toBeGreaterThan(0);
|
||||
expect(result.snapshotUpdated).toBe(true);
|
||||
|
||||
// Verify events were created for each status
|
||||
const events = await eventRepo.getAllByUserId(userId);
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify snapshot exists and has penalties
|
||||
const snapshot = await ratingRepo.findByUserId(userId);
|
||||
expect(snapshot).not.toBeNull();
|
||||
expect(snapshot!.driver.value).toBeLessThan(50); // Should have decreased due to penalties
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user