377 lines
12 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
}); |