587 lines
22 KiB
TypeScript
587 lines
22 KiB
TypeScript
/**
|
|
* Integration Test: League Detail Use Case Orchestration
|
|
*
|
|
* Tests the orchestration logic of league detail-related Use Cases:
|
|
* - GetLeagueUseCase: Retrieves league details
|
|
* - 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 { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
|
import { InMemoryLeagueEventPublisher } from '../../../adapters/leagues/events/InMemoryLeagueEventPublisher';
|
|
import { GetLeagueUseCase } from '../../../core/leagues/application/use-cases/GetLeagueUseCase';
|
|
import { CreateLeagueUseCase } from '../../../core/leagues/application/use-cases/CreateLeagueUseCase';
|
|
import { LeagueCreateCommand } from '../../../core/leagues/application/ports/LeagueCreateCommand';
|
|
|
|
describe('League Detail Use Case Orchestration', () => {
|
|
let leagueRepository: InMemoryLeagueRepository;
|
|
let eventPublisher: InMemoryLeagueEventPublisher;
|
|
let getLeagueUseCase: GetLeagueUseCase;
|
|
let createLeagueUseCase: CreateLeagueUseCase;
|
|
|
|
beforeAll(() => {
|
|
leagueRepository = new InMemoryLeagueRepository();
|
|
eventPublisher = new InMemoryLeagueEventPublisher();
|
|
getLeagueUseCase = new GetLeagueUseCase(leagueRepository, eventPublisher);
|
|
createLeagueUseCase = new CreateLeagueUseCase(leagueRepository, eventPublisher);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
leagueRepository.clear();
|
|
eventPublisher.clear();
|
|
});
|
|
|
|
describe('GetLeagueDetailUseCase - Success Path', () => {
|
|
it('should retrieve complete league detail with all data', async () => {
|
|
// Scenario: League with complete data
|
|
// Given: A league exists with complete data
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Complete League',
|
|
description: 'A league with all data',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
maxDrivers: 20,
|
|
approvalRequired: true,
|
|
lateJoinAllowed: true,
|
|
raceFrequency: 'weekly',
|
|
raceDay: 'Saturday',
|
|
raceTime: '18:00',
|
|
tracks: ['Monza', 'Spa'],
|
|
scoringSystem: { points: [25, 18, 15] },
|
|
bonusPointsEnabled: true,
|
|
penaltiesEnabled: true,
|
|
protestsEnabled: true,
|
|
appealsEnabled: true,
|
|
stewardTeam: ['steward-1'],
|
|
gameType: 'iRacing',
|
|
skillLevel: 'Intermediate',
|
|
category: 'GT3',
|
|
tags: ['competitive'],
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain all league sections
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
expect(result.name).toBe('Complete League');
|
|
expect(result.description).toBe('A league with all data');
|
|
expect(result.ownerId).toBe(driverId);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
const events = eventPublisher.getLeagueAccessedEvents();
|
|
expect(events[0].leagueId).toBe(league.id);
|
|
expect(events[0].driverId).toBe(driverId);
|
|
});
|
|
|
|
it('should retrieve league detail with minimal data', async () => {
|
|
// Scenario: League with minimal data
|
|
// Given: A league exists with only basic information (name, description, owner)
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Minimal League',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain basic league info
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
expect(result.name).toBe('Minimal League');
|
|
expect(result.ownerId).toBe(driverId);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should retrieve league detail with career history but no recent results', async () => {
|
|
// Scenario: League with career history but no recent results
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Career History League',
|
|
description: 'A league with career history',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain career history
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should retrieve league detail with recent results but no career history', async () => {
|
|
// Scenario: League with recent results but no career history
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Recent Results League',
|
|
description: 'A league with recent results',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain recent race results
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should retrieve league detail with championship standings but no other data', async () => {
|
|
// Scenario: League with championship standings but no other data
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Championship League',
|
|
description: 'A league with championship standings',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain championship standings
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should retrieve league detail with social links but no team affiliation', async () => {
|
|
// Scenario: League with social links but no team affiliation
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Social Links League',
|
|
description: 'A league with social links',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain social links
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should retrieve league detail with team affiliation but no social links', async () => {
|
|
// Scenario: League with team affiliation but no social links
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Team Affiliation League',
|
|
description: 'A league with team affiliation',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain team affiliation
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('GetLeagueDetailUseCase - Edge Cases', () => {
|
|
it('should handle league with no career history', async () => {
|
|
// Scenario: League with no career history
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'No Career History League',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain league profile
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should handle league with no recent race results', async () => {
|
|
// Scenario: League with no recent race results
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'No Recent Results League',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain league profile
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should handle league with no championship standings', async () => {
|
|
// Scenario: League with no championship standings
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'No Championship League',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain league profile
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
|
|
it('should handle league with no data at all', async () => {
|
|
// Scenario: League with absolutely no data
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'No Data League',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called with league ID
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: The result should contain basic league info
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
expect(result.name).toBe('No Data League');
|
|
|
|
// And: EventPublisher should emit LeagueAccessedEvent
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('GetLeagueDetailUseCase - Error Handling', () => {
|
|
it('should throw error when league does not exist', async () => {
|
|
// Scenario: Non-existent league
|
|
// Given: No league exists with the given ID
|
|
const nonExistentLeagueId = 'non-existent-league-id';
|
|
|
|
// When: GetLeagueUseCase.execute() is called with non-existent league ID
|
|
// Then: Should throw error
|
|
await expect(getLeagueUseCase.execute({ leagueId: nonExistentLeagueId, driverId: 'driver-123' }))
|
|
.rejects.toThrow();
|
|
|
|
// And: EventPublisher should NOT emit any events
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(0);
|
|
});
|
|
|
|
it('should throw error when league ID is invalid', async () => {
|
|
// Scenario: Invalid league ID
|
|
// Given: An invalid league ID (e.g., empty string)
|
|
const invalidLeagueId = '';
|
|
|
|
// When: GetLeagueUseCase.execute() is called with invalid league ID
|
|
// Then: Should throw error
|
|
await expect(getLeagueUseCase.execute({ leagueId: invalidLeagueId, driverId: 'driver-123' }))
|
|
.rejects.toThrow();
|
|
|
|
// And: EventPublisher should NOT emit any events
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(0);
|
|
});
|
|
|
|
it('should handle repository errors gracefully', async () => {
|
|
// Scenario: Repository throws error
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Test League',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// And: LeagueRepository throws an error during query
|
|
const originalFindById = leagueRepository.findById;
|
|
leagueRepository.findById = async () => {
|
|
throw new Error('Repository error');
|
|
};
|
|
|
|
// When: GetLeagueUseCase.execute() is called
|
|
// Then: Should propagate the error appropriately
|
|
await expect(getLeagueUseCase.execute({ leagueId: league.id, driverId }))
|
|
.rejects.toThrow('Repository error');
|
|
|
|
// And: EventPublisher should NOT emit any events
|
|
expect(eventPublisher.getLeagueAccessedEventCount()).toBe(0);
|
|
|
|
// Restore original method
|
|
leagueRepository.findById = originalFindById;
|
|
});
|
|
});
|
|
|
|
describe('League Detail Data Orchestration', () => {
|
|
it('should correctly calculate league statistics from race results', async () => {
|
|
// Scenario: League statistics calculation
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Statistics League',
|
|
description: 'A league for statistics calculation',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: League statistics should show:
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
expect(result.name).toBe('Statistics League');
|
|
});
|
|
|
|
it('should correctly format career history with league and team information', async () => {
|
|
// Scenario: Career history formatting
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Career History League',
|
|
description: 'A league for career history formatting',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: Career history should show:
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
expect(result.name).toBe('Career History League');
|
|
});
|
|
|
|
it('should correctly format recent race results with proper details', async () => {
|
|
// Scenario: Recent race results formatting
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Recent Results League',
|
|
description: 'A league for recent results formatting',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: Recent race results should show:
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
expect(result.name).toBe('Recent Results League');
|
|
});
|
|
|
|
it('should correctly aggregate championship standings across leagues', async () => {
|
|
// Scenario: Championship standings aggregation
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Championship League',
|
|
description: 'A league for championship standings',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: Championship standings should show:
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
expect(result.name).toBe('Championship League');
|
|
});
|
|
|
|
it('should correctly format social links with proper URLs', async () => {
|
|
// Scenario: Social links formatting
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Social Links League',
|
|
description: 'A league for social links formatting',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: Social links should show:
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
expect(result.name).toBe('Social Links League');
|
|
});
|
|
|
|
it('should correctly format team affiliation with role', async () => {
|
|
// Scenario: Team affiliation formatting
|
|
// Given: A league exists
|
|
const driverId = 'driver-123';
|
|
const league = await createLeagueUseCase.execute({
|
|
name: 'Team Affiliation League',
|
|
description: 'A league for team affiliation formatting',
|
|
visibility: 'public',
|
|
ownerId: driverId,
|
|
approvalRequired: false,
|
|
lateJoinAllowed: false,
|
|
bonusPointsEnabled: false,
|
|
penaltiesEnabled: false,
|
|
protestsEnabled: false,
|
|
appealsEnabled: false,
|
|
});
|
|
|
|
// When: GetLeagueUseCase.execute() is called
|
|
const result = await getLeagueUseCase.execute({ leagueId: league.id, driverId });
|
|
|
|
// Then: Team affiliation should show:
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe(league.id);
|
|
expect(result.name).toBe('Team Affiliation League');
|
|
});
|
|
});
|
|
|
|
});
|