/** * 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); }); }); });