418 lines
12 KiB
TypeScript
418 lines
12 KiB
TypeScript
import { Team } from '@core/racing/domain/entities/Team';
|
|
import type { TeamRatingRepository } from '@core/racing/domain/repositories/TeamRatingRepository';
|
|
import type { TeamRepository } from '@core/racing/domain/repositories/TeamRepository';
|
|
import { TeamRatingSnapshot } from '@core/racing/domain/services/TeamRatingSnapshotCalculator';
|
|
import { TeamRatingValue } from '@core/racing/domain/value-objects/TeamRatingValue';
|
|
import type { Logger } from '@core/shared/domain/Logger';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { TeamRankingUseCase } from './TeamRankingUseCase';
|
|
|
|
// Mock repositories
|
|
class MockTeamRatingRepository implements TeamRatingRepository {
|
|
private snapshots: Map<string, TeamRatingSnapshot> = new Map();
|
|
|
|
async findByTeamId(teamId: string): Promise<TeamRatingSnapshot | null> {
|
|
return this.snapshots.get(teamId) || null;
|
|
}
|
|
|
|
async save(snapshot: TeamRatingSnapshot): Promise<TeamRatingSnapshot> {
|
|
this.snapshots.set(snapshot.teamId, snapshot);
|
|
return snapshot;
|
|
}
|
|
|
|
clear() {
|
|
this.snapshots.clear();
|
|
}
|
|
|
|
setSnapshot(teamId: string, snapshot: TeamRatingSnapshot) {
|
|
this.snapshots.set(teamId, snapshot);
|
|
}
|
|
}
|
|
|
|
class MockTeamRepository implements TeamRepository {
|
|
private teams: Map<string, Team> = new Map();
|
|
|
|
async findById(id: string): Promise<Team | null> {
|
|
return this.teams.get(id) || null;
|
|
}
|
|
|
|
async findAll(): Promise<Team[]> {
|
|
return Array.from(this.teams.values());
|
|
}
|
|
|
|
async findByLeagueId(leagueId: string): Promise<Team[]> {
|
|
return Array.from(this.teams.values()).filter(t =>
|
|
t.leagues.some(l => l.toString() === leagueId)
|
|
);
|
|
}
|
|
|
|
async create(team: Team): Promise<Team> {
|
|
this.teams.set(team.id, team);
|
|
return team;
|
|
}
|
|
|
|
async update(team: Team): Promise<Team> {
|
|
this.teams.set(team.id, team);
|
|
return team;
|
|
}
|
|
|
|
async delete(id: string): Promise<void> {
|
|
this.teams.delete(id);
|
|
}
|
|
|
|
async exists(id: string): Promise<boolean> {
|
|
return this.teams.has(id);
|
|
}
|
|
|
|
clear() {
|
|
this.teams.clear();
|
|
}
|
|
|
|
setTeam(team: Team) {
|
|
this.teams.set(team.id, team);
|
|
}
|
|
}
|
|
|
|
// Mock logger
|
|
const mockLogger: Logger = {
|
|
info: () => {},
|
|
error: () => {},
|
|
warn: () => {},
|
|
debug: () => {},
|
|
};
|
|
|
|
describe('TeamRankingUseCase', () => {
|
|
let useCase: TeamRankingUseCase;
|
|
let mockRatingRepo: MockTeamRatingRepository;
|
|
let mockTeamRepo: MockTeamRepository;
|
|
|
|
beforeEach(() => {
|
|
mockRatingRepo = new MockTeamRatingRepository();
|
|
mockTeamRepo = new MockTeamRepository();
|
|
useCase = new TeamRankingUseCase(mockRatingRepo, mockTeamRepo, mockLogger);
|
|
});
|
|
|
|
afterEach(() => {
|
|
mockRatingRepo.clear();
|
|
mockTeamRepo.clear();
|
|
});
|
|
|
|
describe('getAllTeamRankings', () => {
|
|
it('should return empty array when no teams exist', async () => {
|
|
const result = await useCase.getAllTeamRankings();
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should return empty array when no rating snapshots exist', async () => {
|
|
const team = Team.create({
|
|
id: 'team-123',
|
|
name: 'Test Team',
|
|
tag: 'TT',
|
|
description: 'A test team',
|
|
ownerId: 'driver-123',
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team);
|
|
|
|
const result = await useCase.getAllTeamRankings();
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should return single team ranking', async () => {
|
|
const teamId = 'team-123';
|
|
const team = Team.create({
|
|
id: teamId,
|
|
name: 'Test Team',
|
|
tag: 'TT',
|
|
description: 'A test team',
|
|
ownerId: 'driver-123',
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team);
|
|
|
|
const snapshot: TeamRatingSnapshot = {
|
|
teamId,
|
|
driving: TeamRatingValue.create(75),
|
|
adminTrust: TeamRatingValue.create(80),
|
|
overall: 76.5,
|
|
lastUpdated: new Date('2024-01-01'),
|
|
eventCount: 5,
|
|
};
|
|
mockRatingRepo.setSnapshot(teamId, snapshot);
|
|
|
|
const result = await useCase.getAllTeamRankings();
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toEqual({
|
|
teamId,
|
|
teamName: 'Test Team',
|
|
drivingRating: 75,
|
|
adminTrustRating: 80,
|
|
overallRating: 76.5,
|
|
eventCount: 5,
|
|
lastUpdated: new Date('2024-01-01'),
|
|
overallRank: 1,
|
|
});
|
|
});
|
|
|
|
it('should return multiple teams sorted by overall rating', async () => {
|
|
// Team 1
|
|
const team1 = Team.create({
|
|
id: 'team-1',
|
|
name: 'Team Alpha',
|
|
tag: 'TA',
|
|
description: 'Alpha team',
|
|
ownerId: 'driver-1',
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team1);
|
|
mockRatingRepo.setSnapshot('team-1', {
|
|
teamId: 'team-1',
|
|
driving: TeamRatingValue.create(80),
|
|
adminTrust: TeamRatingValue.create(70),
|
|
overall: 77,
|
|
lastUpdated: new Date('2024-01-01'),
|
|
eventCount: 10,
|
|
});
|
|
|
|
// Team 2
|
|
const team2 = Team.create({
|
|
id: 'team-2',
|
|
name: 'Team Beta',
|
|
tag: 'TB',
|
|
description: 'Beta team',
|
|
ownerId: 'driver-2',
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team2);
|
|
mockRatingRepo.setSnapshot('team-2', {
|
|
teamId: 'team-2',
|
|
driving: TeamRatingValue.create(90),
|
|
adminTrust: TeamRatingValue.create(85),
|
|
overall: 88,
|
|
lastUpdated: new Date('2024-01-02'),
|
|
eventCount: 15,
|
|
});
|
|
|
|
// Team 3
|
|
const team3 = Team.create({
|
|
id: 'team-3',
|
|
name: 'Team Gamma',
|
|
tag: 'TG',
|
|
description: 'Gamma team',
|
|
ownerId: 'driver-3',
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team3);
|
|
mockRatingRepo.setSnapshot('team-3', {
|
|
teamId: 'team-3',
|
|
driving: TeamRatingValue.create(60),
|
|
adminTrust: TeamRatingValue.create(65),
|
|
overall: 61.5,
|
|
lastUpdated: new Date('2024-01-03'),
|
|
eventCount: 3,
|
|
});
|
|
|
|
const result = await useCase.getAllTeamRankings();
|
|
|
|
expect(result).toHaveLength(3);
|
|
|
|
// Should be sorted by overall rating descending
|
|
expect(result[0]).toBeDefined();
|
|
expect(result[0]!.teamId).toBe('team-2');
|
|
expect(result[0]!.overallRank).toBe(1);
|
|
expect(result[0]!.overallRating).toBe(88);
|
|
|
|
expect(result[1]).toBeDefined();
|
|
expect(result[1]!.teamId).toBe('team-1');
|
|
expect(result[1]!.overallRank).toBe(2);
|
|
expect(result[1]!.overallRating).toBe(77);
|
|
|
|
expect(result[2]).toBeDefined();
|
|
expect(result[2]!.teamId).toBe('team-3');
|
|
expect(result[2]!.overallRank).toBe(3);
|
|
expect(result[2]!.overallRating).toBe(61.5);
|
|
});
|
|
|
|
it('should handle teams without snapshots gracefully', async () => {
|
|
// Team with snapshot
|
|
const team1 = Team.create({
|
|
id: 'team-1',
|
|
name: 'Team With Rating',
|
|
tag: 'TWR',
|
|
description: 'Has rating',
|
|
ownerId: 'driver-1',
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team1);
|
|
mockRatingRepo.setSnapshot('team-1', {
|
|
teamId: 'team-1',
|
|
driving: TeamRatingValue.create(70),
|
|
adminTrust: TeamRatingValue.create(70),
|
|
overall: 70,
|
|
lastUpdated: new Date('2024-01-01'),
|
|
eventCount: 5,
|
|
});
|
|
|
|
// Team without snapshot
|
|
const team2 = Team.create({
|
|
id: 'team-2',
|
|
name: 'Team Without Rating',
|
|
tag: 'TWR',
|
|
description: 'No rating',
|
|
ownerId: 'driver-2',
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team2);
|
|
|
|
const result = await useCase.getAllTeamRankings();
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toBeDefined();
|
|
expect(result[0]!.teamId).toBe('team-1');
|
|
expect(result[0]!.overallRank).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('getTeamRanking', () => {
|
|
it('should return null when team does not exist', async () => {
|
|
const result = await useCase.getTeamRanking('non-existent-team');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return null when team exists but has no snapshot', async () => {
|
|
const team = Team.create({
|
|
id: 'team-123',
|
|
name: 'Test Team',
|
|
tag: 'TT',
|
|
description: 'A test team',
|
|
ownerId: 'driver-123',
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team);
|
|
|
|
const result = await useCase.getTeamRanking('team-123');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return team ranking with correct rank', async () => {
|
|
// Setup multiple teams
|
|
const teams = [
|
|
{ id: 'team-1', name: 'Team A', rating: 85 },
|
|
{ id: 'team-2', name: 'Team B', rating: 90 },
|
|
{ id: 'team-3', name: 'Team C', rating: 75 },
|
|
];
|
|
|
|
for (const t of teams) {
|
|
const team = Team.create({
|
|
id: t.id,
|
|
name: t.name,
|
|
tag: t.name.substring(0, 2).toUpperCase(),
|
|
description: `${t.name} description`,
|
|
ownerId: `driver-${t.id}`,
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team);
|
|
mockRatingRepo.setSnapshot(t.id, {
|
|
teamId: t.id,
|
|
driving: TeamRatingValue.create(t.rating),
|
|
adminTrust: TeamRatingValue.create(t.rating),
|
|
overall: t.rating,
|
|
lastUpdated: new Date('2024-01-01'),
|
|
eventCount: 5,
|
|
});
|
|
}
|
|
|
|
// Get ranking for team-2 (should be rank 1 with rating 90)
|
|
const result = await useCase.getTeamRanking('team-2');
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.teamId).toBe('team-2');
|
|
expect(result?.teamName).toBe('Team B');
|
|
expect(result?.overallRating).toBe(90);
|
|
expect(result?.overallRank).toBe(1);
|
|
});
|
|
|
|
it('should calculate correct rank for middle team', async () => {
|
|
// Setup teams
|
|
const teams = [
|
|
{ id: 'team-1', name: 'Team A', rating: 90 },
|
|
{ id: 'team-2', name: 'Team B', rating: 80 },
|
|
{ id: 'team-3', name: 'Team C', rating: 70 },
|
|
];
|
|
|
|
for (const t of teams) {
|
|
const team = Team.create({
|
|
id: t.id,
|
|
name: t.name,
|
|
tag: t.name.substring(0, 2).toUpperCase(),
|
|
description: `${t.name} description`,
|
|
ownerId: `driver-${t.id}`,
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team);
|
|
mockRatingRepo.setSnapshot(t.id, {
|
|
teamId: t.id,
|
|
driving: TeamRatingValue.create(t.rating),
|
|
adminTrust: TeamRatingValue.create(t.rating),
|
|
overall: t.rating,
|
|
lastUpdated: new Date('2024-01-01'),
|
|
eventCount: 5,
|
|
});
|
|
}
|
|
|
|
// Get ranking for team-2 (should be rank 2)
|
|
const result = await useCase.getTeamRanking('team-2');
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.overallRank).toBe(2);
|
|
});
|
|
|
|
it('should return complete team ranking data', async () => {
|
|
const teamId = 'team-123';
|
|
const team = Team.create({
|
|
id: teamId,
|
|
name: 'Complete Team',
|
|
tag: 'CT',
|
|
description: 'Complete team description',
|
|
ownerId: 'driver-123',
|
|
leagues: [],
|
|
});
|
|
mockTeamRepo.setTeam(team);
|
|
|
|
const snapshot: TeamRatingSnapshot = {
|
|
teamId,
|
|
driving: TeamRatingValue.create(82),
|
|
adminTrust: TeamRatingValue.create(78),
|
|
overall: 80.8,
|
|
lastUpdated: new Date('2024-01-15T10:30:00Z'),
|
|
eventCount: 25,
|
|
};
|
|
mockRatingRepo.setSnapshot(teamId, snapshot);
|
|
|
|
const result = await useCase.getTeamRanking(teamId);
|
|
|
|
expect(result).toEqual({
|
|
teamId,
|
|
teamName: 'Complete Team',
|
|
drivingRating: 82,
|
|
adminTrustRating: 78,
|
|
overallRating: 80.8,
|
|
eventCount: 25,
|
|
lastUpdated: new Date('2024-01-15T10:30:00Z'),
|
|
overallRank: 1,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should handle repository errors gracefully', async () => {
|
|
// Mock repository to throw error
|
|
mockTeamRepo.findAll = async () => {
|
|
throw new Error('Repository connection failed');
|
|
};
|
|
|
|
await expect(useCase.getAllTeamRankings()).rejects.toThrow('Repository connection failed');
|
|
});
|
|
});
|
|
}); |