1049 lines
35 KiB
TypeScript
1049 lines
35 KiB
TypeScript
/**
|
|
* Integration Test: Team Rankings Use Case Orchestration
|
|
*
|
|
* Tests the orchestration logic of team rankings-related Use Cases:
|
|
* - GetTeamRankingsUseCase: Retrieves comprehensive list of all teams with search, filter, and sort capabilities
|
|
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
|
* - Uses In-Memory adapters for fast, deterministic testing
|
|
*
|
|
* Focus: Business logic orchestration, NOT UI rendering
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
import { InMemoryLeaderboardsRepository } from '../../../adapters/leaderboards/persistence/inmemory/InMemoryLeaderboardsRepository';
|
|
import { InMemoryLeaderboardsEventPublisher } from '../../../adapters/leaderboards/events/InMemoryLeaderboardsEventPublisher';
|
|
import { GetTeamRankingsUseCase } from '../../../core/leaderboards/application/use-cases/GetTeamRankingsUseCase';
|
|
import { ValidationError } from '../../../core/shared/errors/ValidationError';
|
|
|
|
describe('Team Rankings Use Case Orchestration', () => {
|
|
let leaderboardsRepository: InMemoryLeaderboardsRepository;
|
|
let eventPublisher: InMemoryLeaderboardsEventPublisher;
|
|
let getTeamRankingsUseCase: GetTeamRankingsUseCase;
|
|
|
|
beforeAll(() => {
|
|
leaderboardsRepository = new InMemoryLeaderboardsRepository();
|
|
eventPublisher = new InMemoryLeaderboardsEventPublisher();
|
|
getTeamRankingsUseCase = new GetTeamRankingsUseCase({
|
|
leaderboardsRepository,
|
|
eventPublisher,
|
|
});
|
|
});
|
|
|
|
beforeEach(() => {
|
|
leaderboardsRepository.clear();
|
|
eventPublisher.clear();
|
|
});
|
|
|
|
describe('GetTeamRankingsUseCase - Success Path', () => {
|
|
it('should retrieve all teams with complete data', async () => {
|
|
// Scenario: System has multiple teams with complete data
|
|
// Given: Multiple teams exist with various ratings, names, and member counts
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Racing Team A',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Speed Squad',
|
|
rating: 4.7,
|
|
memberCount: 3,
|
|
raceCount: 80,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Champions League',
|
|
rating: 4.3,
|
|
memberCount: 4,
|
|
raceCount: 60,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with default query
|
|
const result = await getTeamRankingsUseCase.execute({});
|
|
|
|
// Then: The result should contain all teams
|
|
expect(result.teams).toHaveLength(3);
|
|
|
|
// And: Each team entry should include rank, name, rating, member count, and race count
|
|
expect(result.teams[0]).toMatchObject({
|
|
rank: 1,
|
|
id: 'team-1',
|
|
name: 'Racing Team A',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
|
|
// And: Teams should be sorted by rating (highest first)
|
|
expect(result.teams[0].rating).toBe(4.9);
|
|
expect(result.teams[1].rating).toBe(4.7);
|
|
expect(result.teams[2].rating).toBe(4.3);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should retrieve teams with pagination', async () => {
|
|
// Scenario: System has many teams requiring pagination
|
|
// Given: More than 20 teams exist
|
|
for (let i = 1; i <= 25; i++) {
|
|
leaderboardsRepository.addTeam({
|
|
id: `team-${i}`,
|
|
name: `Team ${i}`,
|
|
rating: 5.0 - i * 0.1,
|
|
memberCount: 2 + i,
|
|
raceCount: 20 + i,
|
|
});
|
|
}
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with page=1, limit=20
|
|
const result = await getTeamRankingsUseCase.execute({ page: 1, limit: 20 });
|
|
|
|
// Then: The result should contain 20 teams
|
|
expect(result.teams).toHaveLength(20);
|
|
|
|
// And: The result should include pagination metadata (total, page, limit)
|
|
expect(result.pagination.total).toBe(25);
|
|
expect(result.pagination.page).toBe(1);
|
|
expect(result.pagination.limit).toBe(20);
|
|
expect(result.pagination.totalPages).toBe(2);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should retrieve teams with different page sizes', async () => {
|
|
// Scenario: User requests different page sizes
|
|
// Given: More than 50 teams exist
|
|
for (let i = 1; i <= 60; i++) {
|
|
leaderboardsRepository.addTeam({
|
|
id: `team-${i}`,
|
|
name: `Team ${i}`,
|
|
rating: 5.0 - i * 0.1,
|
|
memberCount: 2 + i,
|
|
raceCount: 20 + i,
|
|
});
|
|
}
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with limit=50
|
|
const result = await getTeamRankingsUseCase.execute({ limit: 50 });
|
|
|
|
// Then: The result should contain 50 teams
|
|
expect(result.teams).toHaveLength(50);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should retrieve teams with consistent ranking order', async () => {
|
|
// Scenario: Verify ranking consistency
|
|
// Given: Multiple teams exist with various ratings
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
rating: 4.9,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Team B',
|
|
rating: 4.7,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Team C',
|
|
rating: 4.3,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
const result = await getTeamRankingsUseCase.execute({});
|
|
|
|
// Then: Team ranks should be sequential (1, 2, 3...)
|
|
expect(result.teams[0].rank).toBe(1);
|
|
expect(result.teams[1].rank).toBe(2);
|
|
expect(result.teams[2].rank).toBe(3);
|
|
|
|
// And: No duplicate ranks should appear
|
|
const ranks = result.teams.map((t) => t.rank);
|
|
expect(new Set(ranks).size).toBe(ranks.length);
|
|
|
|
// And: All ranks should be sequential
|
|
for (let i = 0; i < ranks.length; i++) {
|
|
expect(ranks[i]).toBe(i + 1);
|
|
}
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should retrieve teams with accurate data', async () => {
|
|
// Scenario: Verify data accuracy
|
|
// Given: Teams exist with valid ratings, names, and member counts
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Racing Team A',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
const result = await getTeamRankingsUseCase.execute({});
|
|
|
|
// Then: All team ratings should be valid numbers
|
|
expect(result.teams[0].rating).toBeGreaterThan(0);
|
|
expect(typeof result.teams[0].rating).toBe('number');
|
|
|
|
// And: All team ranks should be sequential
|
|
expect(result.teams[0].rank).toBe(1);
|
|
|
|
// And: All team names should be non-empty strings
|
|
expect(result.teams[0].name).toBeTruthy();
|
|
expect(typeof result.teams[0].name).toBe('string');
|
|
|
|
// And: All member counts should be valid numbers
|
|
expect(result.teams[0].memberCount).toBeGreaterThan(0);
|
|
expect(typeof result.teams[0].memberCount).toBe('number');
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('GetTeamRankingsUseCase - Search Functionality', () => {
|
|
it('should search for teams by name', async () => {
|
|
// Scenario: User searches for a specific team
|
|
// Given: Teams exist with names: "Racing Team", "Speed Squad", "Champions League"
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Racing Team',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Speed Squad',
|
|
rating: 4.7,
|
|
memberCount: 3,
|
|
raceCount: 80,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Champions League',
|
|
rating: 4.3,
|
|
memberCount: 4,
|
|
raceCount: 60,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with search="Racing"
|
|
const result = await getTeamRankingsUseCase.execute({ search: 'Racing' });
|
|
|
|
// Then: The result should contain teams whose names contain "Racing"
|
|
expect(result.teams).toHaveLength(1);
|
|
expect(result.teams[0].name).toBe('Racing Team');
|
|
|
|
// And: The result should not contain teams whose names do not contain "Racing"
|
|
expect(result.teams.map((t) => t.name)).not.toContain('Speed Squad');
|
|
expect(result.teams.map((t) => t.name)).not.toContain('Champions League');
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should search for teams by partial name', async () => {
|
|
// Scenario: User searches with partial name
|
|
// Given: Teams exist with names: "Racing Team", "Racing Squad", "Racing League"
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Racing Team',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Racing Squad',
|
|
rating: 4.7,
|
|
memberCount: 3,
|
|
raceCount: 80,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Racing League',
|
|
rating: 4.3,
|
|
memberCount: 4,
|
|
raceCount: 60,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with search="Racing"
|
|
const result = await getTeamRankingsUseCase.execute({ search: 'Racing' });
|
|
|
|
// Then: The result should contain all teams whose names start with "Racing"
|
|
expect(result.teams).toHaveLength(3);
|
|
expect(result.teams.map((t) => t.name)).toContain('Racing Team');
|
|
expect(result.teams.map((t) => t.name)).toContain('Racing Squad');
|
|
expect(result.teams.map((t) => t.name)).toContain('Racing League');
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should handle case-insensitive search', async () => {
|
|
// Scenario: Search is case-insensitive
|
|
// Given: Teams exist with names: "Racing Team", "RACING SQUAD", "racing league"
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Racing Team',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'RACING SQUAD',
|
|
rating: 4.7,
|
|
memberCount: 3,
|
|
raceCount: 80,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'racing league',
|
|
rating: 4.3,
|
|
memberCount: 4,
|
|
raceCount: 60,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with search="racing"
|
|
const result = await getTeamRankingsUseCase.execute({ search: 'racing' });
|
|
|
|
// Then: The result should contain all teams whose names contain "racing" (case-insensitive)
|
|
expect(result.teams).toHaveLength(3);
|
|
expect(result.teams.map((t) => t.name)).toContain('Racing Team');
|
|
expect(result.teams.map((t) => t.name)).toContain('RACING SQUAD');
|
|
expect(result.teams.map((t) => t.name)).toContain('racing league');
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should return empty result when no teams match search', async () => {
|
|
// Scenario: Search returns no results
|
|
// Given: Teams exist
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Racing Team',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with search="NonExistentTeam"
|
|
const result = await getTeamRankingsUseCase.execute({ search: 'NonExistentTeam' });
|
|
|
|
// Then: The result should contain empty teams list
|
|
expect(result.teams).toHaveLength(0);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('GetTeamRankingsUseCase - Filter Functionality', () => {
|
|
it('should filter teams by rating range', async () => {
|
|
// Scenario: User filters teams by rating
|
|
// Given: Teams exist with ratings: 3.5, 4.0, 4.5, 5.0
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
rating: 3.5,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Team B',
|
|
rating: 4.0,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Team C',
|
|
rating: 4.5,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-4',
|
|
name: 'Team D',
|
|
rating: 5.0,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with minRating=4.0
|
|
const result = await getTeamRankingsUseCase.execute({ minRating: 4.0 });
|
|
|
|
// Then: The result should only contain teams with rating >= 4.0
|
|
expect(result.teams).toHaveLength(3);
|
|
expect(result.teams.every((t) => t.rating >= 4.0)).toBe(true);
|
|
|
|
// And: Teams with rating < 4.0 should not be visible
|
|
expect(result.teams.map((t) => t.name)).not.toContain('Team A');
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should filter teams by member count', async () => {
|
|
// Scenario: User filters teams by member count
|
|
// Given: Teams exist with various member counts
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
rating: 4.9,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Team B',
|
|
rating: 4.7,
|
|
memberCount: 5,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Team C',
|
|
rating: 4.3,
|
|
memberCount: 3,
|
|
raceCount: 20,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with minMemberCount=5
|
|
const result = await getTeamRankingsUseCase.execute({ minMemberCount: 5 });
|
|
|
|
// Then: The result should only contain teams with member count >= 5
|
|
expect(result.teams).toHaveLength(1);
|
|
expect(result.teams[0].memberCount).toBeGreaterThanOrEqual(5);
|
|
|
|
// And: Teams with fewer members should not be visible
|
|
expect(result.teams.map((t) => t.name)).not.toContain('Team A');
|
|
expect(result.teams.map((t) => t.name)).not.toContain('Team C');
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should filter teams by multiple criteria', async () => {
|
|
// Scenario: User applies multiple filters
|
|
// Given: Teams exist with various ratings and member counts
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Team B',
|
|
rating: 4.7,
|
|
memberCount: 3,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Team C',
|
|
rating: 4.3,
|
|
memberCount: 5,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-4',
|
|
name: 'Team D',
|
|
rating: 3.5,
|
|
memberCount: 5,
|
|
raceCount: 20,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with minRating=4.0 and minMemberCount=5
|
|
const result = await getTeamRankingsUseCase.execute({ minRating: 4.0, minMemberCount: 5 });
|
|
|
|
// Then: The result should only contain teams with rating >= 4.0 and member count >= 5
|
|
expect(result.teams).toHaveLength(2);
|
|
expect(result.teams.every((t) => t.rating >= 4.0 && t.memberCount >= 5)).toBe(true);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should handle empty filter results', async () => {
|
|
// Scenario: Filters return no results
|
|
// Given: Teams exist
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
rating: 3.5,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with minRating=10.0 (impossible)
|
|
const result = await getTeamRankingsUseCase.execute({ minRating: 10.0 });
|
|
|
|
// Then: The result should contain empty teams list
|
|
expect(result.teams).toHaveLength(0);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('GetTeamRankingsUseCase - Sort Functionality', () => {
|
|
it('should sort teams by rating (high to low)', async () => {
|
|
// Scenario: User sorts teams by rating
|
|
// Given: Teams exist with ratings: 3.5, 4.0, 4.5, 5.0
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
rating: 3.5,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Team B',
|
|
rating: 4.0,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Team C',
|
|
rating: 4.5,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-4',
|
|
name: 'Team D',
|
|
rating: 5.0,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with sortBy="rating", sortOrder="desc"
|
|
const result = await getTeamRankingsUseCase.execute({ sortBy: 'rating', sortOrder: 'desc' });
|
|
|
|
// Then: The result should be sorted by rating in descending order
|
|
expect(result.teams[0].rating).toBe(5.0);
|
|
expect(result.teams[1].rating).toBe(4.5);
|
|
expect(result.teams[2].rating).toBe(4.0);
|
|
expect(result.teams[3].rating).toBe(3.5);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should sort teams by name (A-Z)', async () => {
|
|
// Scenario: User sorts teams by name
|
|
// Given: Teams exist with names: "Zoe Team", "Alpha Squad", "Beta League"
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Zoe Team',
|
|
rating: 4.9,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Alpha Squad',
|
|
rating: 4.7,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Beta League',
|
|
rating: 4.3,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with sortBy="name", sortOrder="asc"
|
|
const result = await getTeamRankingsUseCase.execute({ sortBy: 'name', sortOrder: 'asc' });
|
|
|
|
// Then: The result should be sorted alphabetically by name
|
|
expect(result.teams[0].name).toBe('Alpha Squad');
|
|
expect(result.teams[1].name).toBe('Beta League');
|
|
expect(result.teams[2].name).toBe('Zoe Team');
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should sort teams by rank (low to high)', async () => {
|
|
// Scenario: User sorts teams by rank
|
|
// Given: Teams exist with various ranks
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
rating: 4.9,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Team B',
|
|
rating: 4.7,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Team C',
|
|
rating: 4.3,
|
|
memberCount: 2,
|
|
raceCount: 20,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with sortBy="rank", sortOrder="asc"
|
|
const result = await getTeamRankingsUseCase.execute({ sortBy: 'rank', sortOrder: 'asc' });
|
|
|
|
// Then: The result should be sorted by rank in ascending order
|
|
expect(result.teams[0].rank).toBe(1);
|
|
expect(result.teams[1].rank).toBe(2);
|
|
expect(result.teams[2].rank).toBe(3);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should sort teams by member count (high to low)', async () => {
|
|
// Scenario: User sorts teams by member count
|
|
// Given: Teams exist with various member counts
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Team B',
|
|
rating: 4.7,
|
|
memberCount: 3,
|
|
raceCount: 20,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Team C',
|
|
rating: 4.3,
|
|
memberCount: 4,
|
|
raceCount: 20,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with sortBy="memberCount", sortOrder="desc"
|
|
const result = await getTeamRankingsUseCase.execute({ sortBy: 'memberCount', sortOrder: 'desc' });
|
|
|
|
// Then: The result should be sorted by member count in descending order
|
|
expect(result.teams[0].memberCount).toBe(5);
|
|
expect(result.teams[1].memberCount).toBe(4);
|
|
expect(result.teams[2].memberCount).toBe(3);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('GetTeamRankingsUseCase - Edge Cases', () => {
|
|
it('should handle system with no teams', async () => {
|
|
// Scenario: System has no teams
|
|
// Given: No teams exist in the system
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
const result = await getTeamRankingsUseCase.execute({});
|
|
|
|
// Then: The result should contain empty teams list
|
|
expect(result.teams).toHaveLength(0);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should handle teams with same rating', async () => {
|
|
// Scenario: Multiple teams with identical ratings
|
|
// Given: Multiple teams exist with the same rating
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Zeta Team',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Alpha Team',
|
|
rating: 4.9,
|
|
memberCount: 3,
|
|
raceCount: 80,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Beta Team',
|
|
rating: 4.9,
|
|
memberCount: 4,
|
|
raceCount: 60,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
const result = await getTeamRankingsUseCase.execute({});
|
|
|
|
// Then: Teams should be sorted by rating
|
|
expect(result.teams[0].rating).toBe(4.9);
|
|
expect(result.teams[1].rating).toBe(4.9);
|
|
expect(result.teams[2].rating).toBe(4.9);
|
|
|
|
// And: Teams with same rating should have consistent ordering (e.g., by name)
|
|
expect(result.teams[0].name).toBe('Alpha Team');
|
|
expect(result.teams[1].name).toBe('Beta Team');
|
|
expect(result.teams[2].name).toBe('Zeta Team');
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should handle teams with no members', async () => {
|
|
// Scenario: Teams with no members
|
|
// Given: Teams exist with and without members
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Team A',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Team B',
|
|
rating: 4.7,
|
|
memberCount: 0,
|
|
raceCount: 80,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
const result = await getTeamRankingsUseCase.execute({});
|
|
|
|
// Then: All teams should be returned
|
|
expect(result.teams).toHaveLength(2);
|
|
|
|
// And: Teams without members should show member count as 0
|
|
expect(result.teams[0].memberCount).toBe(5);
|
|
expect(result.teams[1].memberCount).toBe(0);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should handle pagination with empty results', async () => {
|
|
// Scenario: Pagination with no results
|
|
// Given: No teams exist
|
|
// When: GetTeamRankingsUseCase.execute() is called with page=1, limit=20
|
|
const result = await getTeamRankingsUseCase.execute({ page: 1, limit: 20 });
|
|
|
|
// Then: The result should contain empty teams list
|
|
expect(result.teams).toHaveLength(0);
|
|
|
|
// And: Pagination metadata should show total=0
|
|
expect(result.pagination.total).toBe(0);
|
|
expect(result.pagination.page).toBe(1);
|
|
expect(result.pagination.limit).toBe(20);
|
|
expect(result.pagination.totalPages).toBe(0);
|
|
|
|
// And: EventPublisher should emit TeamRankingsAccessedEvent
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('GetTeamRankingsUseCase - Error Handling', () => {
|
|
it('should handle team repository errors gracefully', async () => {
|
|
// Scenario: Team repository throws error
|
|
// Given: LeaderboardsRepository throws an error during query
|
|
const originalFindAllTeams = leaderboardsRepository.findAllTeams.bind(leaderboardsRepository);
|
|
leaderboardsRepository.findAllTeams = async () => {
|
|
throw new Error('Team repository error');
|
|
};
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
try {
|
|
await getTeamRankingsUseCase.execute({});
|
|
// Should not reach here
|
|
expect(true).toBe(false);
|
|
} catch (error) {
|
|
// Then: Should propagate the error appropriately
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect((error as Error).message).toBe('Team repository error');
|
|
}
|
|
|
|
// And: EventPublisher should NOT emit any events
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(0);
|
|
|
|
// Restore original method
|
|
leaderboardsRepository.findAllTeams = originalFindAllTeams;
|
|
});
|
|
|
|
it('should handle driver repository errors gracefully', async () => {
|
|
// Scenario: Driver repository throws error
|
|
// Given: LeaderboardsRepository throws an error during query
|
|
const originalFindAllDrivers = leaderboardsRepository.findAllDrivers.bind(leaderboardsRepository);
|
|
leaderboardsRepository.findAllDrivers = async () => {
|
|
throw new Error('Driver repository error');
|
|
};
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
try {
|
|
await getTeamRankingsUseCase.execute({});
|
|
// Should not reach here
|
|
expect(true).toBe(false);
|
|
} catch (error) {
|
|
// Then: Should propagate the error appropriately
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect((error as Error).message).toBe('Driver repository error');
|
|
}
|
|
|
|
// And: EventPublisher should NOT emit any events
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(0);
|
|
|
|
// Restore original method
|
|
leaderboardsRepository.findAllDrivers = originalFindAllDrivers;
|
|
});
|
|
|
|
it('should handle invalid query parameters', async () => {
|
|
// Scenario: Invalid query parameters
|
|
// Given: Invalid parameters (e.g., negative page, invalid sort field)
|
|
// When: GetTeamRankingsUseCase.execute() is called with invalid parameters
|
|
try {
|
|
await getTeamRankingsUseCase.execute({ page: -1 });
|
|
// Should not reach here
|
|
expect(true).toBe(false);
|
|
} catch (error) {
|
|
// Then: Should throw ValidationError
|
|
expect(error).toBeInstanceOf(ValidationError);
|
|
}
|
|
|
|
// And: EventPublisher should NOT emit any events
|
|
expect(eventPublisher.getTeamRankingsAccessedEventCount()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Team Rankings Data Orchestration', () => {
|
|
it('should correctly calculate team rankings based on rating', async () => {
|
|
// Scenario: Team ranking calculation
|
|
// Given: Teams exist with ratings: 4.9, 4.7, 4.6, 4.3, 4.1
|
|
const ratings = [4.9, 4.7, 4.6, 4.3, 4.1];
|
|
ratings.forEach((rating, index) => {
|
|
leaderboardsRepository.addTeam({
|
|
id: `team-${index}`,
|
|
name: `Team ${index}`,
|
|
rating,
|
|
memberCount: 2 + index,
|
|
raceCount: 20 + index,
|
|
});
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
const result = await getTeamRankingsUseCase.execute({});
|
|
|
|
// Then: Team rankings should be:
|
|
// - Rank 1: Team with rating 4.9
|
|
// - Rank 2: Team with rating 4.7
|
|
// - Rank 3: Team with rating 4.6
|
|
// - Rank 4: Team with rating 4.3
|
|
// - Rank 5: Team with rating 4.1
|
|
expect(result.teams[0].rank).toBe(1);
|
|
expect(result.teams[0].rating).toBe(4.9);
|
|
expect(result.teams[1].rank).toBe(2);
|
|
expect(result.teams[1].rating).toBe(4.7);
|
|
expect(result.teams[2].rank).toBe(3);
|
|
expect(result.teams[2].rating).toBe(4.6);
|
|
expect(result.teams[3].rank).toBe(4);
|
|
expect(result.teams[3].rating).toBe(4.3);
|
|
expect(result.teams[4].rank).toBe(5);
|
|
expect(result.teams[4].rating).toBe(4.1);
|
|
});
|
|
|
|
it('should correctly format team entries with member count', async () => {
|
|
// Scenario: Team entry formatting
|
|
// Given: A team exists with members
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Racing Team A',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
const result = await getTeamRankingsUseCase.execute({});
|
|
|
|
// Then: Team entry should include:
|
|
// - Rank: Sequential number
|
|
// - Name: Team's name
|
|
// - Rating: Team's rating (formatted)
|
|
// - Member Count: Number of drivers in team
|
|
// - Race Count: Number of races completed
|
|
const team = result.teams[0];
|
|
expect(team.rank).toBe(1);
|
|
expect(team.name).toBe('Racing Team A');
|
|
expect(team.rating).toBe(4.9);
|
|
expect(team.memberCount).toBe(5);
|
|
expect(team.raceCount).toBe(100);
|
|
});
|
|
|
|
it('should correctly handle pagination metadata', async () => {
|
|
// Scenario: Pagination metadata calculation
|
|
// Given: 50 teams exist
|
|
for (let i = 1; i <= 50; i++) {
|
|
leaderboardsRepository.addTeam({
|
|
id: `team-${i}`,
|
|
name: `Team ${i}`,
|
|
rating: 5.0 - i * 0.1,
|
|
memberCount: 2 + i,
|
|
raceCount: 20 + i,
|
|
});
|
|
}
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with page=2, limit=20
|
|
const result = await getTeamRankingsUseCase.execute({ page: 2, limit: 20 });
|
|
|
|
// Then: Pagination metadata should include:
|
|
// - Total: 50
|
|
// - Page: 2
|
|
// - Limit: 20
|
|
// - Total Pages: 3
|
|
expect(result.pagination.total).toBe(50);
|
|
expect(result.pagination.page).toBe(2);
|
|
expect(result.pagination.limit).toBe(20);
|
|
expect(result.pagination.totalPages).toBe(3);
|
|
});
|
|
|
|
it('should correctly aggregate member counts from drivers', async () => {
|
|
// Scenario: Member count aggregation
|
|
// Given: A team exists with 5 drivers
|
|
// And: Each driver is affiliated with the team
|
|
leaderboardsRepository.addDriver({
|
|
id: 'driver-1',
|
|
name: 'Driver A',
|
|
rating: 5.0,
|
|
teamId: 'team-1',
|
|
teamName: 'Team 1',
|
|
raceCount: 10,
|
|
});
|
|
leaderboardsRepository.addDriver({
|
|
id: 'driver-2',
|
|
name: 'Driver B',
|
|
rating: 4.8,
|
|
teamId: 'team-1',
|
|
teamName: 'Team 1',
|
|
raceCount: 10,
|
|
});
|
|
leaderboardsRepository.addDriver({
|
|
id: 'driver-3',
|
|
name: 'Driver C',
|
|
rating: 4.5,
|
|
teamId: 'team-1',
|
|
teamName: 'Team 1',
|
|
raceCount: 10,
|
|
});
|
|
leaderboardsRepository.addDriver({
|
|
id: 'driver-4',
|
|
name: 'Driver D',
|
|
rating: 4.2,
|
|
teamId: 'team-1',
|
|
teamName: 'Team 1',
|
|
raceCount: 10,
|
|
});
|
|
leaderboardsRepository.addDriver({
|
|
id: 'driver-5',
|
|
name: 'Driver E',
|
|
rating: 4.0,
|
|
teamId: 'team-1',
|
|
teamName: 'Team 1',
|
|
raceCount: 10,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called
|
|
const result = await getTeamRankingsUseCase.execute({});
|
|
|
|
// Then: The team entry should show member count as 5
|
|
expect(result.teams[0].memberCount).toBe(5);
|
|
});
|
|
|
|
it('should correctly apply search, filter, and sort together', async () => {
|
|
// Scenario: Combined query operations
|
|
// Given: Teams exist with various names, ratings, and member counts
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-1',
|
|
name: 'Racing Team A',
|
|
rating: 4.9,
|
|
memberCount: 5,
|
|
raceCount: 100,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-2',
|
|
name: 'Racing Squad',
|
|
rating: 4.7,
|
|
memberCount: 3,
|
|
raceCount: 80,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-3',
|
|
name: 'Champions League',
|
|
rating: 4.3,
|
|
memberCount: 4,
|
|
raceCount: 60,
|
|
});
|
|
leaderboardsRepository.addTeam({
|
|
id: 'team-4',
|
|
name: 'Racing League',
|
|
rating: 3.5,
|
|
memberCount: 2,
|
|
raceCount: 40,
|
|
});
|
|
|
|
// When: GetTeamRankingsUseCase.execute() is called with:
|
|
// - search: "Racing"
|
|
// - minRating: 4.0
|
|
// - minMemberCount: 5
|
|
// - sortBy: "rating"
|
|
// - sortOrder: "desc"
|
|
const result = await getTeamRankingsUseCase.execute({
|
|
search: 'Racing',
|
|
minRating: 4.0,
|
|
minMemberCount: 5,
|
|
sortBy: 'rating',
|
|
sortOrder: 'desc',
|
|
});
|
|
|
|
// Then: The result should:
|
|
// - Only contain teams with rating >= 4.0
|
|
// - Only contain teams with member count >= 5
|
|
// - Only contain teams whose names contain "Racing"
|
|
// - Be sorted by rating in descending order
|
|
expect(result.teams).toHaveLength(1);
|
|
expect(result.teams[0].name).toBe('Racing Team A');
|
|
expect(result.teams[0].rating).toBe(4.9);
|
|
expect(result.teams[0].memberCount).toBe(5);
|
|
});
|
|
});
|
|
});
|