Files
gridpilot.gg/core/racing/application/queries/GetTeamRatingLedgerQuery.test.ts
2026-01-16 18:21:06 +01:00

377 lines
12 KiB
TypeScript

/**
* Tests for GetTeamRatingLedgerQuery
*/
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import { GetTeamRatingLedgerQuery, GetTeamRatingLedgerQueryHandler } from './GetTeamRatingLedgerQuery';
import { TeamRatingEvent } from '../../domain/entities/TeamRatingEvent';
import { TeamRatingEventId } from '../../domain/value-objects/TeamRatingEventId';
import { TeamRatingDimensionKey } from '../../domain/value-objects/TeamRatingDimensionKey';
import { TeamRatingDelta } from '../../domain/value-objects/TeamRatingDelta';
import type { TeamRatingEventRepository } from '../../domain/repositories/TeamRatingEventRepository';
describe('GetTeamRatingLedgerQuery', () => {
let mockRatingEventRepo: { findEventsPaginated: Mock };
let handler: GetTeamRatingLedgerQueryHandler;
beforeEach(() => {
mockRatingEventRepo = {
findEventsPaginated: vi.fn(),
};
handler = new GetTeamRatingLedgerQueryHandler(mockRatingEventRepo as unknown as TeamRatingEventRepository);
});
describe('execute', () => {
it('should return paginated ledger entries', async () => {
const teamId = 'team-123';
// Mock paginated result
const event1 = TeamRatingEvent.create({
id: TeamRatingEventId.generate(),
teamId,
dimension: TeamRatingDimensionKey.create('driving'),
delta: TeamRatingDelta.create(10),
occurredAt: new Date('2024-01-01T10:00:00Z'),
createdAt: new Date('2024-01-01T10:00:00Z'),
source: { type: 'race', id: 'race-123' },
reason: { code: 'RACE_FINISH' },
visibility: { public: true },
version: 1,
});
const event2 = TeamRatingEvent.create({
id: TeamRatingEventId.generate(),
teamId,
dimension: TeamRatingDimensionKey.create('adminTrust'),
delta: TeamRatingDelta.create(-5),
occurredAt: new Date('2024-01-02T10:00:00Z'),
createdAt: new Date('2024-01-02T10:00:00Z'),
source: { type: 'penalty', id: 'penalty-456' },
reason: { code: 'LATE_JOIN' },
visibility: { public: true },
version: 1,
weight: 2,
});
mockRatingEventRepo.findEventsPaginated.mockResolvedValue({
items: [event1, event2],
total: 2,
limit: 20,
offset: 0,
hasMore: false,
nextOffset: undefined,
});
const query: GetTeamRatingLedgerQuery = { teamId };
const result = await handler.execute(query);
expect(result.entries.length).toBe(2);
const entry1 = result.entries[0];
expect(entry1).toBeDefined();
if (entry1) {
expect(entry1.teamId).toBe(teamId);
expect(entry1.dimension).toBe('driving');
expect(entry1.delta).toBe(10);
expect(entry1.source.type).toBe('race');
expect(entry1.source.id).toBe('race-123');
expect(entry1.reason.code).toBe('RACE_FINISH');
expect(entry1.visibility.public).toBe(true);
}
const entry2 = result.entries[1];
expect(entry2).toBeDefined();
if (entry2) {
expect(entry2.dimension).toBe('adminTrust');
expect(entry2.delta).toBe(-5);
expect(entry2.weight).toBe(2);
expect(entry2.source.type).toBe('penalty');
expect(entry2.source.id).toBe('penalty-456');
}
expect(result.pagination.total).toBe(2);
expect(result.pagination.limit).toBe(20);
expect(result.pagination.hasMore).toBe(false);
});
it('should apply default pagination values', async () => {
const teamId = 'team-123';
mockRatingEventRepo.findEventsPaginated.mockResolvedValue({
items: [],
total: 0,
limit: 20,
offset: 0,
hasMore: false,
});
const query: GetTeamRatingLedgerQuery = { teamId };
await handler.execute(query);
expect(mockRatingEventRepo.findEventsPaginated).toHaveBeenCalledWith(
teamId,
expect.objectContaining({
limit: 20,
offset: 0,
})
);
});
it('should apply custom pagination values', async () => {
const teamId = 'team-123';
mockRatingEventRepo.findEventsPaginated.mockResolvedValue({
items: [],
total: 0,
limit: 10,
offset: 20,
hasMore: true,
nextOffset: 30,
});
const query: GetTeamRatingLedgerQuery = { teamId, limit: 10, offset: 20 };
await handler.execute(query);
expect(mockRatingEventRepo.findEventsPaginated).toHaveBeenCalledWith(
teamId,
expect.objectContaining({
limit: 10,
offset: 20,
})
);
});
it('should apply filters when provided', async () => {
const teamId = 'team-123';
const filter = {
dimensions: ['driving'],
sourceTypes: ['race', 'penalty'] as ('race' | 'penalty' | 'vote' | 'adminAction' | 'manualAdjustment')[],
from: '2024-01-01T00:00:00Z',
to: '2024-01-31T23:59:59Z',
reasonCodes: ['RACE_FINISH', 'LATE_JOIN'],
};
mockRatingEventRepo.findEventsPaginated.mockResolvedValue({
items: [],
total: 0,
limit: 20,
offset: 0,
hasMore: false,
});
const query: GetTeamRatingLedgerQuery = { teamId, filter };
await handler.execute(query);
expect(mockRatingEventRepo.findEventsPaginated).toHaveBeenCalledWith(
teamId,
expect.objectContaining({
filter: expect.objectContaining({
dimensions: ['driving'],
sourceTypes: ['race', 'penalty'],
from: new Date('2024-01-01T00:00:00Z'),
to: new Date('2024-01-31T23:59:59Z'),
reasonCodes: ['RACE_FINISH', 'LATE_JOIN'],
}),
})
);
});
it('should handle events with optional weight', async () => {
const teamId = 'team-123';
const eventWithWeight = TeamRatingEvent.create({
id: TeamRatingEventId.generate(),
teamId,
dimension: TeamRatingDimensionKey.create('driving'),
delta: TeamRatingDelta.create(15),
weight: 1.5,
occurredAt: new Date('2024-01-01T10:00:00Z'),
createdAt: new Date('2024-01-01T10:00:00Z'),
source: { type: 'race', id: 'race-789' },
reason: { code: 'PERFORMANCE_BONUS' },
visibility: { public: true },
version: 1,
});
const eventWithoutWeight = TeamRatingEvent.create({
id: TeamRatingEventId.generate(),
teamId,
dimension: TeamRatingDimensionKey.create('adminTrust'),
delta: TeamRatingDelta.create(5),
occurredAt: new Date('2024-01-02T10:00:00Z'),
createdAt: new Date('2024-01-02T10:00:00Z'),
source: { type: 'vote', id: 'vote-123' },
reason: { code: 'POSITIVE_VOTE' },
visibility: { public: true },
version: 1,
});
mockRatingEventRepo.findEventsPaginated.mockResolvedValue({
items: [eventWithWeight, eventWithoutWeight],
total: 2,
limit: 20,
offset: 0,
hasMore: false,
});
const query: GetTeamRatingLedgerQuery = { teamId };
const result = await handler.execute(query);
const entry1 = result.entries[0];
const entry2 = result.entries[1];
expect(entry1).toBeDefined();
expect(entry2).toBeDefined();
if (entry1) {
expect(entry1.weight).toBe(1.5);
}
if (entry2) {
expect(entry2.weight).toBeUndefined();
}
});
it('should handle events with optional source.id', async () => {
const teamId = 'team-123';
const eventWithId = TeamRatingEvent.create({
id: TeamRatingEventId.generate(),
teamId,
dimension: TeamRatingDimensionKey.create('driving'),
delta: TeamRatingDelta.create(10),
occurredAt: new Date('2024-01-01T10:00:00Z'),
createdAt: new Date('2024-01-01T10:00:00Z'),
source: { type: 'race', id: 'race-123' },
reason: { code: 'RACE_FINISH' },
visibility: { public: true },
version: 1,
});
const eventWithoutId = TeamRatingEvent.create({
id: TeamRatingEventId.generate(),
teamId,
dimension: TeamRatingDimensionKey.create('adminTrust'),
delta: TeamRatingDelta.create(5),
occurredAt: new Date('2024-01-02T10:00:00Z'),
createdAt: new Date('2024-01-02T10:00:00Z'),
source: { type: 'manualAdjustment' },
reason: { code: 'ADMIN_ADJUSTMENT' },
visibility: { public: true },
version: 1,
});
mockRatingEventRepo.findEventsPaginated.mockResolvedValue({
items: [eventWithId, eventWithoutId],
total: 2,
limit: 20,
offset: 0,
hasMore: false,
});
const query: GetTeamRatingLedgerQuery = { teamId };
const result = await handler.execute(query);
const entry1 = result.entries[0];
const entry2 = result.entries[1];
expect(entry1).toBeDefined();
expect(entry2).toBeDefined();
if (entry1) {
expect(entry1.source.id).toBe('race-123');
}
if (entry2) {
expect(entry2.source.id).toBeUndefined();
}
});
it('should handle events with optional reason.description', async () => {
const teamId = 'team-123';
const eventWithDescription = TeamRatingEvent.create({
id: TeamRatingEventId.generate(),
teamId,
dimension: TeamRatingDimensionKey.create('driving'),
delta: TeamRatingDelta.create(10),
occurredAt: new Date('2024-01-01T10:00:00Z'),
createdAt: new Date('2024-01-01T10:00:00Z'),
source: { type: 'race', id: 'race-123' },
reason: { code: 'RACE_FINISH', description: 'Finished 1st in class' },
visibility: { public: true },
version: 1,
});
const eventWithoutDescription = TeamRatingEvent.create({
id: TeamRatingEventId.generate(),
teamId,
dimension: TeamRatingDimensionKey.create('adminTrust'),
delta: TeamRatingDelta.create(5),
occurredAt: new Date('2024-01-02T10:00:00Z'),
createdAt: new Date('2024-01-02T10:00:00Z'),
source: { type: 'vote', id: 'vote-123' },
reason: { code: 'POSITIVE_VOTE' },
visibility: { public: true },
version: 1,
});
mockRatingEventRepo.findEventsPaginated.mockResolvedValue({
items: [eventWithDescription, eventWithoutDescription],
total: 2,
limit: 20,
offset: 0,
hasMore: false,
});
const query: GetTeamRatingLedgerQuery = { teamId };
const result = await handler.execute(query);
const entry1 = result.entries[0];
const entry2 = result.entries[1];
expect(entry1).toBeDefined();
expect(entry2).toBeDefined();
if (entry1) {
expect(entry1.reason.description).toBe('Finished 1st in class');
}
if (entry2) {
expect(entry2.reason.description).toBeUndefined();
}
});
it('should return nextOffset when hasMore is true', async () => {
const teamId = 'team-123';
mockRatingEventRepo.findEventsPaginated.mockResolvedValue({
items: [],
total: 50,
limit: 20,
offset: 20,
hasMore: true,
nextOffset: 40,
});
const query: GetTeamRatingLedgerQuery = { teamId };
const result = await handler.execute(query);
expect(result.pagination.hasMore).toBe(true);
expect(result.pagination.nextOffset).toBe(40);
});
it('should return null nextOffset when hasMore is false', async () => {
const teamId = 'team-123';
mockRatingEventRepo.findEventsPaginated.mockResolvedValue({
items: [],
total: 15,
limit: 20,
offset: 0,
hasMore: false,
});
const query: GetTeamRatingLedgerQuery = { teamId };
const result = await handler.execute(query);
expect(result.pagination.hasMore).toBe(false);
expect(result.pagination.nextOffset).toBeNull();
});
});
});