code quality
Some checks failed
CI / lint-typecheck (pull_request) Failing after 12s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
Some checks failed
CI / lint-typecheck (pull_request) Failing after 12s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
This commit is contained in:
@@ -72,7 +72,7 @@ describe('GetUserRatingLedgerQueryHandler', () => {
|
|||||||
hasMore: false,
|
hasMore: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filter: any = {
|
const filter: unknown = {
|
||||||
dimensions: ['trust'],
|
dimensions: ['trust'],
|
||||||
sourceTypes: ['vote'],
|
sourceTypes: ['vote'],
|
||||||
from: '2026-01-01T00:00:00Z',
|
from: '2026-01-01T00:00:00Z',
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* Application Use Case Tests: CloseAdminVoteSessionUseCase
|
* Application Use Case Tests: CloseAdminVoteSessionUseCase
|
||||||
*
|
*
|
||||||
* Tests for closing admin vote sessions and generating rating events
|
* Tests for closing admin vote sessions and generating rating events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||||
import { CloseAdminVoteSessionUseCase } from './CloseAdminVoteSessionUseCase';
|
import { CloseAdminVoteSessionUseCase } from './CloseAdminVoteSessionUseCase';
|
||||||
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
||||||
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
||||||
|
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
|
||||||
|
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
||||||
|
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
||||||
|
import { AdminVoteSession, AdminVoteOutcome } from '../../domain/entities/AdminVoteSession';
|
||||||
|
|
||||||
// Mock repositories
|
// Mock repositories
|
||||||
const createMockRepositories = () => ({
|
const createMockRepositories = () => ({
|
||||||
@@ -51,14 +55,14 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockRepositories = createMockRepositories();
|
mockRepositories = createMockRepositories();
|
||||||
useCase = new CloseAdminVoteSessionUseCase(
|
useCase = new CloseAdminVoteSessionUseCase(
|
||||||
mockRepositories.adminVoteSessionRepository,
|
mockRepositories.adminVoteSessionRepository as unknown as AdminVoteSessionRepository,
|
||||||
mockRepositories.ratingEventRepository,
|
mockRepositories.ratingEventRepository as unknown as RatingEventRepository,
|
||||||
mockRepositories.userRatingRepository
|
mockRepositories.userRatingRepository as unknown as UserRatingRepository
|
||||||
);
|
);
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Default mock for RatingEventFactory.createFromVote to return an empty array
|
// Default mock for RatingEventFactory.createFromVote to return an empty array
|
||||||
// to avoid "events is not iterable" error in tests that don't explicitly mock it
|
// to avoid "events is not iterable" error in tests that don't explicitly mock it
|
||||||
(RatingEventFactory.createFromVote as any).mockReturnValue([]);
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Input validation', () => {
|
describe('Input validation', () => {
|
||||||
@@ -84,7 +88,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should accept valid input', async () => {
|
it('should accept valid input', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -144,7 +157,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should find session by ID when provided', async () => {
|
it('should find session by ID when provided', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -186,7 +208,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
describe('Admin ownership validation', () => {
|
describe('Admin ownership validation', () => {
|
||||||
it('should reject when admin does not own the session', async () => {
|
it('should reject when admin does not own the session', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'different-admin',
|
adminId: 'different-admin',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -227,7 +258,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should accept when admin owns the session', async () => {
|
it('should accept when admin owns the session', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -269,7 +309,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
describe('Session closure validation', () => {
|
describe('Session closure validation', () => {
|
||||||
it('should reject when session is already closed', async () => {
|
it('should reject when session is already closed', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -310,7 +359,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should accept when session is not closed', async () => {
|
it('should accept when session is not closed', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -352,7 +410,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
describe('Voting window validation', () => {
|
describe('Voting window validation', () => {
|
||||||
it('should reject when trying to close outside voting window', async () => {
|
it('should reject when trying to close outside voting window', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -388,7 +455,7 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super('2026-02-02');
|
super('2026-02-02');
|
||||||
}
|
}
|
||||||
} as any;
|
} as unknown as typeof Date;
|
||||||
|
|
||||||
const result = await useCase.execute({
|
const result = await useCase.execute({
|
||||||
voteSessionId: 'session-123',
|
voteSessionId: 'session-123',
|
||||||
@@ -404,7 +471,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should accept when trying to close within voting window', async () => {
|
it('should accept when trying to close within voting window', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -440,7 +516,7 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super('2026-01-15T12:00:00');
|
super('2026-01-15T12:00:00');
|
||||||
}
|
}
|
||||||
} as any;
|
} as unknown as typeof Date;
|
||||||
|
|
||||||
const result = await useCase.execute({
|
const result = await useCase.execute({
|
||||||
voteSessionId: 'session-123',
|
voteSessionId: 'session-123',
|
||||||
@@ -457,7 +533,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
describe('Session closure', () => {
|
describe('Session closure', () => {
|
||||||
it('should call close method on session', async () => {
|
it('should call close method on session', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -497,7 +582,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should save closed session', async () => {
|
it('should save closed session', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -537,7 +631,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should return outcome in success response', async () => {
|
it('should return outcome in success response', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -585,7 +688,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
describe('Rating event creation', () => {
|
describe('Rating event creation', () => {
|
||||||
it('should create rating events when outcome is positive', async () => {
|
it('should create rating events when outcome is positive', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -616,7 +728,7 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
||||||
|
|
||||||
const mockEvent = { id: 'event-123' };
|
const mockEvent = { id: 'event-123' };
|
||||||
(RatingEventFactory.createFromVote as any).mockReturnValue([mockEvent]);
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent]);
|
||||||
|
|
||||||
await useCase.execute({
|
await useCase.execute({
|
||||||
voteSessionId: 'session-123',
|
voteSessionId: 'session-123',
|
||||||
@@ -635,7 +747,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should create rating events when outcome is negative', async () => {
|
it('should create rating events when outcome is negative', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -666,7 +787,7 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
||||||
|
|
||||||
const mockEvent = { id: 'event-123' };
|
const mockEvent = { id: 'event-123' };
|
||||||
(RatingEventFactory.createFromVote as any).mockReturnValue([mockEvent]);
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent]);
|
||||||
|
|
||||||
await useCase.execute({
|
await useCase.execute({
|
||||||
voteSessionId: 'session-123',
|
voteSessionId: 'session-123',
|
||||||
@@ -685,7 +806,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should not create rating events when outcome is tie', async () => {
|
it('should not create rating events when outcome is tie', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -726,7 +856,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should save created rating events', async () => {
|
it('should save created rating events', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -758,7 +897,7 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
const mockEvent1 = { id: 'event-123' };
|
const mockEvent1 = { id: 'event-123' };
|
||||||
const mockEvent2 = { id: 'event-124' };
|
const mockEvent2 = { id: 'event-124' };
|
||||||
(RatingEventFactory.createFromVote as any).mockReturnValue([mockEvent1, mockEvent2]);
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent1, mockEvent2]);
|
||||||
|
|
||||||
await useCase.execute({
|
await useCase.execute({
|
||||||
voteSessionId: 'session-123',
|
voteSessionId: 'session-123',
|
||||||
@@ -772,7 +911,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should return eventsCreated count', async () => {
|
it('should return eventsCreated count', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -804,7 +952,7 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
const mockEvent1 = { id: 'event-123' };
|
const mockEvent1 = { id: 'event-123' };
|
||||||
const mockEvent2 = { id: 'event-124' };
|
const mockEvent2 = { id: 'event-124' };
|
||||||
(RatingEventFactory.createFromVote as any).mockReturnValue([mockEvent1, mockEvent2]);
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent1, mockEvent2]);
|
||||||
|
|
||||||
const result = await useCase.execute({
|
const result = await useCase.execute({
|
||||||
voteSessionId: 'session-123',
|
voteSessionId: 'session-123',
|
||||||
@@ -818,7 +966,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
describe('Snapshot recalculation', () => {
|
describe('Snapshot recalculation', () => {
|
||||||
it('should recalculate snapshot when events are created', async () => {
|
it('should recalculate snapshot when events are created', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -849,13 +1006,13 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
||||||
|
|
||||||
const mockEvent = { id: 'event-123' };
|
const mockEvent = { id: 'event-123' };
|
||||||
(RatingEventFactory.createFromVote as any).mockReturnValue([mockEvent]);
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent]);
|
||||||
|
|
||||||
const mockAllEvents = [{ id: 'event-1' }, { id: 'event-2' }];
|
const mockAllEvents = [{ id: 'event-1' }, { id: 'event-2' }];
|
||||||
mockRepositories.ratingEventRepository.getAllByUserId.mockResolvedValue(mockAllEvents);
|
mockRepositories.ratingEventRepository.getAllByUserId.mockResolvedValue(mockAllEvents);
|
||||||
|
|
||||||
const mockSnapshot = { userId: 'admin-123', overallReputation: 75 };
|
const mockSnapshot = { userId: 'admin-123', overallReputation: 75 };
|
||||||
(RatingSnapshotCalculator.calculate as any).mockReturnValue(mockSnapshot);
|
(RatingSnapshotCalculator.calculate as unknown as Mock).mockReturnValue(mockSnapshot);
|
||||||
|
|
||||||
await useCase.execute({
|
await useCase.execute({
|
||||||
voteSessionId: 'session-123',
|
voteSessionId: 'session-123',
|
||||||
@@ -869,7 +1026,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should not recalculate snapshot when no events are created (tie)', async () => {
|
it('should not recalculate snapshot when no events are created (tie)', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -937,7 +1103,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
it('should handle save errors gracefully', async () => {
|
it('should handle save errors gracefully', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
@@ -981,7 +1156,16 @@ describe('CloseAdminVoteSessionUseCase', () => {
|
|||||||
describe('Return values', () => {
|
describe('Return values', () => {
|
||||||
it('should return voteSessionId in success response', async () => {
|
it('should return voteSessionId in success response', async () => {
|
||||||
const futureDate = new Date('2026-02-01');
|
const futureDate = new Date('2026-02-01');
|
||||||
const mockSession: any = {
|
const mockSession: {
|
||||||
|
id: string;
|
||||||
|
adminId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
_closed: boolean;
|
||||||
|
_outcome?: AdminVoteOutcome;
|
||||||
|
close: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
} = {
|
||||||
id: 'session-123',
|
id: 'session-123',
|
||||||
adminId: 'admin-123',
|
adminId: 'admin-123',
|
||||||
startDate: new Date('2026-01-01'),
|
startDate: new Date('2026-01-01'),
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ describe('OpenAdminVoteSessionUseCase', () => {
|
|||||||
|
|
||||||
describe('Business rules', () => {
|
describe('Business rules', () => {
|
||||||
it('should reject when session ID already exists', async () => {
|
it('should reject when session ID already exists', async () => {
|
||||||
mockRepository.findById.mockResolvedValue({ id: 'session-1' } as any);
|
mockRepository.findById.mockResolvedValue({ id: 'session-1' } as unknown as AdminVoteSession);
|
||||||
|
|
||||||
const result = await useCase.execute({
|
const result = await useCase.execute({
|
||||||
voteSessionId: 'session-1',
|
voteSessionId: 'session-1',
|
||||||
@@ -193,7 +193,7 @@ describe('OpenAdminVoteSessionUseCase', () => {
|
|||||||
startDate: new Date('2026-01-05'),
|
startDate: new Date('2026-01-05'),
|
||||||
endDate: new Date('2026-01-10'),
|
endDate: new Date('2026-01-10'),
|
||||||
}
|
}
|
||||||
] as any);
|
] as unknown as AdminVoteSession[]);
|
||||||
|
|
||||||
const result = await useCase.execute({
|
const result = await useCase.execute({
|
||||||
voteSessionId: 'session-1',
|
voteSessionId: 'session-1',
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ describe('Company', () => {
|
|||||||
id: 'comp-123',
|
id: 'comp-123',
|
||||||
name: 'Acme Racing Team',
|
name: 'Acme Racing Team',
|
||||||
ownerUserId: 'user-123',
|
ownerUserId: 'user-123',
|
||||||
contactEmail: null as any,
|
contactEmail: null,
|
||||||
createdAt,
|
createdAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ describe('PasswordHashingService', () => {
|
|||||||
|
|
||||||
it('should reject verification with null hash', async () => {
|
it('should reject verification with null hash', async () => {
|
||||||
// bcrypt throws an error when hash is null, which is expected behavior
|
// bcrypt throws an error when hash is null, which is expected behavior
|
||||||
await expect(service.verify('password', null as any)).rejects.toThrow();
|
await expect(service.verify('password', null as unknown as string)).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject verification with empty hash', async () => {
|
it('should reject verification with empty hash', async () => {
|
||||||
|
|||||||
@@ -216,17 +216,17 @@ describe('EmailAddress', () => {
|
|||||||
|
|
||||||
describe('Edge cases', () => {
|
describe('Edge cases', () => {
|
||||||
it('should handle null input gracefully', () => {
|
it('should handle null input gracefully', () => {
|
||||||
const result = validateEmail(null as any);
|
const result = validateEmail(null as unknown as string);
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle undefined input gracefully', () => {
|
it('should handle undefined input gracefully', () => {
|
||||||
const result = validateEmail(undefined as any);
|
const result = validateEmail(undefined as unknown as string);
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle non-string input gracefully', () => {
|
it('should handle non-string input gracefully', () => {
|
||||||
const result = validateEmail(123 as any);
|
const result = validateEmail(123 as unknown as string);
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -305,13 +305,13 @@ describe('EmailAddress', () => {
|
|||||||
it('should handle null input', () => {
|
it('should handle null input', () => {
|
||||||
// The current implementation throws an error when given null
|
// The current implementation throws an error when given null
|
||||||
// This is expected behavior - the function expects a string
|
// This is expected behavior - the function expects a string
|
||||||
expect(() => isDisposableEmail(null as any)).toThrow();
|
expect(() => isDisposableEmail(null as unknown as string)).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle undefined input', () => {
|
it('should handle undefined input', () => {
|
||||||
// The current implementation throws an error when given undefined
|
// The current implementation throws an error when given undefined
|
||||||
// This is expected behavior - the function expects a string
|
// This is expected behavior - the function expects a string
|
||||||
expect(() => isDisposableEmail(undefined as any)).toThrow();
|
expect(() => isDisposableEmail(undefined as unknown as string)).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { GetDriverRankingsUseCase, GetDriverRankingsUseCasePorts } from './GetDriverRankingsUseCase';
|
import { GetDriverRankingsUseCase, GetDriverRankingsUseCasePorts } from './GetDriverRankingsUseCase';
|
||||||
import { ValidationError } from '../../../shared/errors/ValidationError';
|
import { ValidationError } from '../../../shared/errors/ValidationError';
|
||||||
|
import { LeaderboardsRepository, LeaderboardDriverData } from '../ports/LeaderboardsRepository';
|
||||||
|
import { LeaderboardsEventPublisher } from '../ports/LeaderboardsEventPublisher';
|
||||||
|
|
||||||
describe('GetDriverRankingsUseCase', () => {
|
describe('GetDriverRankingsUseCase', () => {
|
||||||
let mockLeaderboardsRepository: any;
|
let mockLeaderboardsRepository: LeaderboardsRepository;
|
||||||
let mockEventPublisher: any;
|
let mockEventPublisher: LeaderboardsEventPublisher;
|
||||||
let ports: GetDriverRankingsUseCasePorts;
|
let ports: GetDriverRankingsUseCasePorts;
|
||||||
let useCase: GetDriverRankingsUseCase;
|
let useCase: GetDriverRankingsUseCase;
|
||||||
|
|
||||||
const mockDrivers = [
|
const mockDrivers: LeaderboardDriverData[] = [
|
||||||
{ id: '1', name: 'Alice', rating: 2000, raceCount: 10, teamId: 't1', teamName: 'Team A' },
|
{ id: '1', name: 'Alice', rating: 2000, raceCount: 10, teamId: 't1', teamName: 'Team A' },
|
||||||
{ id: '2', name: 'Bob', rating: 1500, raceCount: 5, teamId: 't2', teamName: 'Team B' },
|
{ id: '2', name: 'Bob', rating: 1500, raceCount: 5, teamId: 't2', teamName: 'Team B' },
|
||||||
{ id: '3', name: 'Charlie', rating: 1800, raceCount: 8 },
|
{ id: '3', name: 'Charlie', rating: 1800, raceCount: 8 },
|
||||||
@@ -17,10 +19,14 @@ describe('GetDriverRankingsUseCase', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLeaderboardsRepository = {
|
mockLeaderboardsRepository = {
|
||||||
findAllDrivers: vi.fn().mockResolvedValue([...mockDrivers]),
|
findAllDrivers: vi.fn().mockResolvedValue([...mockDrivers]),
|
||||||
|
findAllTeams: vi.fn(),
|
||||||
|
findDriversByTeamId: vi.fn(),
|
||||||
};
|
};
|
||||||
mockEventPublisher = {
|
mockEventPublisher = {
|
||||||
publishDriverRankingsAccessed: vi.fn().mockResolvedValue(undefined),
|
publishDriverRankingsAccessed: vi.fn().mockResolvedValue(undefined),
|
||||||
publishLeaderboardsError: vi.fn().mockResolvedValue(undefined),
|
publishLeaderboardsError: vi.fn().mockResolvedValue(undefined),
|
||||||
|
publishGlobalLeaderboardsAccessed: vi.fn(),
|
||||||
|
publishTeamRankingsAccessed: vi.fn(),
|
||||||
};
|
};
|
||||||
ports = {
|
ports = {
|
||||||
leaderboardsRepository: mockLeaderboardsRepository,
|
leaderboardsRepository: mockLeaderboardsRepository,
|
||||||
@@ -92,6 +98,6 @@ describe('GetDriverRankingsUseCase', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw ValidationError for invalid sortBy', async () => {
|
it('should throw ValidationError for invalid sortBy', async () => {
|
||||||
await expect(useCase.execute({ sortBy: 'invalid' as any })).rejects.toThrow(ValidationError);
|
await expect(useCase.execute({ sortBy: 'invalid' as unknown as 'name' })).rejects.toThrow(ValidationError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,30 +1,35 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||||
import { GetGlobalLeaderboardsUseCase, GetGlobalLeaderboardsUseCasePorts } from './GetGlobalLeaderboardsUseCase';
|
import { GetGlobalLeaderboardsUseCase, GetGlobalLeaderboardsUseCasePorts } from './GetGlobalLeaderboardsUseCase';
|
||||||
|
import { LeaderboardsRepository, LeaderboardDriverData, LeaderboardTeamData } from '../ports/LeaderboardsRepository';
|
||||||
|
import { LeaderboardsEventPublisher } from '../ports/LeaderboardsEventPublisher';
|
||||||
|
|
||||||
describe('GetGlobalLeaderboardsUseCase', () => {
|
describe('GetGlobalLeaderboardsUseCase', () => {
|
||||||
let mockLeaderboardsRepository: any;
|
let mockLeaderboardsRepository: LeaderboardsRepository;
|
||||||
let mockEventPublisher: any;
|
let mockEventPublisher: LeaderboardsEventPublisher;
|
||||||
let ports: GetGlobalLeaderboardsUseCasePorts;
|
let ports: GetGlobalLeaderboardsUseCasePorts;
|
||||||
let useCase: GetGlobalLeaderboardsUseCase;
|
let useCase: GetGlobalLeaderboardsUseCase;
|
||||||
|
|
||||||
const mockDrivers = [
|
const mockDrivers: LeaderboardDriverData[] = [
|
||||||
{ id: 'd1', name: 'Alice', rating: 2000, raceCount: 10 },
|
{ id: 'd1', name: 'Alice', rating: 2000, raceCount: 10 },
|
||||||
{ id: 'd2', name: 'Bob', rating: 1500, raceCount: 5 },
|
{ id: 'd2', name: 'Bob', rating: 1500, raceCount: 5 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockTeams = [
|
const mockTeams: LeaderboardTeamData[] = [
|
||||||
{ id: 't1', name: 'Team A', rating: 2500, memberCount: 5, raceCount: 20 },
|
{ id: 't1', name: 'Team A', rating: 2500, memberCount: 5, raceCount: 20 },
|
||||||
{ id: 't2', name: 'Team B', rating: 2200, memberCount: 3, raceCount: 15 },
|
{ id: 't2', name: 'Team B', rating: 2200, memberCount: 3, raceCount: 15 },
|
||||||
];
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLeaderboardsRepository = {
|
mockLeaderboardsRepository = {
|
||||||
findAllDrivers: vi.fn().mockResolvedValue([...mockDrivers]),
|
findAllDrivers: vi.fn().mockResolvedValue([...mockDrivers]) as unknown as Mock,
|
||||||
findAllTeams: vi.fn().mockResolvedValue([...mockTeams]),
|
findAllTeams: vi.fn().mockResolvedValue([...mockTeams]) as unknown as Mock,
|
||||||
|
findDriversByTeamId: vi.fn() as unknown as Mock,
|
||||||
};
|
};
|
||||||
mockEventPublisher = {
|
mockEventPublisher = {
|
||||||
publishGlobalLeaderboardsAccessed: vi.fn().mockResolvedValue(undefined),
|
publishGlobalLeaderboardsAccessed: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
publishLeaderboardsError: vi.fn().mockResolvedValue(undefined),
|
publishLeaderboardsError: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
publishDriverRankingsAccessed: vi.fn() as unknown as Mock,
|
||||||
|
publishTeamRankingsAccessed: vi.fn() as unknown as Mock,
|
||||||
};
|
};
|
||||||
ports = {
|
ports = {
|
||||||
leaderboardsRepository: mockLeaderboardsRepository,
|
leaderboardsRepository: mockLeaderboardsRepository,
|
||||||
@@ -57,7 +62,7 @@ describe('GetGlobalLeaderboardsUseCase', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle errors and publish error event', async () => {
|
it('should handle errors and publish error event', async () => {
|
||||||
mockLeaderboardsRepository.findAllDrivers.mockRejectedValue(new Error('Repo error'));
|
(mockLeaderboardsRepository.findAllDrivers as Mock).mockRejectedValue(new Error('Repo error'));
|
||||||
|
|
||||||
await expect(useCase.execute()).rejects.toThrow('Repo error');
|
await expect(useCase.execute()).rejects.toThrow('Repo error');
|
||||||
expect(mockEventPublisher.publishLeaderboardsError).toHaveBeenCalled();
|
expect(mockEventPublisher.publishLeaderboardsError).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||||
import { GetTeamRankingsUseCase, GetTeamRankingsUseCasePorts } from './GetTeamRankingsUseCase';
|
import { GetTeamRankingsUseCase, GetTeamRankingsUseCasePorts } from './GetTeamRankingsUseCase';
|
||||||
import { ValidationError } from '../../../shared/errors/ValidationError';
|
import { ValidationError } from '../../../shared/errors/ValidationError';
|
||||||
|
import { LeaderboardsRepository, LeaderboardTeamData, LeaderboardDriverData } from '../ports/LeaderboardsRepository';
|
||||||
|
import { LeaderboardsEventPublisher } from '../ports/LeaderboardsEventPublisher';
|
||||||
|
|
||||||
describe('GetTeamRankingsUseCase', () => {
|
describe('GetTeamRankingsUseCase', () => {
|
||||||
let mockLeaderboardsRepository: any;
|
let mockLeaderboardsRepository: LeaderboardsRepository;
|
||||||
let mockEventPublisher: any;
|
let mockEventPublisher: LeaderboardsEventPublisher;
|
||||||
let ports: GetTeamRankingsUseCasePorts;
|
let ports: GetTeamRankingsUseCasePorts;
|
||||||
let useCase: GetTeamRankingsUseCase;
|
let useCase: GetTeamRankingsUseCase;
|
||||||
|
|
||||||
const mockTeams = [
|
const mockTeams: LeaderboardTeamData[] = [
|
||||||
{ id: 't1', name: 'Team A', rating: 2500, memberCount: 0, raceCount: 20 },
|
{ id: 't1', name: 'Team A', rating: 2500, memberCount: 0, raceCount: 20 },
|
||||||
{ id: 't2', name: 'Team B', rating: 2200, memberCount: 0, raceCount: 15 },
|
{ id: 't2', name: 'Team B', rating: 2200, memberCount: 0, raceCount: 15 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockDrivers = [
|
const mockDrivers: LeaderboardDriverData[] = [
|
||||||
{ id: 'd1', name: 'Alice', rating: 2000, raceCount: 10, teamId: 't1', teamName: 'Team A' },
|
{ id: 'd1', name: 'Alice', rating: 2000, raceCount: 10, teamId: 't1', teamName: 'Team A' },
|
||||||
{ id: 'd2', name: 'Bob', rating: 1500, raceCount: 5, teamId: 't1', teamName: 'Team A' },
|
{ id: 'd2', name: 'Bob', rating: 1500, raceCount: 5, teamId: 't1', teamName: 'Team A' },
|
||||||
{ id: 'd3', name: 'Charlie', rating: 1800, raceCount: 8, teamId: 't2', teamName: 'Team B' },
|
{ id: 'd3', name: 'Charlie', rating: 1800, raceCount: 8, teamId: 't2', teamName: 'Team B' },
|
||||||
@@ -22,12 +24,15 @@ describe('GetTeamRankingsUseCase', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLeaderboardsRepository = {
|
mockLeaderboardsRepository = {
|
||||||
findAllTeams: vi.fn().mockResolvedValue([...mockTeams]),
|
findAllTeams: vi.fn().mockResolvedValue([...mockTeams]) as unknown as Mock,
|
||||||
findAllDrivers: vi.fn().mockResolvedValue([...mockDrivers]),
|
findAllDrivers: vi.fn().mockResolvedValue([...mockDrivers]) as unknown as Mock,
|
||||||
|
findDriversByTeamId: vi.fn() as unknown as Mock,
|
||||||
};
|
};
|
||||||
mockEventPublisher = {
|
mockEventPublisher = {
|
||||||
publishTeamRankingsAccessed: vi.fn().mockResolvedValue(undefined),
|
publishTeamRankingsAccessed: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
publishLeaderboardsError: vi.fn().mockResolvedValue(undefined),
|
publishLeaderboardsError: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
publishGlobalLeaderboardsAccessed: vi.fn() as unknown as Mock,
|
||||||
|
publishDriverRankingsAccessed: vi.fn() as unknown as Mock,
|
||||||
};
|
};
|
||||||
ports = {
|
ports = {
|
||||||
leaderboardsRepository: mockLeaderboardsRepository,
|
leaderboardsRepository: mockLeaderboardsRepository,
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ export interface GetTeamRankingsUseCasePorts {
|
|||||||
eventPublisher: LeaderboardsEventPublisher;
|
eventPublisher: LeaderboardsEventPublisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DiscoveredTeam {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
rating: number;
|
||||||
|
memberCount: number;
|
||||||
|
raceCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class GetTeamRankingsUseCase {
|
export class GetTeamRankingsUseCase {
|
||||||
constructor(private readonly ports: GetTeamRankingsUseCasePorts) {}
|
constructor(private readonly ports: GetTeamRankingsUseCasePorts) {}
|
||||||
|
|
||||||
@@ -56,7 +64,7 @@ export class GetTeamRankingsUseCase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Discover teams that only exist in the drivers repository
|
// Discover teams that only exist in the drivers repository
|
||||||
const discoveredTeams: any[] = [];
|
const discoveredTeams: DiscoveredTeam[] = [];
|
||||||
driverCounts.forEach((count, teamId) => {
|
driverCounts.forEach((count, teamId) => {
|
||||||
if (!allTeams.some(t => t.id === teamId)) {
|
if (!allTeams.some(t => t.id === teamId)) {
|
||||||
const driverWithTeam = allDrivers.find(d => d.teamId === teamId);
|
const driverWithTeam = allDrivers.find(d => d.teamId === teamId);
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
export interface ScoringSystem {
|
||||||
|
// Define scoring system properties based on your domain
|
||||||
|
// This is a placeholder - adjust based on actual scoring system structure
|
||||||
|
pointsPerPosition?: Record<number, number>;
|
||||||
|
bonusPoints?: {
|
||||||
|
polePosition?: number;
|
||||||
|
fastestLap?: number;
|
||||||
|
cleanRace?: number;
|
||||||
|
};
|
||||||
|
penalties?: {
|
||||||
|
timePenalty?: number;
|
||||||
|
pointsDeduction?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface LeagueCreateCommand {
|
export interface LeagueCreateCommand {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -16,7 +31,7 @@ export interface LeagueCreateCommand {
|
|||||||
tracks?: string[];
|
tracks?: string[];
|
||||||
|
|
||||||
// Scoring
|
// Scoring
|
||||||
scoringSystem?: any;
|
scoringSystem?: ScoringSystem;
|
||||||
bonusPointsEnabled: boolean;
|
bonusPointsEnabled: boolean;
|
||||||
penaltiesEnabled: boolean;
|
penaltiesEnabled: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ScoringSystem } from './LeagueCreateCommand';
|
||||||
|
|
||||||
export interface LeagueCreatedEvent {
|
export interface LeagueCreatedEvent {
|
||||||
type: 'LeagueCreatedEvent';
|
type: 'LeagueCreatedEvent';
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
@@ -5,10 +7,33 @@ export interface LeagueCreatedEvent {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LeagueUpdates {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
visibility?: 'public' | 'private';
|
||||||
|
maxDrivers?: number;
|
||||||
|
approvalRequired?: boolean;
|
||||||
|
lateJoinAllowed?: boolean;
|
||||||
|
raceFrequency?: string;
|
||||||
|
raceDay?: string;
|
||||||
|
raceTime?: string;
|
||||||
|
tracks?: string[];
|
||||||
|
scoringSystem?: ScoringSystem;
|
||||||
|
bonusPointsEnabled?: boolean;
|
||||||
|
penaltiesEnabled?: boolean;
|
||||||
|
protestsEnabled?: boolean;
|
||||||
|
appealsEnabled?: boolean;
|
||||||
|
stewardTeam?: string[];
|
||||||
|
gameType?: string;
|
||||||
|
skillLevel?: string;
|
||||||
|
category?: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface LeagueUpdatedEvent {
|
export interface LeagueUpdatedEvent {
|
||||||
type: 'LeagueUpdatedEvent';
|
type: 'LeagueUpdatedEvent';
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
updates: Partial<any>;
|
updates: Partial<LeagueUpdates>;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ScoringSystem } from './LeagueCreateCommand';
|
||||||
|
|
||||||
export interface LeagueData {
|
export interface LeagueData {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -20,7 +22,7 @@ export interface LeagueData {
|
|||||||
tracks: string[] | null;
|
tracks: string[] | null;
|
||||||
|
|
||||||
// Scoring
|
// Scoring
|
||||||
scoringSystem: any | null;
|
scoringSystem: ScoringSystem | null;
|
||||||
bonusPointsEnabled: boolean;
|
bonusPointsEnabled: boolean;
|
||||||
penaltiesEnabled: boolean;
|
penaltiesEnabled: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,63 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||||
import { CreateLeagueUseCase } from './CreateLeagueUseCase';
|
import { CreateLeagueUseCase } from './CreateLeagueUseCase';
|
||||||
import { LeagueCreateCommand } from '../ports/LeagueCreateCommand';
|
import { LeagueCreateCommand } from '../ports/LeagueCreateCommand';
|
||||||
|
import { LeagueRepository } from '../ports/LeagueRepository';
|
||||||
|
import { LeagueEventPublisher } from '../ports/LeagueEventPublisher';
|
||||||
|
|
||||||
describe('CreateLeagueUseCase', () => {
|
describe('CreateLeagueUseCase', () => {
|
||||||
let mockLeagueRepository: any;
|
let mockLeagueRepository: LeagueRepository;
|
||||||
let mockEventPublisher: any;
|
let mockEventPublisher: LeagueEventPublisher;
|
||||||
let useCase: CreateLeagueUseCase;
|
let useCase: CreateLeagueUseCase;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLeagueRepository = {
|
mockLeagueRepository = {
|
||||||
create: vi.fn().mockImplementation((data) => Promise.resolve(data)),
|
create: vi.fn().mockImplementation((data) => Promise.resolve(data)) as unknown as Mock,
|
||||||
updateStats: vi.fn().mockResolvedValue(undefined),
|
findById: vi.fn() as unknown as Mock,
|
||||||
updateFinancials: vi.fn().mockResolvedValue(undefined),
|
findByName: vi.fn() as unknown as Mock,
|
||||||
updateStewardingMetrics: vi.fn().mockResolvedValue(undefined),
|
findByOwner: vi.fn() as unknown as Mock,
|
||||||
updatePerformanceMetrics: vi.fn().mockResolvedValue(undefined),
|
search: vi.fn() as unknown as Mock,
|
||||||
updateRatingMetrics: vi.fn().mockResolvedValue(undefined),
|
update: vi.fn() as unknown as Mock,
|
||||||
updateTrendMetrics: vi.fn().mockResolvedValue(undefined),
|
delete: vi.fn() as unknown as Mock,
|
||||||
updateSuccessRateMetrics: vi.fn().mockResolvedValue(undefined),
|
getStats: vi.fn() as unknown as Mock,
|
||||||
updateResolutionTimeMetrics: vi.fn().mockResolvedValue(undefined),
|
updateStats: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
updateComplexSuccessRateMetrics: vi.fn().mockResolvedValue(undefined),
|
getFinancials: vi.fn() as unknown as Mock,
|
||||||
updateComplexResolutionTimeMetrics: vi.fn().mockResolvedValue(undefined),
|
updateFinancials: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
getStewardingMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateStewardingMetrics: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
getPerformanceMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updatePerformanceMetrics: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
getRatingMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateRatingMetrics: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
getTrendMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateTrendMetrics: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
getSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateSuccessRateMetrics: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
getResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateResolutionTimeMetrics: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
getComplexSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateComplexSuccessRateMetrics: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
getComplexResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateComplexResolutionTimeMetrics: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
getLeagueMembers: vi.fn() as unknown as Mock,
|
||||||
|
getPendingRequests: vi.fn() as unknown as Mock,
|
||||||
|
addLeagueMembers: vi.fn() as unknown as Mock,
|
||||||
|
updateLeagueMember: vi.fn() as unknown as Mock,
|
||||||
|
removeLeagueMember: vi.fn() as unknown as Mock,
|
||||||
|
addPendingRequests: vi.fn() as unknown as Mock,
|
||||||
|
removePendingRequest: vi.fn() as unknown as Mock,
|
||||||
};
|
};
|
||||||
mockEventPublisher = {
|
mockEventPublisher = {
|
||||||
emitLeagueCreated: vi.fn().mockResolvedValue(undefined),
|
emitLeagueCreated: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
emitLeagueUpdated: vi.fn() as unknown as Mock,
|
||||||
|
emitLeagueDeleted: vi.fn() as unknown as Mock,
|
||||||
|
emitLeagueAccessed: vi.fn() as unknown as Mock,
|
||||||
|
emitLeagueRosterAccessed: vi.fn() as unknown as Mock,
|
||||||
|
getLeagueCreatedEventCount: vi.fn().mockReturnValue(0) as unknown as Mock,
|
||||||
|
getLeagueUpdatedEventCount: vi.fn().mockReturnValue(0) as unknown as Mock,
|
||||||
|
getLeagueDeletedEventCount: vi.fn().mockReturnValue(0) as unknown as Mock,
|
||||||
|
getLeagueAccessedEventCount: vi.fn().mockReturnValue(0) as unknown as Mock,
|
||||||
|
getLeagueRosterAccessedEventCount: vi.fn().mockReturnValue(0) as unknown as Mock,
|
||||||
|
clear: vi.fn() as unknown as Mock,
|
||||||
};
|
};
|
||||||
useCase = new CreateLeagueUseCase(mockLeagueRepository, mockEventPublisher);
|
useCase = new CreateLeagueUseCase(mockLeagueRepository, mockEventPublisher);
|
||||||
});
|
});
|
||||||
@@ -51,12 +86,12 @@ describe('CreateLeagueUseCase', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if name is missing', async () => {
|
it('should throw error if name is missing', async () => {
|
||||||
const command: any = { ownerId: 'owner-1' };
|
const command = { ownerId: 'owner-1' } as unknown as LeagueCreateCommand;
|
||||||
await expect(useCase.execute(command)).rejects.toThrow('League name is required');
|
await expect(useCase.execute(command)).rejects.toThrow('League name is required');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if ownerId is missing', async () => {
|
it('should throw error if ownerId is missing', async () => {
|
||||||
const command: any = { name: 'League' };
|
const command = { name: 'League' } as unknown as LeagueCreateCommand;
|
||||||
await expect(useCase.execute(command)).rejects.toThrow('Owner ID is required');
|
await expect(useCase.execute(command)).rejects.toThrow('Owner ID is required');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,94 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||||
import { DemoteAdminUseCase } from './DemoteAdminUseCase';
|
import { DemoteAdminUseCase } from './DemoteAdminUseCase';
|
||||||
|
import { LeagueRepository } from '../ports/LeagueRepository';
|
||||||
|
import { DriverRepository } from '../../racing/domain/repositories/DriverRepository';
|
||||||
|
import { LeagueEventPublisher } from '../ports/LeagueEventPublisher';
|
||||||
|
|
||||||
describe('DemoteAdminUseCase', () => {
|
describe('DemoteAdminUseCase', () => {
|
||||||
let mockLeagueRepository: any;
|
let mockLeagueRepository: LeagueRepository;
|
||||||
let mockDriverRepository: any;
|
let mockDriverRepository: DriverRepository;
|
||||||
let mockEventPublisher: any;
|
let mockEventPublisher: LeagueEventPublisher;
|
||||||
let useCase: DemoteAdminUseCase;
|
let useCase: DemoteAdminUseCase;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLeagueRepository = {
|
mockLeagueRepository = {
|
||||||
updateLeagueMember: vi.fn().mockResolvedValue(undefined),
|
updateLeagueMember: vi.fn().mockResolvedValue(undefined) as unknown as Mock,
|
||||||
|
create: vi.fn() as unknown as Mock,
|
||||||
|
findById: vi.fn() as unknown as Mock,
|
||||||
|
findByName: vi.fn() as unknown as Mock,
|
||||||
|
findByOwner: vi.fn() as unknown as Mock,
|
||||||
|
search: vi.fn() as unknown as Mock,
|
||||||
|
update: vi.fn() as unknown as Mock,
|
||||||
|
delete: vi.fn() as unknown as Mock,
|
||||||
|
getStats: vi.fn() as unknown as Mock,
|
||||||
|
updateStats: vi.fn() as unknown as Mock,
|
||||||
|
getFinancials: vi.fn() as unknown as Mock,
|
||||||
|
updateFinancials: vi.fn() as unknown as Mock,
|
||||||
|
getStewardingMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateStewardingMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getPerformanceMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updatePerformanceMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getRatingMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateRatingMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getTrendMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateTrendMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getComplexSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateComplexSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getComplexResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateComplexResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getLeagueMembers: vi.fn() as unknown as Mock,
|
||||||
|
getPendingRequests: vi.fn() as unknown as Mock,
|
||||||
|
addLeagueMembers: vi.fn() as unknown as Mock,
|
||||||
|
removeLeagueMember: vi.fn() as unknown as Mock,
|
||||||
|
addPendingRequests: vi.fn() as unknown as Mock,
|
||||||
|
removePendingRequest: vi.fn() as unknown as Mock,
|
||||||
};
|
};
|
||||||
mockDriverRepository = {};
|
mockDriverRepository = {
|
||||||
mockEventPublisher = {};
|
findById: vi.fn() as unknown as Mock,
|
||||||
useCase = new DemoteAdminUseCase(mockLeagueRepository, mockDriverRepository, mockEventPublisher as any);
|
findByName: vi.fn() as unknown as Mock,
|
||||||
|
findByEmail: vi.fn() as unknown as Mock,
|
||||||
|
search: vi.fn() as unknown as Mock,
|
||||||
|
update: vi.fn() as unknown as Mock,
|
||||||
|
delete: vi.fn() as unknown as Mock,
|
||||||
|
getStats: vi.fn() as unknown as Mock,
|
||||||
|
updateStats: vi.fn() as unknown as Mock,
|
||||||
|
getPerformanceMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updatePerformanceMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getRatingMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateRatingMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getTrendMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateTrendMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getComplexSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateComplexSuccessRateMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getComplexResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
updateComplexResolutionTimeMetrics: vi.fn() as unknown as Mock,
|
||||||
|
getDriverMemberships: vi.fn() as unknown as Mock,
|
||||||
|
addDriverMembership: vi.fn() as unknown as Mock,
|
||||||
|
updateDriverMembership: vi.fn() as unknown as Mock,
|
||||||
|
removeDriverMembership: vi.fn() as unknown as Mock,
|
||||||
|
};
|
||||||
|
mockEventPublisher = {
|
||||||
|
emitLeagueCreated: vi.fn() as unknown as Mock,
|
||||||
|
emitLeagueUpdated: vi.fn() as unknown as Mock,
|
||||||
|
emitLeagueDeleted: vi.fn() as unknown as Mock,
|
||||||
|
emitLeagueAccessed: vi.fn() as unknown as Mock,
|
||||||
|
emitLeagueRosterAccessed: vi.fn() as unknown as Mock,
|
||||||
|
getLeagueCreatedEventCount: vi.fn() as unknown as Mock,
|
||||||
|
getLeagueUpdatedEventCount: vi.fn() as unknown as Mock,
|
||||||
|
getLeagueDeletedEventCount: vi.fn() as unknown as Mock,
|
||||||
|
getLeagueAccessedEventCount: vi.fn() as unknown as Mock,
|
||||||
|
getLeagueRosterAccessedEventCount: vi.fn() as unknown as Mock,
|
||||||
|
clear: vi.fn() as unknown as Mock,
|
||||||
|
};
|
||||||
|
useCase = new DemoteAdminUseCase(mockLeagueRepository, mockDriverRepository, mockEventPublisher);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update member role to member', async () => {
|
it('should update member role to member', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Unit tests for CalculateRatingUseCase
|
* Unit tests for CalculateRatingUseCase
|
||||||
*
|
*
|
||||||
* Tests business logic and orchestration using mocked ports.
|
* Tests business logic and orchestration using mocked ports.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -9,6 +9,11 @@ import { CalculateRatingUseCase } from './CalculateRatingUseCase';
|
|||||||
import { Driver } from '../../../racing/domain/entities/Driver';
|
import { Driver } from '../../../racing/domain/entities/Driver';
|
||||||
import { Race } from '../../../racing/domain/entities/Race';
|
import { Race } from '../../../racing/domain/entities/Race';
|
||||||
import { Result } from '../../../racing/domain/entities/result/Result';
|
import { Result } from '../../../racing/domain/entities/result/Result';
|
||||||
|
import { DriverRepository } from '../../../racing/domain/repositories/DriverRepository';
|
||||||
|
import { RaceRepository } from '../../../racing/domain/repositories/RaceRepository';
|
||||||
|
import { ResultRepository } from '../../../racing/domain/repositories/ResultRepository';
|
||||||
|
import { RatingRepository } from '../../ports/RatingRepository';
|
||||||
|
import { EventPublisher } from '../../../shared/ports/EventPublisher';
|
||||||
|
|
||||||
// Mock repositories and publisher
|
// Mock repositories and publisher
|
||||||
const mockDriverRepository = {
|
const mockDriverRepository = {
|
||||||
@@ -37,11 +42,11 @@ describe('CalculateRatingUseCase', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
useCase = new CalculateRatingUseCase({
|
useCase = new CalculateRatingUseCase({
|
||||||
driverRepository: mockDriverRepository as any,
|
driverRepository: mockDriverRepository as unknown as DriverRepository,
|
||||||
raceRepository: mockRaceRepository as any,
|
raceRepository: mockRaceRepository as unknown as RaceRepository,
|
||||||
resultRepository: mockResultRepository as any,
|
resultRepository: mockResultRepository as unknown as ResultRepository,
|
||||||
ratingRepository: mockRatingRepository as any,
|
ratingRepository: mockRatingRepository as unknown as RatingRepository,
|
||||||
eventPublisher: mockEventPublisher as any,
|
eventPublisher: mockEventPublisher as unknown as EventPublisher,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { RatingComponents } from '../../domain/RatingComponents';
|
|||||||
import { RatingCalculatedEvent } from '../../domain/events/RatingCalculatedEvent';
|
import { RatingCalculatedEvent } from '../../domain/events/RatingCalculatedEvent';
|
||||||
import { DriverId } from '../../../racing/domain/entities/DriverId';
|
import { DriverId } from '../../../racing/domain/entities/DriverId';
|
||||||
import { RaceId } from '../../../racing/domain/entities/RaceId';
|
import { RaceId } from '../../../racing/domain/entities/RaceId';
|
||||||
|
import { Result as RaceResult } from '../../../racing/domain/entities/result/Result';
|
||||||
|
|
||||||
export interface CalculateRatingUseCasePorts {
|
export interface CalculateRatingUseCasePorts {
|
||||||
driverRepository: DriverRepository;
|
driverRepository: DriverRepository;
|
||||||
@@ -84,12 +85,12 @@ export class CalculateRatingUseCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateComponents(driverResult: any, allResults: any[]): RatingComponents {
|
private calculateComponents(driverResult: RaceResult, allResults: RaceResult[]): RatingComponents {
|
||||||
const position = typeof driverResult.position === 'object' ? (typeof driverResult.position.toNumber === 'function' ? driverResult.position.toNumber() : driverResult.position.value) : driverResult.position;
|
const position = driverResult.position.toNumber();
|
||||||
const totalDrivers = allResults.length;
|
const totalDrivers = allResults.length;
|
||||||
const incidents = typeof driverResult.incidents === 'object' ? (typeof driverResult.incidents.toNumber === 'function' ? driverResult.incidents.toNumber() : driverResult.incidents.value) : driverResult.incidents;
|
const incidents = driverResult.incidents.toNumber();
|
||||||
const lapsCompleted = typeof driverResult.lapsCompleted === 'object' ? (typeof driverResult.lapsCompleted.toNumber === 'function' ? driverResult.lapsCompleted.toNumber() : driverResult.lapsCompleted.value) : (driverResult.lapsCompleted !== undefined ? driverResult.lapsCompleted : (driverResult.totalTime === 0 && (typeof position === 'object' ? position.value : position) > 0 ? 5 : (driverResult.points === 0 && (typeof position === 'object' ? position.value : position) > 0 ? 5 : 20)));
|
const startPosition = driverResult.startPosition.toNumber();
|
||||||
const startPosition = typeof driverResult.startPosition === 'object' ? driverResult.startPosition.toNumber() : driverResult.startPosition;
|
const points = driverResult.points;
|
||||||
|
|
||||||
// Results Strength: Based on position relative to field size
|
// Results Strength: Based on position relative to field size
|
||||||
const resultsStrength = this.calculateResultsStrength(position, totalDrivers);
|
const resultsStrength = this.calculateResultsStrength(position, totalDrivers);
|
||||||
@@ -104,10 +105,12 @@ export class CalculateRatingUseCase {
|
|||||||
const racecraft = this.calculateRacecraft(position, startPosition);
|
const racecraft = this.calculateRacecraft(position, startPosition);
|
||||||
|
|
||||||
// Reliability: Based on laps completed and DNF/DNS
|
// Reliability: Based on laps completed and DNF/DNS
|
||||||
const reliability = this.calculateReliability(lapsCompleted, position, driverResult.points);
|
// For the Result entity, we need to determine reliability based on position and points
|
||||||
|
// If position is 0 (DNS) or points are 0 (DNF), reliability is low
|
||||||
|
const reliability = this.calculateReliabilityFromResult(position, points);
|
||||||
|
|
||||||
// Team Contribution: Based on points scored
|
// Team Contribution: Based on points scored
|
||||||
const teamContribution = this.calculateTeamContribution(driverResult.points);
|
const teamContribution = this.calculateTeamContribution(points);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resultsStrength,
|
resultsStrength,
|
||||||
@@ -119,6 +122,21 @@ export class CalculateRatingUseCase {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private calculateReliabilityFromResult(position: number, points: number): number {
|
||||||
|
// DNS (Did Not Start) - position 0
|
||||||
|
if (position === 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNF (Did Not Finish) - no points but finished (position > 0)
|
||||||
|
if (points === 0 && position > 0) {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finished with points - high reliability
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
private calculateResultsStrength(position: number, totalDrivers: number): number {
|
private calculateResultsStrength(position: number, totalDrivers: number): number {
|
||||||
if (position <= 0) return 1; // DNF/DNS (ensure > 0)
|
if (position <= 0) return 1; // DNF/DNS (ensure > 0)
|
||||||
const drivers = totalDrivers || 1;
|
const drivers = totalDrivers || 1;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { Driver } from '../../../racing/domain/entities/Driver';
|
|||||||
import { Race } from '../../../racing/domain/entities/Race';
|
import { Race } from '../../../racing/domain/entities/Race';
|
||||||
import { Result } from '../../../racing/domain/entities/result/Result';
|
import { Result } from '../../../racing/domain/entities/result/Result';
|
||||||
import { Rating } from '../../domain/Rating';
|
import { Rating } from '../../domain/Rating';
|
||||||
|
import { DriverId } from '../../../racing/domain/entities/DriverId';
|
||||||
|
import { RaceId } from '../../../racing/domain/entities/RaceId';
|
||||||
|
|
||||||
const mockRatingRepository = {
|
const mockRatingRepository = {
|
||||||
findByDriverAndRace: vi.fn(),
|
findByDriverAndRace: vi.fn(),
|
||||||
@@ -92,8 +94,8 @@ describe('CalculateTeamContributionUseCase', () => {
|
|||||||
const points = 12.5; // 50% contribution
|
const points = 12.5; // 50% contribution
|
||||||
|
|
||||||
const existingRating = Rating.create({
|
const existingRating = Rating.create({
|
||||||
driverId: 'driver-1' as any, // Simplified for test
|
driverId: DriverId.create('driver-1'),
|
||||||
raceId: 'race-1' as any,
|
raceId: RaceId.create('race-1'),
|
||||||
rating: 1500,
|
rating: 1500,
|
||||||
components: {
|
components: {
|
||||||
resultsStrength: 80,
|
resultsStrength: 80,
|
||||||
@@ -106,10 +108,10 @@ describe('CalculateTeamContributionUseCase', () => {
|
|||||||
timestamp: new Date('2023-01-01')
|
timestamp: new Date('2023-01-01')
|
||||||
});
|
});
|
||||||
|
|
||||||
mockDriverRepository.findById.mockResolvedValue({ id: driverId } as any);
|
mockDriverRepository.findById.mockResolvedValue({ id: driverId });
|
||||||
mockRaceRepository.findById.mockResolvedValue({ id: raceId } as any);
|
mockRaceRepository.findById.mockResolvedValue({ id: raceId });
|
||||||
mockResultRepository.findByRaceId.mockResolvedValue([
|
mockResultRepository.findByRaceId.mockResolvedValue([
|
||||||
{ driverId: { toString: () => driverId }, points } as any
|
{ driverId: { toString: () => driverId }, points }
|
||||||
]);
|
]);
|
||||||
mockRatingRepository.findByDriverAndRace.mockResolvedValue(existingRating);
|
mockRatingRepository.findByDriverAndRace.mockResolvedValue(existingRating);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { GetRatingLeaderboardUseCase } from './GetRatingLeaderboardUseCase';
|
import { GetRatingLeaderboardUseCase } from './GetRatingLeaderboardUseCase';
|
||||||
import { Rating } from '../../domain/Rating';
|
import { Rating } from '../../domain/Rating';
|
||||||
|
import { DriverId } from '../../../racing/domain/entities/DriverId';
|
||||||
|
import { RaceId } from '../../../racing/domain/entities/RaceId';
|
||||||
|
|
||||||
const mockRatingRepository = {
|
const mockRatingRepository = {
|
||||||
findByDriver: vi.fn(),
|
findByDriver: vi.fn(),
|
||||||
@@ -37,37 +39,65 @@ describe('GetRatingLeaderboardUseCase', () => {
|
|||||||
|
|
||||||
const ratingsD1 = [
|
const ratingsD1 = [
|
||||||
Rating.create({
|
Rating.create({
|
||||||
driverId: 'd1' as any,
|
driverId: DriverId.create('d1'),
|
||||||
raceId: 'r1' as any,
|
raceId: RaceId.create('r1'),
|
||||||
rating: 1000,
|
rating: 1000,
|
||||||
components: {} as any,
|
components: {
|
||||||
|
resultsStrength: 0,
|
||||||
|
consistency: 0,
|
||||||
|
cleanDriving: 0,
|
||||||
|
racecraft: 0,
|
||||||
|
reliability: 0,
|
||||||
|
teamContribution: 0,
|
||||||
|
},
|
||||||
timestamp: new Date('2023-01-01')
|
timestamp: new Date('2023-01-01')
|
||||||
}),
|
}),
|
||||||
Rating.create({
|
Rating.create({
|
||||||
driverId: 'd1' as any,
|
driverId: DriverId.create('d1'),
|
||||||
raceId: 'r2' as any,
|
raceId: RaceId.create('r2'),
|
||||||
rating: 1200, // Latest for D1
|
rating: 1200, // Latest for D1
|
||||||
components: {} as any,
|
components: {
|
||||||
|
resultsStrength: 0,
|
||||||
|
consistency: 0,
|
||||||
|
cleanDriving: 0,
|
||||||
|
racecraft: 0,
|
||||||
|
reliability: 0,
|
||||||
|
teamContribution: 0,
|
||||||
|
},
|
||||||
timestamp: new Date('2023-01-02')
|
timestamp: new Date('2023-01-02')
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const ratingsD2 = [
|
const ratingsD2 = [
|
||||||
Rating.create({
|
Rating.create({
|
||||||
driverId: 'd2' as any,
|
driverId: DriverId.create('d2'),
|
||||||
raceId: 'r1' as any,
|
raceId: RaceId.create('r1'),
|
||||||
rating: 1500, // Latest for D2
|
rating: 1500, // Latest for D2
|
||||||
components: {} as any,
|
components: {
|
||||||
|
resultsStrength: 0,
|
||||||
|
consistency: 0,
|
||||||
|
cleanDriving: 0,
|
||||||
|
racecraft: 0,
|
||||||
|
reliability: 0,
|
||||||
|
teamContribution: 0,
|
||||||
|
},
|
||||||
timestamp: new Date('2023-01-01')
|
timestamp: new Date('2023-01-01')
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const ratingsD3 = [
|
const ratingsD3 = [
|
||||||
Rating.create({
|
Rating.create({
|
||||||
driverId: 'd3' as any,
|
driverId: DriverId.create('d3'),
|
||||||
raceId: 'r1' as any,
|
raceId: RaceId.create('r1'),
|
||||||
rating: 800, // Latest for D3
|
rating: 800, // Latest for D3
|
||||||
components: {} as any,
|
components: {
|
||||||
|
resultsStrength: 0,
|
||||||
|
consistency: 0,
|
||||||
|
cleanDriving: 0,
|
||||||
|
racecraft: 0,
|
||||||
|
reliability: 0,
|
||||||
|
teamContribution: 0,
|
||||||
|
},
|
||||||
timestamp: new Date('2023-01-01')
|
timestamp: new Date('2023-01-01')
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { SaveRatingUseCase } from './SaveRatingUseCase';
|
import { SaveRatingUseCase } from './SaveRatingUseCase';
|
||||||
|
import { RatingRepository } from '../../ports/RatingRepository';
|
||||||
|
|
||||||
const mockRatingRepository = {
|
const mockRatingRepository = {
|
||||||
save: vi.fn(),
|
save: vi.fn(),
|
||||||
@@ -15,7 +16,7 @@ describe('SaveRatingUseCase', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
useCase = new SaveRatingUseCase({
|
useCase = new SaveRatingUseCase({
|
||||||
ratingRepository: mockRatingRepository as any,
|
ratingRepository: mockRatingRepository as unknown as RatingRepository,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Rating Entity
|
* Rating Entity
|
||||||
*
|
*
|
||||||
* Represents a driver's rating calculated after a race.
|
* Represents a driver's rating calculated after a race.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -16,6 +16,14 @@ export interface RatingProps {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RatingJSON {
|
||||||
|
driverId: string;
|
||||||
|
raceId: string;
|
||||||
|
rating: number;
|
||||||
|
components: RatingComponents;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class Rating {
|
export class Rating {
|
||||||
private constructor(private readonly props: RatingProps) {}
|
private constructor(private readonly props: RatingProps) {}
|
||||||
|
|
||||||
@@ -43,7 +51,7 @@ export class Rating {
|
|||||||
return this.props.timestamp;
|
return this.props.timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): Record<string, any> {
|
toJSON(): RatingJSON {
|
||||||
return {
|
return {
|
||||||
driverId: this.driverId.toString(),
|
driverId: this.driverId.toString(),
|
||||||
raceId: this.raceId.toString(),
|
raceId: this.raceId.toString(),
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* RatingCalculatedEvent
|
* RatingCalculatedEvent
|
||||||
*
|
*
|
||||||
* Event published when a driver's rating is calculated.
|
* Event published when a driver's rating is calculated.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DomainEvent } from '../../../shared/ports/EventPublisher';
|
import { DomainEvent } from '../../../shared/ports/EventPublisher';
|
||||||
import { Rating } from '../Rating';
|
import { Rating, RatingJSON } from '../Rating';
|
||||||
|
|
||||||
|
export interface RatingCalculatedEventJSON {
|
||||||
|
type: string;
|
||||||
|
timestamp: string;
|
||||||
|
rating: RatingJSON;
|
||||||
|
}
|
||||||
|
|
||||||
export class RatingCalculatedEvent implements DomainEvent {
|
export class RatingCalculatedEvent implements DomainEvent {
|
||||||
readonly type = 'RatingCalculatedEvent';
|
readonly type = 'RatingCalculatedEvent';
|
||||||
readonly timestamp: Date;
|
readonly timestamp: Date;
|
||||||
|
[key: string]: unknown;
|
||||||
|
|
||||||
constructor(private readonly rating: Rating) {
|
constructor(private readonly rating: Rating) {
|
||||||
this.timestamp = new Date();
|
this.timestamp = new Date();
|
||||||
@@ -19,7 +26,7 @@ export class RatingCalculatedEvent implements DomainEvent {
|
|||||||
return this.rating;
|
return this.rating;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): Record<string, any> {
|
toJSON(): RatingCalculatedEventJSON {
|
||||||
return {
|
return {
|
||||||
type: this.type,
|
type: this.type,
|
||||||
timestamp: this.timestamp.toISOString(),
|
timestamp: this.timestamp.toISOString(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* EventPublisher Port
|
* EventPublisher Port
|
||||||
*
|
*
|
||||||
* Defines the interface for publishing domain events.
|
* Defines the interface for publishing domain events.
|
||||||
* This port is implemented by adapters that can publish events.
|
* This port is implemented by adapters that can publish events.
|
||||||
*/
|
*/
|
||||||
@@ -15,5 +15,5 @@ export interface EventPublisher {
|
|||||||
export interface DomainEvent {
|
export interface DomainEvent {
|
||||||
type: string;
|
type: string;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
[key: string]: any;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user