diff --git a/adapters/payments/persistence/inmemory/InMemoryWalletRepository.ts b/adapters/payments/persistence/inmemory/InMemoryWalletRepository.ts index 93c7cb2d4..054c37861 100644 --- a/adapters/payments/persistence/inmemory/InMemoryWalletRepository.ts +++ b/adapters/payments/persistence/inmemory/InMemoryWalletRepository.ts @@ -5,52 +5,86 @@ import type { Transaction, Wallet } from '@core/payments/domain/entities/Wallet'; import type { WalletRepository, TransactionRepository } from '@core/payments/domain/repositories/WalletRepository'; import type { Logger } from '@core/shared/domain/Logger'; +import type { LeagueWalletRepository } from '@core/racing/domain/repositories/LeagueWalletRepository'; -const wallets: Map = new Map(); -const transactions: Map = new Map(); +const wallets: Map = new Map(); +const transactions: Map = new Map(); -export class InMemoryWalletRepository implements WalletRepository { +export class InMemoryWalletRepository implements WalletRepository, LeagueWalletRepository { constructor(private readonly logger: Logger) {} - async findById(id: string): Promise { + async findById(id: string): Promise { this.logger.debug('[InMemoryWalletRepository] findById', { id }); return wallets.get(id) || null; } - async findByLeagueId(leagueId: string): Promise { + async findByLeagueId(leagueId: string): Promise { this.logger.debug('[InMemoryWalletRepository] findByLeagueId', { leagueId }); - return Array.from(wallets.values()).find(w => w.leagueId === leagueId) || null; + return Array.from(wallets.values()).find(w => w.leagueId.toString() === leagueId) || null; } - async create(wallet: Wallet): Promise { + async create(wallet: any): Promise { this.logger.debug('[InMemoryWalletRepository] create', { wallet }); - wallets.set(wallet.id, wallet); + wallets.set(wallet.id.toString(), wallet); return wallet; } - async update(wallet: Wallet): Promise { + async update(wallet: any): Promise { this.logger.debug('[InMemoryWalletRepository] update', { wallet }); - wallets.set(wallet.id, wallet); + wallets.set(wallet.id.toString(), wallet); return wallet; } + + async delete(id: string): Promise { + wallets.delete(id); + } + + async exists(id: string): Promise { + return wallets.has(id); + } + + clear(): void { + wallets.clear(); + } } export class InMemoryTransactionRepository implements TransactionRepository { constructor(private readonly logger: Logger) {} - async findById(id: string): Promise { + async findById(id: string): Promise { this.logger.debug('[InMemoryTransactionRepository] findById', { id }); return transactions.get(id) || null; } - async findByWalletId(walletId: string): Promise { + async findByWalletId(walletId: string): Promise { this.logger.debug('[InMemoryTransactionRepository] findByWalletId', { walletId }); - return Array.from(transactions.values()).filter(t => t.walletId === walletId); + return Array.from(transactions.values()).filter(t => t.walletId.toString() === walletId); } - async create(transaction: Transaction): Promise { + async create(transaction: any): Promise { this.logger.debug('[InMemoryTransactionRepository] create', { transaction }); - transactions.set(transaction.id, transaction); + transactions.set(transaction.id.toString(), transaction); return transaction; } -} \ No newline at end of file + + async update(transaction: any): Promise { + transactions.set(transaction.id.toString(), transaction); + return transaction; + } + + async delete(id: string): Promise { + transactions.delete(id); + } + + async exists(id: string): Promise { + return transactions.has(id); + } + + findByType(type: any): Promise { + return Promise.resolve(Array.from(transactions.values()).filter(t => t.type === type)); + } + + clear(): void { + transactions.clear(); + } +} diff --git a/tests/integration/leagues/LeaguesTestContext.ts b/tests/integration/leagues/LeaguesTestContext.ts index b338ca2d5..77678f530 100644 --- a/tests/integration/leagues/LeaguesTestContext.ts +++ b/tests/integration/leagues/LeaguesTestContext.ts @@ -1,6 +1,7 @@ import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository'; import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository'; import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher'; +import type { Logger } from '../../../core/shared/domain/Logger'; import { CreateLeagueUseCase } from '../../../core/leagues/application/use-cases/CreateLeagueUseCase'; import { GetLeagueUseCase } from '../../../core/leagues/application/use-cases/GetLeagueUseCase'; import { GetLeagueRosterUseCase } from '../../../core/leagues/application/use-cases/GetLeagueRosterUseCase'; @@ -13,6 +14,32 @@ import { DemoteAdminUseCase } from '../../../core/leagues/application/use-cases/ import { RemoveMemberUseCase } from '../../../core/leagues/application/use-cases/RemoveMemberUseCase'; import { LeagueCreateCommand } from '../../../core/leagues/application/ports/LeagueCreateCommand'; +import { getPointsSystems } from '../../../adapters/bootstrap/PointsSystems'; +import { InMemoryLeagueRepository as InMemoryRacingLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository'; +import { InMemoryDriverRepository as InMemoryRacingDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository'; +import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository'; +import { InMemoryResultRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryResultRepository'; +import { InMemorySeasonRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonRepository'; +import { InMemorySeasonSponsorshipRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository'; +import { InMemoryRaceRegistrationRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository'; +import { InMemoryLeagueMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository'; +import { InMemoryStandingRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryStandingRepository'; +import { InMemoryPenaltyRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryPenaltyRepository'; +import { InMemoryProtestRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryProtestRepository'; +import { GetLeagueScheduleUseCase } from '../../../core/racing/application/use-cases/GetLeagueScheduleUseCase'; +import { CreateLeagueSeasonScheduleRaceUseCase } from '../../../core/racing/application/use-cases/CreateLeagueSeasonScheduleRaceUseCase'; +import { UpdateLeagueSeasonScheduleRaceUseCase } from '../../../core/racing/application/use-cases/UpdateLeagueSeasonScheduleRaceUseCase'; +import { DeleteLeagueSeasonScheduleRaceUseCase } from '../../../core/racing/application/use-cases/DeleteLeagueSeasonScheduleRaceUseCase'; +import { PublishLeagueSeasonScheduleUseCase } from '../../../core/racing/application/use-cases/PublishLeagueSeasonScheduleUseCase'; +import { UnpublishLeagueSeasonScheduleUseCase } from '../../../core/racing/application/use-cases/UnpublishLeagueSeasonScheduleUseCase'; +import { RegisterForRaceUseCase } from '../../../core/racing/application/use-cases/RegisterForRaceUseCase'; +import { WithdrawFromRaceUseCase } from '../../../core/racing/application/use-cases/WithdrawFromRaceUseCase'; +import { GetLeagueStandingsUseCase } from '../../../core/racing/application/use-cases/GetLeagueStandingsUseCase'; +import { InMemoryWalletRepository } from '../../../adapters/payments/persistence/inmemory/InMemoryWalletRepository'; +import { GetLeagueWalletUseCase } from '../../../core/racing/application/use-cases/GetLeagueWalletUseCase'; +import { WithdrawFromLeagueWalletUseCase } from '../../../core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase'; +import { InMemoryTransactionRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTransactionRepository'; + export class LeaguesTestContext { public readonly leagueRepository: InMemoryLeagueRepository; public readonly driverRepository: InMemoryDriverRepository; @@ -29,6 +56,35 @@ export class LeaguesTestContext { public readonly demoteAdminUseCase: DemoteAdminUseCase; public readonly removeMemberUseCase: RemoveMemberUseCase; + public readonly logger: Logger; + public readonly racingLeagueRepository: InMemoryRacingLeagueRepository; + public readonly seasonRepository: InMemorySeasonRepository; + public readonly seasonSponsorshipRepository: InMemorySeasonSponsorshipRepository; + public readonly raceRepository: InMemoryRaceRepository; + public readonly resultRepository: InMemoryResultRepository; + public readonly standingRepository: InMemoryStandingRepository; + public readonly racingDriverRepository: InMemoryRacingDriverRepository; + public readonly raceRegistrationRepository: InMemoryRaceRegistrationRepository; + public readonly leagueMembershipRepository: InMemoryLeagueMembershipRepository; + public readonly penaltyRepository: InMemoryPenaltyRepository; + public readonly protestRepository: InMemoryProtestRepository; + + public readonly getLeagueScheduleUseCase: GetLeagueScheduleUseCase; + public readonly createLeagueSeasonScheduleRaceUseCase: CreateLeagueSeasonScheduleRaceUseCase; + public readonly updateLeagueSeasonScheduleRaceUseCase: UpdateLeagueSeasonScheduleRaceUseCase; + public readonly deleteLeagueSeasonScheduleRaceUseCase: DeleteLeagueSeasonScheduleRaceUseCase; + public readonly publishLeagueSeasonScheduleUseCase: PublishLeagueSeasonScheduleUseCase; + public readonly unpublishLeagueSeasonScheduleUseCase: UnpublishLeagueSeasonScheduleUseCase; + public readonly registerForRaceUseCase: RegisterForRaceUseCase; + public readonly withdrawFromRaceUseCase: WithdrawFromRaceUseCase; + public readonly getLeagueStandingsUseCase: GetLeagueStandingsUseCase; + + public readonly walletRepository: InMemoryWalletRepository; + public readonly transactionRepository: InMemoryTransactionRepository; + + public readonly getLeagueWalletUseCase: GetLeagueWalletUseCase; + public readonly withdrawFromLeagueWalletUseCase: WithdrawFromLeagueWalletUseCase; + constructor() { this.leagueRepository = new InMemoryLeagueRepository(); this.driverRepository = new InMemoryDriverRepository(); @@ -44,12 +100,114 @@ export class LeaguesTestContext { this.promoteMemberUseCase = new PromoteMemberUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher); this.demoteAdminUseCase = new DemoteAdminUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher); this.removeMemberUseCase = new RemoveMemberUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher); + + this.logger = { + info: () => {}, + debug: () => {}, + warn: () => {}, + error: () => {}, + } as unknown as Logger; + + this.racingLeagueRepository = new InMemoryRacingLeagueRepository(this.logger); + this.seasonRepository = new InMemorySeasonRepository(this.logger); + this.seasonSponsorshipRepository = new InMemorySeasonSponsorshipRepository(this.logger); + this.raceRepository = new InMemoryRaceRepository(this.logger); + this.resultRepository = new InMemoryResultRepository(this.logger, this.raceRepository); + this.standingRepository = new InMemoryStandingRepository( + this.logger, + getPointsSystems(), + this.resultRepository, + this.raceRepository, + this.racingLeagueRepository, + ); + this.racingDriverRepository = new InMemoryRacingDriverRepository(this.logger); + this.raceRegistrationRepository = new InMemoryRaceRegistrationRepository(this.logger); + this.leagueMembershipRepository = new InMemoryLeagueMembershipRepository(this.logger); + this.penaltyRepository = new InMemoryPenaltyRepository(this.logger); + this.protestRepository = new InMemoryProtestRepository(this.logger); + + this.getLeagueScheduleUseCase = new GetLeagueScheduleUseCase( + this.racingLeagueRepository, + this.seasonRepository, + this.raceRepository, + this.logger, + ); + + let raceIdSequence = 0; + this.createLeagueSeasonScheduleRaceUseCase = new CreateLeagueSeasonScheduleRaceUseCase( + this.seasonRepository, + this.raceRepository, + this.logger, + { + generateRaceId: () => `race-${++raceIdSequence}`, + }, + ); + + this.updateLeagueSeasonScheduleRaceUseCase = new UpdateLeagueSeasonScheduleRaceUseCase( + this.seasonRepository, + this.raceRepository, + this.logger, + ); + + this.deleteLeagueSeasonScheduleRaceUseCase = new DeleteLeagueSeasonScheduleRaceUseCase( + this.seasonRepository, + this.raceRepository, + this.logger, + ); + + this.publishLeagueSeasonScheduleUseCase = new PublishLeagueSeasonScheduleUseCase(this.seasonRepository, this.logger); + this.unpublishLeagueSeasonScheduleUseCase = new UnpublishLeagueSeasonScheduleUseCase(this.seasonRepository, this.logger); + + this.registerForRaceUseCase = new RegisterForRaceUseCase( + this.raceRegistrationRepository, + this.leagueMembershipRepository, + this.logger, + ); + + this.withdrawFromRaceUseCase = new WithdrawFromRaceUseCase( + this.raceRepository, + this.raceRegistrationRepository, + this.logger, + ); + + this.getLeagueStandingsUseCase = new GetLeagueStandingsUseCase( + this.standingRepository, + this.racingDriverRepository, + ); + + this.walletRepository = new InMemoryWalletRepository(this.logger); + this.transactionRepository = new InMemoryTransactionRepository(this.logger); + + this.getLeagueWalletUseCase = new GetLeagueWalletUseCase( + this.racingLeagueRepository, + this.walletRepository, + this.transactionRepository, + ); + + this.withdrawFromLeagueWalletUseCase = new WithdrawFromLeagueWalletUseCase( + this.racingLeagueRepository, + this.walletRepository, + this.transactionRepository, + this.logger, + ); } public clear(): void { this.leagueRepository.clear(); this.driverRepository.clear(); this.eventPublisher.clear(); + + this.racingLeagueRepository.clear(); + this.seasonRepository.clear(); + this.seasonSponsorshipRepository.clear(); + this.raceRepository.clear(); + this.leagueMembershipRepository.clear(); + + (this.raceRegistrationRepository as unknown as { registrations: Map }).registrations?.clear?.(); + (this.resultRepository as unknown as { results: Map }).results?.clear?.(); + (this.standingRepository as unknown as { standings: Map }).standings?.clear?.(); + (this.racingDriverRepository as unknown as { drivers: Map }).drivers?.clear?.(); + (this.racingDriverRepository as unknown as { iracingIdIndex: Map }).iracingIdIndex?.clear?.(); } public async createLeague(command: Partial = {}) { diff --git a/tests/integration/leagues/league-schedule-use-cases.integration.test.ts b/tests/integration/leagues/league-schedule-use-cases.integration.test.ts deleted file mode 100644 index c33558e64..000000000 --- a/tests/integration/leagues/league-schedule-use-cases.integration.test.ts +++ /dev/null @@ -1,1303 +0,0 @@ -/** - * Integration Test: League Schedule Use Case Orchestration - * - * Tests the orchestration logic of league schedule-related Use Cases: - * - GetLeagueScheduleUseCase: Retrieves league schedule with race information - * - AddRaceUseCase: Admin adds a new race to the schedule - * - EditRaceUseCase: Admin edits an existing race - * - DeleteRaceUseCase: Admin deletes a race from the schedule - * - OpenRaceRegistrationUseCase: Admin opens race registration - * - CloseRaceRegistrationUseCase: Admin closes race registration - * - RegisterForRaceUseCase: Driver registers for a race - * - UnregisterFromRaceUseCase: Driver unregisters from a race - * - ImportRaceResultsUseCase: Admin imports race results - * - ExportRaceResultsUseCase: Admin exports race results - * - 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, afterAll, beforeEach } from 'vitest'; -import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository'; -import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository'; -import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository'; -import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher'; -import { GetLeagueScheduleUseCase } from '../../../core/leagues/use-cases/GetLeagueScheduleUseCase'; -import { AddRaceUseCase } from '../../../core/leagues/use-cases/AddRaceUseCase'; -import { EditRaceUseCase } from '../../../core/leagues/use-cases/EditRaceUseCase'; -import { DeleteRaceUseCase } from '../../../core/leagues/use-cases/DeleteRaceUseCase'; -import { OpenRaceRegistrationUseCase } from '../../../core/leagues/use-cases/OpenRaceRegistrationUseCase'; -import { CloseRaceRegistrationUseCase } from '../../../core/leagues/use-cases/CloseRaceRegistrationUseCase'; -import { RegisterForRaceUseCase } from '../../../core/leagues/use-cases/RegisterForRaceUseCase'; -import { UnregisterFromRaceUseCase } from '../../../core/leagues/use-cases/UnregisterFromRaceUseCase'; -import { ImportRaceResultsUseCase } from '../../../core/leagues/use-cases/ImportRaceResultsUseCase'; -import { ExportRaceResultsUseCase } from '../../../core/leagues/use-cases/ExportRaceResultsUseCase'; -import { LeagueScheduleQuery } from '../../../core/leagues/ports/LeagueScheduleQuery'; -import { AddRaceCommand } from '../../../core/leagues/ports/AddRaceCommand'; -import { EditRaceCommand } from '../../../core/leagues/ports/EditRaceCommand'; -import { DeleteRaceCommand } from '../../../core/leagues/ports/DeleteRaceCommand'; -import { OpenRaceRegistrationCommand } from '../../../core/leagues/ports/OpenRaceRegistrationCommand'; -import { CloseRaceRegistrationCommand } from '../../../core/leagues/ports/CloseRaceRegistrationCommand'; -import { RegisterForRaceCommand } from '../../../core/leagues/ports/RegisterForRaceCommand'; -import { UnregisterFromRaceCommand } from '../../../core/leagues/ports/UnregisterFromRaceCommand'; -import { ImportRaceResultsCommand } from '../../../core/leagues/ports/ImportRaceResultsCommand'; -import { ExportRaceResultsCommand } from '../../../core/leagues/ports/ExportRaceResultsCommand'; - -describe('League Schedule Use Case Orchestration', () => { - let leagueRepository: InMemoryLeagueRepository; - let raceRepository: InMemoryRaceRepository; - let driverRepository: InMemoryDriverRepository; - let eventPublisher: InMemoryEventPublisher; - let getLeagueScheduleUseCase: GetLeagueScheduleUseCase; - let addRaceUseCase: AddRaceUseCase; - let editRaceUseCase: EditRaceUseCase; - let deleteRaceUseCase: DeleteRaceUseCase; - let openRaceRegistrationUseCase: OpenRaceRegistrationUseCase; - let closeRaceRegistrationUseCase: CloseRaceRegistrationUseCase; - let registerForRaceUseCase: RegisterForRaceUseCase; - let unregisterFromRaceUseCase: UnregisterFromRaceUseCase; - let importRaceResultsUseCase: ImportRaceResultsUseCase; - let exportRaceResultsUseCase: ExportRaceResultsUseCase; - - beforeAll(() => { - // TODO: Initialize In-Memory repositories and event publisher - // leagueRepository = new InMemoryLeagueRepository(); - // raceRepository = new InMemoryRaceRepository(); - // driverRepository = new InMemoryDriverRepository(); - // eventPublisher = new InMemoryEventPublisher(); - // getLeagueScheduleUseCase = new GetLeagueScheduleUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - // addRaceUseCase = new AddRaceUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - // editRaceUseCase = new EditRaceUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - // deleteRaceUseCase = new DeleteRaceUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - // openRaceRegistrationUseCase = new OpenRaceRegistrationUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - // closeRaceRegistrationUseCase = new CloseRaceRegistrationUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - // registerForRaceUseCase = new RegisterForRaceUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - // unregisterFromRaceUseCase = new UnregisterFromRaceUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - // importRaceResultsUseCase = new ImportRaceResultsUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - // exportRaceResultsUseCase = new ExportRaceResultsUseCase({ - // leagueRepository, - // raceRepository, - // driverRepository, - // eventPublisher, - // }); - }); - - beforeEach(() => { - // TODO: Clear all In-Memory repositories before each test - // leagueRepository.clear(); - // raceRepository.clear(); - // driverRepository.clear(); - // eventPublisher.clear(); - }); - - describe('GetLeagueScheduleUseCase - Success Path', () => { - it('should retrieve complete league schedule with all races', async () => { - // TODO: Implement test - // Scenario: League with complete schedule - // Given: A league exists with multiple races - // And: The league has upcoming races - // And: The league has in-progress races - // And: The league has completed races with results - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain all races in the league - // And: Each race should display its track name - // And: Each race should display its car type - // And: Each race should display its date and time - // And: Each race should display its duration - // And: Each race should display its registration status - // And: Each race should display its status (upcoming/in-progress/completed) - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with only upcoming races', async () => { - // TODO: Implement test - // Scenario: League with only upcoming races - // Given: A league exists with only upcoming races - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain only upcoming races - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with only completed races', async () => { - // TODO: Implement test - // Scenario: League with only completed races - // Given: A league exists with only completed races - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain only completed races - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with only in-progress races', async () => { - // TODO: Implement test - // Scenario: League with only in-progress races - // Given: A league exists with only in-progress races - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain only in-progress races - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race results', async () => { - // TODO: Implement test - // Scenario: League with race results - // Given: A league exists with completed races that have results - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show results for completed races - // And: Results should include top finishers - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race registration count', async () => { - // TODO: Implement test - // Scenario: League with race registration count - // Given: A league exists with races that have registration counts - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show registration count for each race - // And: The count should be accurate - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race max drivers', async () => { - // TODO: Implement test - // Scenario: League with race max drivers - // Given: A league exists with races that have max drivers - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show max drivers for each race - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race available slots', async () => { - // TODO: Implement test - // Scenario: League with race available slots - // Given: A league exists with races that have available slots - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show available slots for each race - // And: The available slots should be calculated correctly - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race weather information', async () => { - // TODO: Implement test - // Scenario: League with race weather information - // Given: A league exists with races that have weather information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show weather information for each race - // And: Weather should include temperature, conditions, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race track layout', async () => { - // TODO: Implement test - // Scenario: League with race track layout - // Given: A league exists with races that have track layout information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show track layout information for each race - // And: Track layout should include length, turns, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race qualifying information', async () => { - // TODO: Implement test - // Scenario: League with race qualifying information - // Given: A league exists with races that have qualifying information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show qualifying information for each race - // And: Qualifying should include duration, format, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race practice information', async () => { - // TODO: Implement test - // Scenario: League with race practice information - // Given: A league exists with races that have practice information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show practice information for each race - // And: Practice should include duration, format, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race warmup information', async () => { - // TODO: Implement test - // Scenario: League with race warmup information - // Given: A league exists with races that have warmup information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show warmup information for each race - // And: Warmup should include duration, format, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race grid size', async () => { - // TODO: Implement test - // Scenario: League with race grid size - // Given: A league exists with races that have grid size - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show grid size for each race - // And: Grid size should be displayed as number of positions - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race pit lane information', async () => { - // TODO: Implement test - // Scenario: League with race pit lane information - // Given: A league exists with races that have pit lane information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show pit lane information for each race - // And: Pit lane should include duration, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race safety car information', async () => { - // TODO: Implement test - // Scenario: League with race safety car information - // Given: A league exists with races that have safety car information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show safety car information for each race - // And: Safety car should include deployment rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race virtual safety car information', async () => { - // TODO: Implement test - // Scenario: League with race virtual safety car information - // Given: A league exists with races that have virtual safety car information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show virtual safety car information for each race - // And: Virtual safety car should include deployment rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race FCY information', async () => { - // TODO: Implement test - // Scenario: League with race FCY information - // Given: A league exists with races that have FCY information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show FCY information for each race - // And: FCY should include deployment rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race caution periods information', async () => { - // TODO: Implement test - // Scenario: League with race caution periods information - // Given: A league exists with races that have caution periods information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show caution periods information for each race - // And: Caution periods should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race restart procedures information', async () => { - // TODO: Implement test - // Scenario: League with race restart procedures information - // Given: A league exists with races that have restart procedures information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show restart procedures information for each race - // And: Restart procedures should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race penalty information', async () => { - // TODO: Implement test - // Scenario: League with race penalty information - // Given: A league exists with races that have penalty information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show penalty information for each race - // And: Penalties should include types, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race protest information', async () => { - // TODO: Implement test - // Scenario: League with race protest information - // Given: A league exists with races that have protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show protest information for each race - // And: Protests should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race appeal information', async () => { - // TODO: Implement test - // Scenario: League with race appeal information - // Given: A league exists with races that have appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show appeal information for each race - // And: Appeals should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race stewarding information', async () => { - // TODO: Implement test - // Scenario: League with race stewarding information - // Given: A league exists with races that have stewarding information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show stewarding information for each race - // And: Stewarding should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race incident review information', async () => { - // TODO: Implement test - // Scenario: League with race incident review information - // Given: A league exists with races that have incident review information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show incident review information for each race - // And: Incident review should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race penalty appeal information', async () => { - // TODO: Implement test - // Scenario: League with race penalty appeal information - // Given: A league exists with races that have penalty appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show penalty appeal information for each race - // And: Penalty appeal should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race protest appeal information', async () => { - // TODO: Implement test - // Scenario: League with race protest appeal information - // Given: A league exists with races that have protest appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show protest appeal information for each race - // And: Protest appeal should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race stewarding action appeal information', async () => { - // TODO: Implement test - // Scenario: League with race stewarding action appeal information - // Given: A league exists with races that have stewarding action appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show stewarding action appeal information for each race - // And: Stewarding action appeal should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race penalty protest information', async () => { - // TODO: Implement test - // Scenario: League with race penalty protest information - // Given: A league exists with races that have penalty protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show penalty protest information for each race - // And: Penalty protest should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race stewarding action protest information', async () => { - // TODO: Implement test - // Scenario: League with race stewarding action protest information - // Given: A league exists with races that have stewarding action protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show stewarding action protest information for each race - // And: Stewarding action protest should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race penalty appeal protest information', async () => { - // TODO: Implement test - // Scenario: League with race penalty appeal protest information - // Given: A league exists with races that have penalty appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show penalty appeal protest information for each race - // And: Penalty appeal protest should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race stewarding action appeal protest information', async () => { - // TODO: Implement test - // Scenario: League with race stewarding action appeal protest information - // Given: A league exists with races that have stewarding action appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show stewarding action appeal protest information for each race - // And: Stewarding action appeal protest should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race penalty appeal protest stewarding action information', async () => { - // TODO: Implement test - // Scenario: League with race penalty appeal protest stewarding action information - // Given: A league exists with races that have penalty appeal protest stewarding action information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show penalty appeal protest stewarding action information for each race - // And: Penalty appeal protest stewarding action should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race stewarding action appeal protest penalty information', async () => { - // TODO: Implement test - // Scenario: League with race stewarding action appeal protest penalty information - // Given: A league exists with races that have stewarding action appeal protest penalty information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show stewarding action appeal protest penalty information for each race - // And: Stewarding action appeal protest penalty should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race penalty appeal protest stewarding action appeal information', async () => { - // TODO: Implement test - // Scenario: League with race penalty appeal protest stewarding action appeal information - // Given: A league exists with races that have penalty appeal protest stewarding action appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show penalty appeal protest stewarding action appeal information for each race - // And: Penalty appeal protest stewarding action appeal should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race stewarding action appeal protest penalty appeal information', async () => { - // TODO: Implement test - // Scenario: League with race stewarding action appeal protest penalty appeal information - // Given: A league exists with races that have stewarding action appeal protest penalty appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show stewarding action appeal protest penalty appeal information for each race - // And: Stewarding action appeal protest penalty appeal should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race penalty appeal protest stewarding action appeal protest information', async () => { - // TODO: Implement test - // Scenario: League with race penalty appeal protest stewarding action appeal protest information - // Given: A league exists with races that have penalty appeal protest stewarding action appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show penalty appeal protest stewarding action appeal protest information for each race - // And: Penalty appeal protest stewarding action appeal protest should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race stewarding action appeal protest penalty appeal protest information', async () => { - // TODO: Implement test - // Scenario: League with race stewarding action appeal protest penalty appeal protest information - // Given: A league exists with races that have stewarding action appeal protest penalty appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show stewarding action appeal protest penalty appeal protest information for each race - // And: Stewarding action appeal protest penalty appeal protest should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race penalty appeal protest stewarding action appeal protest penalty information', async () => { - // TODO: Implement test - // Scenario: League with race penalty appeal protest stewarding action appeal protest penalty information - // Given: A league exists with races that have penalty appeal protest stewarding action appeal protest penalty information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show penalty appeal protest stewarding action appeal protest penalty information for each race - // And: Penalty appeal protest stewarding action appeal protest penalty should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should retrieve league schedule with race stewarding action appeal protest penalty appeal protest penalty information', async () => { - // TODO: Implement test - // Scenario: League with race stewarding action appeal protest penalty appeal protest penalty information - // Given: A league exists with races that have stewarding action appeal protest penalty appeal protest penalty information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should show stewarding action appeal protest penalty appeal protest penalty information for each race - // And: Stewarding action appeal protest penalty appeal protest penalty should include rules, etc. - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - }); - - describe('GetLeagueScheduleUseCase - Edge Cases', () => { - it('should handle league with no races', async () => { - // TODO: Implement test - // Scenario: League with no races - // Given: A league exists - // And: The league has no races - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain empty schedule - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with only upcoming races', async () => { - // TODO: Implement test - // Scenario: League with only upcoming races - // Given: A league exists - // And: The league has only upcoming races - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain only upcoming races - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with only completed races', async () => { - // TODO: Implement test - // Scenario: League with only completed races - // Given: A league exists - // And: The league has only completed races - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain only completed races - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with only in-progress races', async () => { - // TODO: Implement test - // Scenario: League with only in-progress races - // Given: A league exists - // And: The league has only in-progress races - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain only in-progress races - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no results', async () => { - // TODO: Implement test - // Scenario: League with races but no results - // Given: A league exists - // And: The league has races but no results - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without results - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no registration count', async () => { - // TODO: Implement test - // Scenario: League with races but no registration count - // Given: A league exists - // And: The league has races but no registration count - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without registration count - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no max drivers', async () => { - // TODO: Implement test - // Scenario: League with races but no max drivers - // Given: A league exists - // And: The league has races but no max drivers - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without max drivers - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no available slots', async () => { - // TODO: Implement test - // Scenario: League with races but no available slots - // Given: A league exists - // And: The league has races but no available slots - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without available slots - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no weather information', async () => { - // TODO: Implement test - // Scenario: League with races but no weather information - // Given: A league exists - // And: The league has races but no weather information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without weather information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no track layout', async () => { - // TODO: Implement test - // Scenario: League with races but no track layout - // Given: A league exists - // And: The league has races but no track layout - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without track layout - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no qualifying information', async () => { - // TODO: Implement test - // Scenario: League with races but no qualifying information - // Given: A league exists - // And: The league has races but no qualifying information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without qualifying information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no practice information', async () => { - // TODO: Implement test - // Scenario: League with races but no practice information - // Given: A league exists - // And: The league has races but no practice information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without practice information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no warmup information', async () => { - // TODO: Implement test - // Scenario: League with races but no warmup information - // Given: A league exists - // And: The league has races but no warmup information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without warmup information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no grid size', async () => { - // TODO: Implement test - // Scenario: League with races but no grid size - // Given: A league exists - // And: The league has races but no grid size - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without grid size - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no pit lane information', async () => { - // TODO: Implement test - // Scenario: League with races but no pit lane information - // Given: A league exists - // And: The league has races but no pit lane information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without pit lane information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no safety car information', async () => { - // TODO: Implement test - // Scenario: League with races but no safety car information - // Given: A league exists - // And: The league has races but no safety car information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without safety car information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no virtual safety car information', async () => { - // TODO: Implement test - // Scenario: League with races but no virtual safety car information - // Given: A league exists - // And: The league has races but no virtual safety car information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without virtual safety car information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no FCY information', async () => { - // TODO: Implement test - // Scenario: League with races but no FCY information - // Given: A league exists - // And: The league has races but no FCY information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without FCY information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no caution periods information', async () => { - // TODO: Implement test - // Scenario: League with races but no caution periods information - // Given: A league exists - // And: The league has races but no caution periods information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without caution periods information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no restart procedures information', async () => { - // TODO: Implement test - // Scenario: League with races but no restart procedures information - // Given: A league exists - // And: The league has races but no restart procedures information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without restart procedures information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no penalty information', async () => { - // TODO: Implement test - // Scenario: League with races but no penalty information - // Given: A league exists - // And: The league has races but no penalty information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without penalty information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no protest information', async () => { - // TODO: Implement test - // Scenario: League with races but no protest information - // Given: A league exists - // And: The league has races but no protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without protest information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no appeal information', async () => { - // TODO: Implement test - // Scenario: League with races but no appeal information - // Given: A league exists - // And: The league has races but no appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without appeal information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no stewarding information', async () => { - // TODO: Implement test - // Scenario: League with races but no stewarding information - // Given: A league exists - // And: The league has races but no stewarding information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without stewarding information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no incident review information', async () => { - // TODO: Implement test - // Scenario: League with races but no incident review information - // Given: A league exists - // And: The league has races but no incident review information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without incident review information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no penalty appeal information', async () => { - // TODO: Implement test - // Scenario: League with races but no penalty appeal information - // Given: A league exists - // And: The league has races but no penalty appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without penalty appeal information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no protest appeal information', async () => { - // TODO: Implement test - // Scenario: League with races but no protest appeal information - // Given: A league exists - // And: The league has races but no protest appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without protest appeal information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no stewarding action appeal information', async () => { - // TODO: Implement test - // Scenario: League with races but no stewarding action appeal information - // Given: A league exists - // And: The league has races but no stewarding action appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without stewarding action appeal information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no penalty protest information', async () => { - // TODO: Implement test - // Scenario: League with races but no penalty protest information - // Given: A league exists - // And: The league has races but no penalty protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without penalty protest information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no stewarding action protest information', async () => { - // TODO: Implement test - // Scenario: League with races but no stewarding action protest information - // Given: A league exists - // And: The league has races but no stewarding action protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without stewarding action protest information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no penalty appeal protest information', async () => { - // TODO: Implement test - // Scenario: League with races but no penalty appeal protest information - // Given: A league exists - // And: The league has races but no penalty appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without penalty appeal protest information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no stewarding action appeal protest information', async () => { - // TODO: Implement test - // Scenario: League with races but no stewarding action appeal protest information - // Given: A league exists - // And: The league has races but no stewarding action appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without stewarding action appeal protest information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no penalty appeal protest stewarding action information', async () => { - // TODO: Implement test - // Scenario: League with races but no penalty appeal protest stewarding action information - // Given: A league exists - // And: The league has races but no penalty appeal protest stewarding action information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without penalty appeal protest stewarding action information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no stewarding action appeal protest penalty information', async () => { - // TODO: Implement test - // Scenario: League with races but no stewarding action appeal protest penalty information - // Given: A league exists - // And: The league has races but no stewarding action appeal protest penalty information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without stewarding action appeal protest penalty information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no penalty appeal protest stewarding action appeal information', async () => { - // TODO: Implement test - // Scenario: League with races but no penalty appeal protest stewarding action appeal information - // Given: A league exists - // And: The league has races but no penalty appeal protest stewarding action appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without penalty appeal protest stewarding action appeal information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no stewarding action appeal protest penalty appeal information', async () => { - // TODO: Implement test - // Scenario: League with races but no stewarding action appeal protest penalty appeal information - // Given: A league exists - // And: The league has races but no stewarding action appeal protest penalty appeal information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without stewarding action appeal protest penalty appeal information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no penalty appeal protest stewarding action appeal protest information', async () => { - // TODO: Implement test - // Scenario: League with races but no penalty appeal protest stewarding action appeal protest information - // Given: A league exists - // And: The league has races but no penalty appeal protest stewarding action appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without penalty appeal protest stewarding action appeal protest information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no stewarding action appeal protest penalty appeal protest information', async () => { - // TODO: Implement test - // Scenario: League with races but no stewarding action appeal protest penalty appeal protest information - // Given: A league exists - // And: The league has races but no stewarding action appeal protest penalty appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without stewarding action appeal protest penalty appeal protest information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no penalty appeal protest stewarding action appeal protest penalty information', async () => { - // TODO: Implement test - // Scenario: League with races but no penalty appeal protest stewarding action appeal protest penalty information - // Given: A league exists - // And: The league has races but no penalty appeal protest stewarding action appeal protest penalty information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without penalty appeal protest stewarding action appeal protest penalty information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - - it('should handle league with races but no stewarding action appeal protest penalty appeal protest penalty information', async () => { - // TODO: Implement test - // Scenario: League with races but no stewarding action appeal protest penalty appeal protest penalty information - // Given: A league exists - // And: The league has races but no stewarding action appeal protest penalty appeal protest penalty information - // When: GetLeagueScheduleUseCase.execute() is called with league ID - // Then: The result should contain races without stewarding action appeal protest penalty appeal protest penalty information - // And: EventPublisher should emit LeagueScheduleAccessedEvent - }); - }); - - describe('GetLeagueScheduleUseCase - Error Handling', () => { - it('should throw error when league does not exist', async () => { - // TODO: Implement test - // Scenario: Non-existent league - // Given: No league exists with the given ID - // When: GetLeagueScheduleUseCase.execute() is called with non-existent league ID - // Then: Should throw LeagueNotFoundError - // And: EventPublisher should NOT emit any events - }); - - it('should throw error when league ID is invalid', async () => { - // TODO: Implement test - // Scenario: Invalid league ID - // Given: An invalid league ID (e.g., empty string, null, undefined) - // When: GetLeagueScheduleUseCase.execute() is called with invalid league ID - // Then: Should throw ValidationError - // And: EventPublisher should NOT emit any events - }); - - it('should handle repository errors gracefully', async () => { - // TODO: Implement test - // Scenario: Repository throws error - // Given: A league exists - // And: LeagueRepository throws an error during query - // When: GetLeagueScheduleUseCase.execute() is called - // Then: Should propagate the error appropriately - // And: EventPublisher should NOT emit any events - }); - }); - - describe('League Schedule Data Orchestration', () => { - it('should correctly calculate race available slots', async () => { - // TODO: Implement test - // Scenario: Race available slots calculation - // Given: A league exists - // And: A race has max drivers set to 20 - // And: The race has 15 registered drivers - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show 5 available slots - }); - - it('should correctly format race date and time', async () => { - // TODO: Implement test - // Scenario: Race date and time formatting - // Given: A league exists - // And: A race has date and time - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted date and time - }); - - it('should correctly format race duration', async () => { - // TODO: Implement test - // Scenario: Race duration formatting - // Given: A league exists - // And: A race has duration - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted duration - }); - - it('should correctly format race registration deadline', async () => { - // TODO: Implement test - // Scenario: Race registration deadline formatting - // Given: A league exists - // And: A race has registration deadline - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted registration deadline - }); - - it('should correctly format race weather information', async () => { - // TODO: Implement test - // Scenario: Race weather information formatting - // Given: A league exists - // And: A race has weather information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted weather information - }); - - it('should correctly format race track layout', async () => { - // TODO: Implement test - // Scenario: Race track layout formatting - // Given: A league exists - // And: A race has track layout information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted track layout information - }); - - it('should correctly format race qualifying information', async () => { - // TODO: Implement test - // Scenario: Race qualifying information formatting - // Given: A league exists - // And: A race has qualifying information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted qualifying information - }); - - it('should correctly format race practice information', async () => { - // TODO: Implement test - // Scenario: Race practice information formatting - // Given: A league exists - // And: A race has practice information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted practice information - }); - - it('should correctly format race warmup information', async () => { - // TODO: Implement test - // Scenario: Race warmup information formatting - // Given: A league exists - // And: A race has warmup information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted warmup information - }); - - it('should correctly format race grid size', async () => { - // TODO: Implement test - // Scenario: Race grid size formatting - // Given: A league exists - // And: A race has grid size - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted grid size - }); - - it('should correctly format race pit lane information', async () => { - // TODO: Implement test - // Scenario: Race pit lane information formatting - // Given: A league exists - // And: A race has pit lane information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted pit lane information - }); - - it('should correctly format race safety car information', async () => { - // TODO: Implement test - // Scenario: Race safety car information formatting - // Given: A league exists - // And: A race has safety car information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted safety car information - }); - - it('should correctly format race virtual safety car information', async () => { - // TODO: Implement test - // Scenario: Race virtual safety car information formatting - // Given: A league exists - // And: A race has virtual safety car information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted virtual safety car information - }); - - it('should correctly format race FCY information', async () => { - // TODO: Implement test - // Scenario: Race FCY information formatting - // Given: A league exists - // And: A race has FCY information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted FCY information - }); - - it('should correctly format race caution periods information', async () => { - // TODO: Implement test - // Scenario: Race caution periods information formatting - // Given: A league exists - // And: A race has caution periods information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted caution periods information - }); - - it('should correctly format race restart procedures information', async () => { - // TODO: Implement test - // Scenario: Race restart procedures information formatting - // Given: A league exists - // And: A race has restart procedures information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted restart procedures information - }); - - it('should correctly format race penalty information', async () => { - // TODO: Implement test - // Scenario: Race penalty information formatting - // Given: A league exists - // And: A race has penalty information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted penalty information - }); - - it('should correctly format race protest information', async () => { - // TODO: Implement test - // Scenario: Race protest information formatting - // Given: A league exists - // And: A race has protest information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted protest information - }); - - it('should correctly format race appeal information', async () => { - // TODO: Implement test - // Scenario: Race appeal information formatting - // Given: A league exists - // And: A race has appeal information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted appeal information - }); - - it('should correctly format race stewarding information', async () => { - // TODO: Implement test - // Scenario: Race stewarding information formatting - // Given: A league exists - // And: A race has stewarding information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted stewarding information - }); - - it('should correctly format race incident review information', async () => { - // TODO: Implement test - // Scenario: Race incident review information formatting - // Given: A league exists - // And: A race has incident review information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted incident review information - }); - - it('should correctly format race penalty appeal information', async () => { - // TODO: Implement test - // Scenario: Race penalty appeal information formatting - // Given: A league exists - // And: A race has penalty appeal information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted penalty appeal information - }); - - it('should correctly format race protest appeal information', async () => { - // TODO: Implement test - // Scenario: Race protest appeal information formatting - // Given: A league exists - // And: A race has protest appeal information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted protest appeal information - }); - - it('should correctly format race stewarding action appeal information', async () => { - // TODO: Implement test - // Scenario: Race stewarding action appeal information formatting - // Given: A league exists - // And: A race has stewarding action appeal information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted stewarding action appeal information - }); - - it('should correctly format race penalty protest information', async () => { - // TODO: Implement test - // Scenario: Race penalty protest information formatting - // Given: A league exists - // And: A race has penalty protest information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted penalty protest information - }); - - it('should correctly format race stewarding action protest information', async () => { - // TODO: Implement test - // Scenario: Race stewarding action protest information formatting - // Given: A league exists - // And: A race has stewarding action protest information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted stewarding action protest information - }); - - it('should correctly format race penalty appeal protest information', async () => { - // TODO: Implement test - // Scenario: Race penalty appeal protest information formatting - // Given: A league exists - // And: A race has penalty appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted penalty appeal protest information - }); - - it('should correctly format race stewarding action appeal protest information', async () => { - // TODO: Implement test - // Scenario: Race stewarding action appeal protest information formatting - // Given: A league exists - // And: A race has stewarding action appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted stewarding action appeal protest information - }); - - it('should correctly format race penalty appeal protest stewarding action information', async () => { - // TODO: Implement test - // Scenario: Race penalty appeal protest stewarding action information formatting - // Given: A league exists - // And: A race has penalty appeal protest stewarding action information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted penalty appeal protest stewarding action information - }); - - it('should correctly format race stewarding action appeal protest penalty information', async () => { - // TODO: Implement test - // Scenario: Race stewarding action appeal protest penalty information formatting - // Given: A league exists - // And: A race has stewarding action appeal protest penalty information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted stewarding action appeal protest penalty information - }); - - it('should correctly format race penalty appeal protest stewarding action appeal information', async () => { - // TODO: Implement test - // Scenario: Race penalty appeal protest stewarding action appeal information formatting - // Given: A league exists - // And: A race has penalty appeal protest stewarding action appeal information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted penalty appeal protest stewarding action appeal information - }); - - it('should correctly format race stewarding action appeal protest penalty appeal information', async () => { - // TODO: Implement test - // Scenario: Race stewarding action appeal protest penalty appeal information formatting - // Given: A league exists - // And: A race has stewarding action appeal protest penalty appeal information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted stewarding action appeal protest penalty appeal information - }); - - it('should correctly format race penalty appeal protest stewarding action appeal protest information', async () => { - // TODO: Implement test - // Scenario: Race penalty appeal protest stewarding action appeal protest information formatting - // Given: A league exists - // And: A race has penalty appeal protest stewarding action appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted penalty appeal protest stewarding action appeal protest information - }); - - it('should correctly format race stewarding action appeal protest penalty appeal protest information', async () => { - // TODO: Implement test - // Scenario: Race stewarding action appeal protest penalty appeal protest information formatting - // Given: A league exists - // And: A race has stewarding action appeal protest penalty appeal protest information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted stewarding action appeal protest penalty appeal protest information - }); - - it('should correctly format race penalty appeal protest stewarding action appeal protest penalty information', async () => { - // TODO: Implement test - // Scenario: Race penalty appeal protest stewarding action appeal protest penalty information formatting - // Given: A league exists - // And: A race has penalty appeal protest stewarding action appeal protest penalty information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted penalty appeal protest stewarding action appeal protest penalty information - }); - - it('should correctly format race stewarding action appeal protest penalty appeal protest penalty information', async () => { - // TODO: Implement test - // Scenario: Race stewarding action appeal protest penalty appeal protest penalty information formatting - // Given: A league exists - // And: A race has stewarding action appeal protest penalty appeal protest penalty information - // When: GetLeagueScheduleUseCase.execute() is called - // Then: The race should show formatted stewarding action appeal protest penalty appeal protest penalty information - }); - }); -}); diff --git a/tests/integration/leagues/league-sponsorships-use-cases.integration.test.ts b/tests/integration/leagues/league-sponsorships-use-cases.integration.test.ts deleted file mode 100644 index 7fb5c7995..000000000 --- a/tests/integration/leagues/league-sponsorships-use-cases.integration.test.ts +++ /dev/null @@ -1,711 +0,0 @@ -/** - * Integration Test: League Sponsorships Use Case Orchestration - * - * Tests the orchestration logic of league sponsorships-related Use Cases: - * - GetLeagueSponsorshipsUseCase: Retrieves league sponsorships overview - * - GetLeagueSponsorshipDetailsUseCase: Retrieves details of a specific sponsorship - * - GetLeagueSponsorshipApplicationsUseCase: Retrieves sponsorship applications - * - GetLeagueSponsorshipOffersUseCase: Retrieves sponsorship offers - * - GetLeagueSponsorshipContractsUseCase: Retrieves sponsorship contracts - * - GetLeagueSponsorshipPaymentsUseCase: Retrieves sponsorship payments - * - GetLeagueSponsorshipReportsUseCase: Retrieves sponsorship reports - * - GetLeagueSponsorshipStatisticsUseCase: Retrieves sponsorship statistics - * - 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, afterAll, beforeEach } from 'vitest'; -import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository'; -import { InMemorySponsorshipRepository } from '../../../adapters/sponsorships/persistence/inmemory/InMemorySponsorshipRepository'; -import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher'; -import { GetLeagueSponsorshipsUseCase } from '../../../core/leagues/use-cases/GetLeagueSponsorshipsUseCase'; -import { GetLeagueSponsorshipDetailsUseCase } from '../../../core/leagues/use-cases/GetLeagueSponsorshipDetailsUseCase'; -import { GetLeagueSponsorshipApplicationsUseCase } from '../../../core/leagues/use-cases/GetLeagueSponsorshipApplicationsUseCase'; -import { GetLeagueSponsorshipOffersUseCase } from '../../../core/leagues/use-cases/GetLeagueSponsorshipOffersUseCase'; -import { GetLeagueSponsorshipContractsUseCase } from '../../../core/leagues/use-cases/GetLeagueSponsorshipContractsUseCase'; -import { GetLeagueSponsorshipPaymentsUseCase } from '../../../core/leagues/use-cases/GetLeagueSponsorshipPaymentsUseCase'; -import { GetLeagueSponsorshipReportsUseCase } from '../../../core/leagues/use-cases/GetLeagueSponsorshipReportsUseCase'; -import { GetLeagueSponsorshipStatisticsUseCase } from '../../../core/leagues/use-cases/GetLeagueSponsorshipStatisticsUseCase'; -import { LeagueSponsorshipsQuery } from '../../../core/leagues/ports/LeagueSponsorshipsQuery'; -import { LeagueSponsorshipDetailsQuery } from '../../../core/leagues/ports/LeagueSponsorshipDetailsQuery'; -import { LeagueSponsorshipApplicationsQuery } from '../../../core/leagues/ports/LeagueSponsorshipApplicationsQuery'; -import { LeagueSponsorshipOffersQuery } from '../../../core/leagues/ports/LeagueSponsorshipOffersQuery'; -import { LeagueSponsorshipContractsQuery } from '../../../core/leagues/ports/LeagueSponsorshipContractsQuery'; -import { LeagueSponsorshipPaymentsQuery } from '../../../core/leagues/ports/LeagueSponsorshipPaymentsQuery'; -import { LeagueSponsorshipReportsQuery } from '../../../core/leagues/ports/LeagueSponsorshipReportsQuery'; -import { LeagueSponsorshipStatisticsQuery } from '../../../core/leagues/ports/LeagueSponsorshipStatisticsQuery'; - -describe('League Sponsorships Use Case Orchestration', () => { - let leagueRepository: InMemoryLeagueRepository; - let sponsorshipRepository: InMemorySponsorshipRepository; - let eventPublisher: InMemoryEventPublisher; - let getLeagueSponsorshipsUseCase: GetLeagueSponsorshipsUseCase; - let getLeagueSponsorshipDetailsUseCase: GetLeagueSponsorshipDetailsUseCase; - let getLeagueSponsorshipApplicationsUseCase: GetLeagueSponsorshipApplicationsUseCase; - let getLeagueSponsorshipOffersUseCase: GetLeagueSponsorshipOffersUseCase; - let getLeagueSponsorshipContractsUseCase: GetLeagueSponsorshipContractsUseCase; - let getLeagueSponsorshipPaymentsUseCase: GetLeagueSponsorshipPaymentsUseCase; - let getLeagueSponsorshipReportsUseCase: GetLeagueSponsorshipReportsUseCase; - let getLeagueSponsorshipStatisticsUseCase: GetLeagueSponsorshipStatisticsUseCase; - - beforeAll(() => { - // TODO: Initialize In-Memory repositories and event publisher - // leagueRepository = new InMemoryLeagueRepository(); - // sponsorshipRepository = new InMemorySponsorshipRepository(); - // eventPublisher = new InMemoryEventPublisher(); - // getLeagueSponsorshipsUseCase = new GetLeagueSponsorshipsUseCase({ - // leagueRepository, - // sponsorshipRepository, - // eventPublisher, - // }); - // getLeagueSponsorshipDetailsUseCase = new GetLeagueSponsorshipDetailsUseCase({ - // leagueRepository, - // sponsorshipRepository, - // eventPublisher, - // }); - // getLeagueSponsorshipApplicationsUseCase = new GetLeagueSponsorshipApplicationsUseCase({ - // leagueRepository, - // sponsorshipRepository, - // eventPublisher, - // }); - // getLeagueSponsorshipOffersUseCase = new GetLeagueSponsorshipOffersUseCase({ - // leagueRepository, - // sponsorshipRepository, - // eventPublisher, - // }); - // getLeagueSponsorshipContractsUseCase = new GetLeagueSponsorshipContractsUseCase({ - // leagueRepository, - // sponsorshipRepository, - // eventPublisher, - // }); - // getLeagueSponsorshipPaymentsUseCase = new GetLeagueSponsorshipPaymentsUseCase({ - // leagueRepository, - // sponsorshipRepository, - // eventPublisher, - // }); - // getLeagueSponsorshipReportsUseCase = new GetLeagueSponsorshipReportsUseCase({ - // leagueRepository, - // sponsorshipRepository, - // eventPublisher, - // }); - // getLeagueSponsorshipStatisticsUseCase = new GetLeagueSponsorshipStatisticsUseCase({ - // leagueRepository, - // sponsorshipRepository, - // eventPublisher, - // }); - }); - - beforeEach(() => { - // TODO: Clear all In-Memory repositories before each test - // leagueRepository.clear(); - // sponsorshipRepository.clear(); - // eventPublisher.clear(); - }); - - describe('GetLeagueSponsorshipsUseCase - Success Path', () => { - it('should retrieve league sponsorships overview', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorships overview - // Given: A league exists with sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorships overview - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve active sponsorships', async () => { - // TODO: Implement test - // Scenario: Admin views active sponsorships - // Given: A league exists with active sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show active sponsorships - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve pending sponsorships', async () => { - // TODO: Implement test - // Scenario: Admin views pending sponsorships - // Given: A league exists with pending sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show pending sponsorships - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve expired sponsorships', async () => { - // TODO: Implement test - // Scenario: Admin views expired sponsorships - // Given: A league exists with expired sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show expired sponsorships - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve sponsorship statistics', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship statistics - // Given: A league exists with sponsorship statistics - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorship statistics - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve sponsorship revenue', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship revenue - // Given: A league exists with sponsorship revenue - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorship revenue - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve sponsorship exposure', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship exposure - // Given: A league exists with sponsorship exposure - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorship exposure - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve sponsorship reports', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship reports - // Given: A league exists with sponsorship reports - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorship reports - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve sponsorship activity log', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship activity log - // Given: A league exists with sponsorship activity - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorship activity log - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve sponsorship alerts', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship alerts - // Given: A league exists with sponsorship alerts - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorship alerts - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve sponsorship settings', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship settings - // Given: A league exists with sponsorship settings - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorship settings - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve sponsorship templates', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship templates - // Given: A league exists with sponsorship templates - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorship templates - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should retrieve sponsorship guidelines', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship guidelines - // Given: A league exists with sponsorship guidelines - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show sponsorship guidelines - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - }); - - describe('GetLeagueSponsorshipsUseCase - Edge Cases', () => { - it('should handle league with no sponsorships', async () => { - // TODO: Implement test - // Scenario: League with no sponsorships - // Given: A league exists - // And: The league has no sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show empty sponsorships list - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should handle league with no active sponsorships', async () => { - // TODO: Implement test - // Scenario: League with no active sponsorships - // Given: A league exists - // And: The league has no active sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show empty active sponsorships list - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should handle league with no pending sponsorships', async () => { - // TODO: Implement test - // Scenario: League with no pending sponsorships - // Given: A league exists - // And: The league has no pending sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show empty pending sponsorships list - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should handle league with no expired sponsorships', async () => { - // TODO: Implement test - // Scenario: League with no expired sponsorships - // Given: A league exists - // And: The league has no expired sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show empty expired sponsorships list - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should handle league with no sponsorship reports', async () => { - // TODO: Implement test - // Scenario: League with no sponsorship reports - // Given: A league exists - // And: The league has no sponsorship reports - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show empty sponsorship reports list - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should handle league with no sponsorship alerts', async () => { - // TODO: Implement test - // Scenario: League with no sponsorship alerts - // Given: A league exists - // And: The league has no sponsorship alerts - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show no alerts - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should handle league with no sponsorship templates', async () => { - // TODO: Implement test - // Scenario: League with no sponsorship templates - // Given: A league exists - // And: The league has no sponsorship templates - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show no templates - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - - it('should handle league with no sponsorship guidelines', async () => { - // TODO: Implement test - // Scenario: League with no sponsorship guidelines - // Given: A league exists - // And: The league has no sponsorship guidelines - // When: GetLeagueSponsorshipsUseCase.execute() is called with league ID - // Then: The result should show no guidelines - // And: EventPublisher should emit LeagueSponsorshipsAccessedEvent - }); - }); - - describe('GetLeagueSponsorshipsUseCase - Error Handling', () => { - it('should throw error when league does not exist', async () => { - // TODO: Implement test - // Scenario: Non-existent league - // Given: No league exists with the given ID - // When: GetLeagueSponsorshipsUseCase.execute() is called with non-existent league ID - // Then: Should throw LeagueNotFoundError - // And: EventPublisher should NOT emit any events - }); - - it('should throw error when league ID is invalid', async () => { - // TODO: Implement test - // Scenario: Invalid league ID - // Given: An invalid league ID (e.g., empty string, null, undefined) - // When: GetLeagueSponsorshipsUseCase.execute() is called with invalid league ID - // Then: Should throw ValidationError - // And: EventPublisher should NOT emit any events - }); - - it('should handle repository errors gracefully', async () => { - // TODO: Implement test - // Scenario: Repository throws error - // Given: A league exists - // And: SponsorshipRepository throws an error during query - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Should propagate the error appropriately - // And: EventPublisher should NOT emit any events - }); - }); - - describe('League Sponsorships Data Orchestration', () => { - it('should correctly format sponsorships overview', async () => { - // TODO: Implement test - // Scenario: Sponsorships overview formatting - // Given: A league exists with sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorships overview should show: - // - Total sponsorships - // - Active sponsorships - // - Pending sponsorships - // - Expired sponsorships - // - Total revenue - }); - - it('should correctly format sponsorship details', async () => { - // TODO: Implement test - // Scenario: Sponsorship details formatting - // Given: A league exists with sponsorships - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship details should show: - // - Sponsor name - // - Sponsorship type - // - Amount - // - Duration - // - Status - // - Start date - // - End date - }); - - it('should correctly format sponsorship statistics', async () => { - // TODO: Implement test - // Scenario: Sponsorship statistics formatting - // Given: A league exists with sponsorship statistics - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship statistics should show: - // - Total revenue - // - Average sponsorship value - // - Sponsorship growth rate - // - Sponsor retention rate - }); - - it('should correctly format sponsorship revenue', async () => { - // TODO: Implement test - // Scenario: Sponsorship revenue formatting - // Given: A league exists with sponsorship revenue - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship revenue should show: - // - Total revenue - // - Revenue by sponsor - // - Revenue by type - // - Revenue by period - }); - - it('should correctly format sponsorship exposure', async () => { - // TODO: Implement test - // Scenario: Sponsorship exposure formatting - // Given: A league exists with sponsorship exposure - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship exposure should show: - // - Impressions - // - Clicks - // - Engagement rate - // - Brand visibility - }); - - it('should correctly format sponsorship reports', async () => { - // TODO: Implement test - // Scenario: Sponsorship reports formatting - // Given: A league exists with sponsorship reports - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship reports should show: - // - Report type - // - Report period - // - Key metrics - // - Recommendations - }); - - it('should correctly format sponsorship activity log', async () => { - // TODO: Implement test - // Scenario: Sponsorship activity log formatting - // Given: A league exists with sponsorship activity - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship activity log should show: - // - Timestamp - // - Action type - // - User - // - Details - }); - - it('should correctly format sponsorship alerts', async () => { - // TODO: Implement test - // Scenario: Sponsorship alerts formatting - // Given: A league exists with sponsorship alerts - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship alerts should show: - // - Alert type - // - Timestamp - // - Details - }); - - it('should correctly format sponsorship settings', async () => { - // TODO: Implement test - // Scenario: Sponsorship settings formatting - // Given: A league exists with sponsorship settings - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship settings should show: - // - Minimum sponsorship amount - // - Maximum sponsorship amount - // - Approval process - // - Payment terms - }); - - it('should correctly format sponsorship templates', async () => { - // TODO: Implement test - // Scenario: Sponsorship templates formatting - // Given: A league exists with sponsorship templates - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship templates should show: - // - Template name - // - Template content - // - Usage instructions - }); - - it('should correctly format sponsorship guidelines', async () => { - // TODO: Implement test - // Scenario: Sponsorship guidelines formatting - // Given: A league exists with sponsorship guidelines - // When: GetLeagueSponsorshipsUseCase.execute() is called - // Then: Sponsorship guidelines should show: - // - Guidelines content - // - Rules - // - Restrictions - }); - }); - - describe('GetLeagueSponsorshipDetailsUseCase - Success Path', () => { - it('should retrieve sponsorship details', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship details - // Given: A league exists with a sponsorship - // When: GetLeagueSponsorshipDetailsUseCase.execute() is called with league ID and sponsorship ID - // Then: The result should show sponsorship details - // And: EventPublisher should emit LeagueSponsorshipDetailsAccessedEvent - }); - - it('should retrieve sponsorship with all metadata', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship with metadata - // Given: A league exists with a sponsorship - // When: GetLeagueSponsorshipDetailsUseCase.execute() is called with league ID and sponsorship ID - // Then: The result should show sponsorship with all metadata - // And: EventPublisher should emit LeagueSponsorshipDetailsAccessedEvent - }); - }); - - describe('GetLeagueSponsorshipApplicationsUseCase - Success Path', () => { - it('should retrieve sponsorship applications with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship applications with pagination - // Given: A league exists with many sponsorship applications - // When: GetLeagueSponsorshipApplicationsUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated sponsorship applications - // And: EventPublisher should emit LeagueSponsorshipApplicationsAccessedEvent - }); - - it('should retrieve sponsorship applications filtered by status', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship applications filtered by status - // Given: A league exists with sponsorship applications of different statuses - // When: GetLeagueSponsorshipApplicationsUseCase.execute() is called with league ID and status filter - // Then: The result should show filtered sponsorship applications - // And: EventPublisher should emit LeagueSponsorshipApplicationsAccessedEvent - }); - - it('should retrieve sponsorship applications filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship applications filtered by date range - // Given: A league exists with sponsorship applications over time - // When: GetLeagueSponsorshipApplicationsUseCase.execute() is called with league ID and date range - // Then: The result should show filtered sponsorship applications - // And: EventPublisher should emit LeagueSponsorshipApplicationsAccessedEvent - }); - - it('should retrieve sponsorship applications sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship applications sorted by date - // Given: A league exists with sponsorship applications - // When: GetLeagueSponsorshipApplicationsUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted sponsorship applications - // And: EventPublisher should emit LeagueSponsorshipApplicationsAccessedEvent - }); - }); - - describe('GetLeagueSponsorshipOffersUseCase - Success Path', () => { - it('should retrieve sponsorship offers with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship offers with pagination - // Given: A league exists with many sponsorship offers - // When: GetLeagueSponsorshipOffersUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated sponsorship offers - // And: EventPublisher should emit LeagueSponsorshipOffersAccessedEvent - }); - - it('should retrieve sponsorship offers filtered by status', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship offers filtered by status - // Given: A league exists with sponsorship offers of different statuses - // When: GetLeagueSponsorshipOffersUseCase.execute() is called with league ID and status filter - // Then: The result should show filtered sponsorship offers - // And: EventPublisher should emit LeagueSponsorshipOffersAccessedEvent - }); - - it('should retrieve sponsorship offers filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship offers filtered by date range - // Given: A league exists with sponsorship offers over time - // When: GetLeagueSponsorshipOffersUseCase.execute() is called with league ID and date range - // Then: The result should show filtered sponsorship offers - // And: EventPublisher should emit LeagueSponsorshipOffersAccessedEvent - }); - - it('should retrieve sponsorship offers sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship offers sorted by date - // Given: A league exists with sponsorship offers - // When: GetLeagueSponsorshipOffersUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted sponsorship offers - // And: EventPublisher should emit LeagueSponsorshipOffersAccessedEvent - }); - }); - - describe('GetLeagueSponsorshipContractsUseCase - Success Path', () => { - it('should retrieve sponsorship contracts with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship contracts with pagination - // Given: A league exists with many sponsorship contracts - // When: GetLeagueSponsorshipContractsUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated sponsorship contracts - // And: EventPublisher should emit LeagueSponsorshipContractsAccessedEvent - }); - - it('should retrieve sponsorship contracts filtered by status', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship contracts filtered by status - // Given: A league exists with sponsorship contracts of different statuses - // When: GetLeagueSponsorshipContractsUseCase.execute() is called with league ID and status filter - // Then: The result should show filtered sponsorship contracts - // And: EventPublisher should emit LeagueSponsorshipContractsAccessedEvent - }); - - it('should retrieve sponsorship contracts filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship contracts filtered by date range - // Given: A league exists with sponsorship contracts over time - // When: GetLeagueSponsorshipContractsUseCase.execute() is called with league ID and date range - // Then: The result should show filtered sponsorship contracts - // And: EventPublisher should emit LeagueSponsorshipContractsAccessedEvent - }); - - it('should retrieve sponsorship contracts sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship contracts sorted by date - // Given: A league exists with sponsorship contracts - // When: GetLeagueSponsorshipContractsUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted sponsorship contracts - // And: EventPublisher should emit LeagueSponsorshipContractsAccessedEvent - }); - }); - - describe('GetLeagueSponsorshipPaymentsUseCase - Success Path', () => { - it('should retrieve sponsorship payments with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship payments with pagination - // Given: A league exists with many sponsorship payments - // When: GetLeagueSponsorshipPaymentsUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated sponsorship payments - // And: EventPublisher should emit LeagueSponsorshipPaymentsAccessedEvent - }); - - it('should retrieve sponsorship payments filtered by status', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship payments filtered by status - // Given: A league exists with sponsorship payments of different statuses - // When: GetLeagueSponsorshipPaymentsUseCase.execute() is called with league ID and status filter - // Then: The result should show filtered sponsorship payments - // And: EventPublisher should emit LeagueSponsorshipPaymentsAccessedEvent - }); - - it('should retrieve sponsorship payments filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship payments filtered by date range - // Given: A league exists with sponsorship payments over time - // When: GetLeagueSponsorshipPaymentsUseCase.execute() is called with league ID and date range - // Then: The result should show filtered sponsorship payments - // And: EventPublisher should emit LeagueSponsorshipPaymentsAccessedEvent - }); - - it('should retrieve sponsorship payments sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship payments sorted by date - // Given: A league exists with sponsorship payments - // When: GetLeagueSponsorshipPaymentsUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted sponsorship payments - // And: EventPublisher should emit LeagueSponsorshipPaymentsAccessedEvent - }); - }); - - describe('GetLeagueSponsorshipReportsUseCase - Success Path', () => { - it('should retrieve sponsorship reports with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship reports with pagination - // Given: A league exists with many sponsorship reports - // When: GetLeagueSponsorshipReportsUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated sponsorship reports - // And: EventPublisher should emit LeagueSponsorshipReportsAccessedEvent - }); - - it('should retrieve sponsorship reports filtered by type', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship reports filtered by type - // Given: A league exists with sponsorship reports of different types - // When: GetLeagueSponsorshipReportsUseCase.execute() is called with league ID and type filter - // Then: The result should show filtered sponsorship reports - // And: EventPublisher should emit LeagueSponsorshipReportsAccessedEvent - }); - - it('should retrieve sponsorship reports filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship reports filtered by date range - // Given: A league exists with sponsorship reports over time - // When: GetLeagueSponsorshipReportsUseCase.execute() is called with league ID and date range - // Then: The result should show filtered sponsorship reports - // And: EventPublisher should emit LeagueSponsorshipReportsAccessedEvent - }); - - it('should retrieve sponsorship reports sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship reports sorted by date - // Given: A league exists with sponsorship reports - // When: GetLeagueSponsorshipReportsUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted sponsorship reports - // And: EventPublisher should emit LeagueSponsorshipReportsAccessedEvent - }); - }); - - describe('GetLeagueSponsorshipStatisticsUseCase - Success Path', () => { - it('should retrieve sponsorship statistics', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship statistics - // Given: A league exists with sponsorship statistics - // When: GetLeagueSponsorshipStatisticsUseCase.execute() is called with league ID - // Then: The result should show sponsorship statistics - // And: EventPublisher should emit LeagueSponsorshipStatisticsAccessedEvent - }); - - it('should retrieve sponsorship statistics with date range', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship statistics with date range - // Given: A league exists with sponsorship statistics - // When: GetLeagueSponsorshipStatisticsUseCase.execute() is called with league ID and date range - // Then: The result should show sponsorship statistics for the date range - // And: EventPublisher should emit LeagueSponsorshipStatisticsAccessedEvent - }); - - it('should retrieve sponsorship statistics with granularity', async () => { - // TODO: Implement test - // Scenario: Admin views sponsorship statistics with granularity - // Given: A league exists with sponsorship statistics - // When: GetLeagueSponsorshipStatisticsUseCase.execute() is called with league ID and granularity - // Then: The result should show sponsorship statistics with the specified granularity - // And: EventPublisher should emit LeagueSponsorshipStatisticsAccessedEvent - }); - }); -}); diff --git a/tests/integration/leagues/league-standings-use-cases.integration.test.ts b/tests/integration/leagues/league-standings-use-cases.integration.test.ts deleted file mode 100644 index 5c156aa85..000000000 --- a/tests/integration/leagues/league-standings-use-cases.integration.test.ts +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Integration Test: League Standings Use Case Orchestration - * - * Tests the orchestration logic of league standings-related Use Cases: - * - GetLeagueStandingsUseCase: Retrieves championship standings with driver statistics - * - 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, afterAll, beforeEach } from 'vitest'; -import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository'; -import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository'; -import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository'; -import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher'; -import { GetLeagueStandingsUseCase } from '../../../core/leagues/use-cases/GetLeagueStandingsUseCase'; -import { LeagueStandingsQuery } from '../../../core/leagues/ports/LeagueStandingsQuery'; - -describe('League Standings Use Case Orchestration', () => { - let leagueRepository: InMemoryLeagueRepository; - let driverRepository: InMemoryDriverRepository; - let raceRepository: InMemoryRaceRepository; - let eventPublisher: InMemoryEventPublisher; - let getLeagueStandingsUseCase: GetLeagueStandingsUseCase; - - beforeAll(() => { - // TODO: Initialize In-Memory repositories and event publisher - // leagueRepository = new InMemoryLeagueRepository(); - // driverRepository = new InMemoryDriverRepository(); - // raceRepository = new InMemoryRaceRepository(); - // eventPublisher = new InMemoryEventPublisher(); - // getLeagueStandingsUseCase = new GetLeagueStandingsUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - }); - - beforeEach(() => { - // TODO: Clear all In-Memory repositories before each test - // leagueRepository.clear(); - // driverRepository.clear(); - // raceRepository.clear(); - // eventPublisher.clear(); - }); - - describe('GetLeagueStandingsUseCase - Success Path', () => { - it('should retrieve championship standings with all driver statistics', async () => { - // TODO: Implement test - // Scenario: League with complete standings - // Given: A league exists with multiple drivers - // And: Each driver has points, wins, podiums, starts, DNFs - // And: Each driver has win rate, podium rate, DNF rate - // And: Each driver has average finish position - // And: Each driver has best and worst finish position - // And: Each driver has average points per race - // And: Each driver has total points - // And: Each driver has points behind leader - // And: Each driver has points ahead of next driver - // And: Each driver has gap to leader - // And: Each driver has gap to next driver - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain all drivers ranked by points - // And: Each driver should display their position - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - - it('should retrieve standings with minimal driver statistics', async () => { - // TODO: Implement test - // Scenario: League with minimal standings - // Given: A league exists with drivers who have minimal statistics - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers with basic statistics - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - - it('should retrieve standings with drivers who have no recent results', async () => { - // TODO: Implement test - // Scenario: League with drivers who have no recent results - // Given: A league exists with drivers who have no recent results - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers with no recent results - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - - it('should retrieve standings with drivers who have no career history', async () => { - // TODO: Implement test - // Scenario: League with drivers who have no career history - // Given: A league exists with drivers who have no career history - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers with no career history - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - - it('should retrieve standings with drivers who have championship standings but no other data', async () => { - // TODO: Implement test - // Scenario: League with drivers who have championship standings but no other data - // Given: A league exists with drivers who have championship standings - // And: The drivers have no career history - // And: The drivers have no recent race results - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers with championship standings - // And: Career history section should be empty - // And: Recent race results section should be empty - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - - it('should retrieve standings with drivers who have social links but no team affiliation', async () => { - // TODO: Implement test - // Scenario: League with drivers who have social links but no team affiliation - // Given: A league exists with drivers who have social links - // And: The drivers have no team affiliation - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers with social links - // And: Team affiliation section should be empty - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - - it('should retrieve standings with drivers who have team affiliation but no social links', async () => { - // TODO: Implement test - // Scenario: League with drivers who have team affiliation but no social links - // Given: A league exists with drivers who have team affiliation - // And: The drivers have no social links - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers with team affiliation - // And: Social links section should be empty - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - }); - - describe('GetLeagueStandingsUseCase - Edge Cases', () => { - it('should handle drivers with no career history', async () => { - // TODO: Implement test - // Scenario: Drivers with no career history - // Given: A league exists - // And: The drivers have no career history - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers - // And: Career history section should be empty - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - - it('should handle drivers with no recent race results', async () => { - // TODO: Implement test - // Scenario: Drivers with no recent race results - // Given: A league exists - // And: The drivers have no recent race results - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers - // And: Recent race results section should be empty - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - - it('should handle drivers with no championship standings', async () => { - // TODO: Implement test - // Scenario: Drivers with no championship standings - // Given: A league exists - // And: The drivers have no championship standings - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers - // And: Championship standings section should be empty - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - - it('should handle drivers with no data at all', async () => { - // TODO: Implement test - // Scenario: Drivers with absolutely no data - // Given: A league exists - // And: The drivers have no statistics - // And: The drivers have no career history - // And: The drivers have no recent race results - // And: The drivers have no championship standings - // And: The drivers have no social links - // And: The drivers have no team affiliation - // When: GetLeagueStandingsUseCase.execute() is called with league ID - // Then: The result should contain drivers - // And: All sections should be empty or show default values - // And: EventPublisher should emit LeagueStandingsAccessedEvent - }); - }); - - describe('GetLeagueStandingsUseCase - Error Handling', () => { - it('should throw error when league does not exist', async () => { - // TODO: Implement test - // Scenario: Non-existent league - // Given: No league exists with the given ID - // When: GetLeagueStandingsUseCase.execute() is called with non-existent league ID - // Then: Should throw LeagueNotFoundError - // And: EventPublisher should NOT emit any events - }); - - it('should throw error when league ID is invalid', async () => { - // TODO: Implement test - // Scenario: Invalid league ID - // Given: An invalid league ID (e.g., empty string, null, undefined) - // When: GetLeagueStandingsUseCase.execute() is called with invalid league ID - // Then: Should throw ValidationError - // And: EventPublisher should NOT emit any events - }); - - it('should handle repository errors gracefully', async () => { - // TODO: Implement test - // Scenario: Repository throws error - // Given: A league exists - // And: LeagueRepository throws an error during query - // When: GetLeagueStandingsUseCase.execute() is called - // Then: Should propagate the error appropriately - // And: EventPublisher should NOT emit any events - }); - }); - - describe('League Standings Data Orchestration', () => { - it('should correctly calculate driver statistics from race results', async () => { - // TODO: Implement test - // Scenario: Driver statistics calculation - // Given: A league exists - // And: A driver has 10 completed races - // And: The driver has 3 wins - // And: The driver has 5 podiums - // When: GetLeagueStandingsUseCase.execute() is called - // Then: Driver statistics should show: - // - Starts: 10 - // - Wins: 3 - // - Podiums: 5 - // - Rating: Calculated based on performance - // - Rank: Calculated based on rating - }); - - it('should correctly format career history with league and team information', async () => { - // TODO: Implement test - // Scenario: Career history formatting - // Given: A league exists - // And: A driver has participated in 2 leagues - // And: The driver has been on 3 teams across seasons - // When: GetLeagueStandingsUseCase.execute() is called - // Then: Career history should show: - // - League A: Season 2024, Team X - // - League B: Season 2024, Team Y - // - League A: Season 2023, Team Z - }); - - it('should correctly format recent race results with proper details', async () => { - // TODO: Implement test - // Scenario: Recent race results formatting - // Given: A league exists - // And: A driver has 5 recent race results - // When: GetLeagueStandingsUseCase.execute() is called - // Then: Recent race results should show: - // - Race name - // - Track name - // - Finishing position - // - Points earned - // - Race date (sorted newest first) - }); - - it('should correctly aggregate championship standings across leagues', async () => { - // TODO: Implement test - // Scenario: Championship standings aggregation - // Given: A league exists - // And: A driver is in 2 championships - // And: In Championship A: Position 5, 150 points, 20 drivers - // And: In Championship B: Position 12, 85 points, 15 drivers - // When: GetLeagueStandingsUseCase.execute() is called - // Then: Championship standings should show: - // - League A: Position 5, 150 points, 20 drivers - // - League B: Position 12, 85 points, 15 drivers - }); - - it('should correctly format social links with proper URLs', async () => { - // TODO: Implement test - // Scenario: Social links formatting - // Given: A league exists - // And: A driver has social links (Discord, Twitter, iRacing) - // When: GetLeagueStandingsUseCase.execute() is called - // Then: Social links should show: - // - Discord: https://discord.gg/username - // - Twitter: https://twitter.com/username - // - iRacing: https://members.iracing.com/membersite/member/profile?username=username - }); - - it('should correctly format team affiliation with role', async () => { - // TODO: Implement test - // Scenario: Team affiliation formatting - // Given: A league exists - // And: A driver is affiliated with Team XYZ - // And: The driver's role is "Driver" - // When: GetLeagueStandingsUseCase.execute() is called - // Then: Team affiliation should show: - // - Team name: Team XYZ - // - Team logo: (if available) - // - Driver role: Driver - }); - }); -}); diff --git a/tests/integration/leagues/league-stewarding-use-cases.integration.test.ts b/tests/integration/leagues/league-stewarding-use-cases.integration.test.ts deleted file mode 100644 index 3ac2512f6..000000000 --- a/tests/integration/leagues/league-stewarding-use-cases.integration.test.ts +++ /dev/null @@ -1,487 +0,0 @@ -/** - * Integration Test: League Stewarding Use Case Orchestration - * - * Tests the orchestration logic of league stewarding-related Use Cases: - * - GetLeagueStewardingUseCase: Retrieves stewarding dashboard with pending protests, resolved cases, penalties - * - ReviewProtestUseCase: Steward reviews a protest - * - IssuePenaltyUseCase: Steward issues a penalty - * - EditPenaltyUseCase: Steward edits an existing penalty - * - RevokePenaltyUseCase: Steward revokes a penalty - * - ReviewAppealUseCase: Steward reviews an appeal - * - FinalizeProtestDecisionUseCase: Steward finalizes a protest decision - * - FinalizeAppealDecisionUseCase: Steward finalizes an appeal decision - * - NotifyDriversOfDecisionUseCase: Steward notifies drivers of a decision - * - 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, afterAll, beforeEach } from 'vitest'; -import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository'; -import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository'; -import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository'; -import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher'; -import { GetLeagueStewardingUseCase } from '../../../core/leagues/use-cases/GetLeagueStewardingUseCase'; -import { ReviewProtestUseCase } from '../../../core/leagues/use-cases/ReviewProtestUseCase'; -import { IssuePenaltyUseCase } from '../../../core/leagues/use-cases/IssuePenaltyUseCase'; -import { EditPenaltyUseCase } from '../../../core/leagues/use-cases/EditPenaltyUseCase'; -import { RevokePenaltyUseCase } from '../../../core/leagues/use-cases/RevokePenaltyUseCase'; -import { ReviewAppealUseCase } from '../../../core/leagues/use-cases/ReviewAppealUseCase'; -import { FinalizeProtestDecisionUseCase } from '../../../core/leagues/use-cases/FinalizeProtestDecisionUseCase'; -import { FinalizeAppealDecisionUseCase } from '../../../core/leagues/use-cases/FinalizeAppealDecisionUseCase'; -import { NotifyDriversOfDecisionUseCase } from '../../../core/leagues/use-cases/NotifyDriversOfDecisionUseCase'; -import { LeagueStewardingQuery } from '../../../core/leagues/ports/LeagueStewardingQuery'; -import { ReviewProtestCommand } from '../../../core/leagues/ports/ReviewProtestCommand'; -import { IssuePenaltyCommand } from '../../../core/leagues/ports/IssuePenaltyCommand'; -import { EditPenaltyCommand } from '../../../core/leagues/ports/EditPenaltyCommand'; -import { RevokePenaltyCommand } from '../../../core/leagues/ports/RevokePenaltyCommand'; -import { ReviewAppealCommand } from '../../../core/leagues/ports/ReviewAppealCommand'; -import { FinalizeProtestDecisionCommand } from '../../../core/leagues/ports/FinalizeProtestDecisionCommand'; -import { FinalizeAppealDecisionCommand } from '../../../core/leagues/ports/FinalizeAppealDecisionCommand'; -import { NotifyDriversOfDecisionCommand } from '../../../core/leagues/ports/NotifyDriversOfDecisionCommand'; - -describe('League Stewarding Use Case Orchestration', () => { - let leagueRepository: InMemoryLeagueRepository; - let driverRepository: InMemoryDriverRepository; - let raceRepository: InMemoryRaceRepository; - let eventPublisher: InMemoryEventPublisher; - let getLeagueStewardingUseCase: GetLeagueStewardingUseCase; - let reviewProtestUseCase: ReviewProtestUseCase; - let issuePenaltyUseCase: IssuePenaltyUseCase; - let editPenaltyUseCase: EditPenaltyUseCase; - let revokePenaltyUseCase: RevokePenaltyUseCase; - let reviewAppealUseCase: ReviewAppealUseCase; - let finalizeProtestDecisionUseCase: FinalizeProtestDecisionUseCase; - let finalizeAppealDecisionUseCase: FinalizeAppealDecisionUseCase; - let notifyDriversOfDecisionUseCase: NotifyDriversOfDecisionUseCase; - - beforeAll(() => { - // TODO: Initialize In-Memory repositories and event publisher - // leagueRepository = new InMemoryLeagueRepository(); - // driverRepository = new InMemoryDriverRepository(); - // raceRepository = new InMemoryRaceRepository(); - // eventPublisher = new InMemoryEventPublisher(); - // getLeagueStewardingUseCase = new GetLeagueStewardingUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - // reviewProtestUseCase = new ReviewProtestUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - // issuePenaltyUseCase = new IssuePenaltyUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - // editPenaltyUseCase = new EditPenaltyUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - // revokePenaltyUseCase = new RevokePenaltyUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - // reviewAppealUseCase = new ReviewAppealUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - // finalizeProtestDecisionUseCase = new FinalizeProtestDecisionUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - // finalizeAppealDecisionUseCase = new FinalizeAppealDecisionUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - // notifyDriversOfDecisionUseCase = new NotifyDriversOfDecisionUseCase({ - // leagueRepository, - // driverRepository, - // raceRepository, - // eventPublisher, - // }); - }); - - beforeEach(() => { - // TODO: Clear all In-Memory repositories before each test - // leagueRepository.clear(); - // driverRepository.clear(); - // raceRepository.clear(); - // eventPublisher.clear(); - }); - - describe('GetLeagueStewardingUseCase - Success Path', () => { - it('should retrieve stewarding dashboard with pending protests', async () => { - // TODO: Implement test - // Scenario: Steward views stewarding dashboard - // Given: A league exists with pending protests - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show total pending protests - // And: The result should show total resolved cases - // And: The result should show total penalties issued - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve list of pending protests', async () => { - // TODO: Implement test - // Scenario: Steward views pending protests - // Given: A league exists with pending protests - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show a list of pending protests - // And: Each protest should display race, lap, drivers involved, and status - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve list of resolved cases', async () => { - // TODO: Implement test - // Scenario: Steward views resolved cases - // Given: A league exists with resolved cases - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show a list of resolved cases - // And: Each case should display the final decision - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve list of penalties', async () => { - // TODO: Implement test - // Scenario: Steward views penalty list - // Given: A league exists with penalties - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show a list of all penalties issued - // And: Each penalty should display driver, race, type, and status - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve stewarding statistics', async () => { - // TODO: Implement test - // Scenario: Steward views stewarding statistics - // Given: A league exists with stewarding statistics - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show stewarding statistics - // And: Statistics should include average resolution time, etc. - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve stewarding activity log', async () => { - // TODO: Implement test - // Scenario: Steward views stewarding activity log - // Given: A league exists with stewarding activity - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show an activity log of all stewarding actions - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve steward performance metrics', async () => { - // TODO: Implement test - // Scenario: Steward views performance metrics - // Given: A league exists with steward performance metrics - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show performance metrics for the stewarding team - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve steward workload', async () => { - // TODO: Implement test - // Scenario: Steward views workload - // Given: A league exists with steward workload - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show the workload distribution among stewards - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve steward availability', async () => { - // TODO: Implement test - // Scenario: Steward views availability - // Given: A league exists with steward availability - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show the availability of other stewards - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve stewarding notifications', async () => { - // TODO: Implement test - // Scenario: Steward views notifications - // Given: A league exists with stewarding notifications - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show notifications for new protests, appeals, etc. - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve stewarding help and documentation', async () => { - // TODO: Implement test - // Scenario: Steward views help - // Given: A league exists with stewarding help - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show links to stewarding help and documentation - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve stewarding templates', async () => { - // TODO: Implement test - // Scenario: Steward views templates - // Given: A league exists with stewarding templates - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show stewarding decision templates - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should retrieve stewarding reports', async () => { - // TODO: Implement test - // Scenario: Steward views reports - // Given: A league exists with stewarding reports - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show comprehensive stewarding reports - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - }); - - describe('GetLeagueStewardingUseCase - Edge Cases', () => { - it('should handle league with no pending protests', async () => { - // TODO: Implement test - // Scenario: League with no pending protests - // Given: A league exists - // And: The league has no pending protests - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show 0 pending protests - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should handle league with no resolved cases', async () => { - // TODO: Implement test - // Scenario: League with no resolved cases - // Given: A league exists - // And: The league has no resolved cases - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show 0 resolved cases - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should handle league with no penalties issued', async () => { - // TODO: Implement test - // Scenario: League with no penalties issued - // Given: A league exists - // And: The league has no penalties issued - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show 0 penalties issued - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should handle league with no stewarding activity', async () => { - // TODO: Implement test - // Scenario: League with no stewarding activity - // Given: A league exists - // And: The league has no stewarding activity - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show empty activity log - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should handle league with no stewarding notifications', async () => { - // TODO: Implement test - // Scenario: League with no stewarding notifications - // Given: A league exists - // And: The league has no stewarding notifications - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show no notifications - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should handle league with no stewarding templates', async () => { - // TODO: Implement test - // Scenario: League with no stewarding templates - // Given: A league exists - // And: The league has no stewarding templates - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show no templates - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - - it('should handle league with no stewarding reports', async () => { - // TODO: Implement test - // Scenario: League with no stewarding reports - // Given: A league exists - // And: The league has no stewarding reports - // When: GetLeagueStewardingUseCase.execute() is called with league ID - // Then: The result should show no reports - // And: EventPublisher should emit LeagueStewardingAccessedEvent - }); - }); - - describe('GetLeagueStewardingUseCase - Error Handling', () => { - it('should throw error when league does not exist', async () => { - // TODO: Implement test - // Scenario: Non-existent league - // Given: No league exists with the given ID - // When: GetLeagueStewardingUseCase.execute() is called with non-existent league ID - // Then: Should throw LeagueNotFoundError - // And: EventPublisher should NOT emit any events - }); - - it('should throw error when league ID is invalid', async () => { - // TODO: Implement test - // Scenario: Invalid league ID - // Given: An invalid league ID (e.g., empty string, null, undefined) - // When: GetLeagueStewardingUseCase.execute() is called with invalid league ID - // Then: Should throw ValidationError - // And: EventPublisher should NOT emit any events - }); - - it('should handle repository errors gracefully', async () => { - // TODO: Implement test - // Scenario: Repository throws error - // Given: A league exists - // And: LeagueRepository throws an error during query - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Should propagate the error appropriately - // And: EventPublisher should NOT emit any events - }); - }); - - describe('League Stewarding Data Orchestration', () => { - it('should correctly format protest details with evidence', async () => { - // TODO: Implement test - // Scenario: Protest details formatting - // Given: A league exists with protests - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Protest details should show: - // - Race information - // - Lap number - // - Drivers involved - // - Evidence (video links, screenshots) - // - Status (pending, resolved) - }); - - it('should correctly format penalty details with type and amount', async () => { - // TODO: Implement test - // Scenario: Penalty details formatting - // Given: A league exists with penalties - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Penalty details should show: - // - Driver name - // - Race information - // - Penalty type - // - Penalty amount - // - Status (issued, revoked) - }); - - it('should correctly format stewarding statistics', async () => { - // TODO: Implement test - // Scenario: Stewarding statistics formatting - // Given: A league exists with stewarding statistics - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Stewarding statistics should show: - // - Average resolution time - // - Average protest resolution time - // - Average penalty appeal success rate - // - Average protest success rate - // - Average stewarding action success rate - }); - - it('should correctly format stewarding activity log', async () => { - // TODO: Implement test - // Scenario: Stewarding activity log formatting - // Given: A league exists with stewarding activity - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Stewarding activity log should show: - // - Timestamp - // - Action type - // - Steward name - // - Details - }); - - it('should correctly format steward performance metrics', async () => { - // TODO: Implement test - // Scenario: Steward performance metrics formatting - // Given: A league exists with steward performance metrics - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Steward performance metrics should show: - // - Number of cases handled - // - Average resolution time - // - Success rate - // - Workload distribution - }); - - it('should correctly format steward workload distribution', async () => { - // TODO: Implement test - // Scenario: Steward workload distribution formatting - // Given: A league exists with steward workload - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Steward workload should show: - // - Number of cases per steward - // - Workload percentage - // - Availability status - }); - - it('should correctly format steward availability', async () => { - // TODO: Implement test - // Scenario: Steward availability formatting - // Given: A league exists with steward availability - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Steward availability should show: - // - Steward name - // - Availability status - // - Next available time - }); - - it('should correctly format stewarding notifications', async () => { - // TODO: Implement test - // Scenario: Stewarding notifications formatting - // Given: A league exists with stewarding notifications - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Stewarding notifications should show: - // - Notification type - // - Timestamp - // - Details - }); - - it('should correctly format stewarding help and documentation', async () => { - // TODO: Implement test - // Scenario: Stewarding help and documentation formatting - // Given: A league exists with stewarding help - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Stewarding help should show: - // - Links to documentation - // - Help articles - // - Contact information - }); - - it('should correctly format stewarding templates', async () => { - // TODO: Implement test - // Scenario: Stewarding templates formatting - // Given: A league exists with stewarding templates - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Stewarding templates should show: - // - Template name - // - Template content - // - Usage instructions - }); - - it('should correctly format stewarding reports', async () => { - // TODO: Implement test - // Scenario: Stewarding reports formatting - // Given: A league exists with stewarding reports - // When: GetLeagueStewardingUseCase.execute() is called - // Then: Stewarding reports should show: - // - Report type - // - Report period - // - Key metrics - // - Recommendations - }); - }); -}); diff --git a/tests/integration/leagues/league-wallet-use-cases.integration.test.ts b/tests/integration/leagues/league-wallet-use-cases.integration.test.ts deleted file mode 100644 index 9f0ff05be..000000000 --- a/tests/integration/leagues/league-wallet-use-cases.integration.test.ts +++ /dev/null @@ -1,879 +0,0 @@ -/** - * Integration Test: League Wallet Use Case Orchestration - * - * Tests the orchestration logic of league wallet-related Use Cases: - * - GetLeagueWalletUseCase: Retrieves league wallet balance and transaction history - * - GetLeagueWalletBalanceUseCase: Retrieves current league wallet balance - * - GetLeagueWalletTransactionsUseCase: Retrieves league wallet transaction history - * - GetLeagueWalletTransactionDetailsUseCase: Retrieves details of a specific transaction - * - GetLeagueWalletWithdrawalHistoryUseCase: Retrieves withdrawal history - * - GetLeagueWalletDepositHistoryUseCase: Retrieves deposit history - * - GetLeagueWalletPayoutHistoryUseCase: Retrieves payout history - * - GetLeagueWalletRefundHistoryUseCase: Retrieves refund history - * - GetLeagueWalletFeeHistoryUseCase: Retrieves fee history - * - GetLeagueWalletPrizeHistoryUseCase: Retrieves prize history - * - 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, afterAll, beforeEach } from 'vitest'; -import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository'; -import { InMemoryWalletRepository } from '../../../adapters/payments/persistence/inmemory/InMemoryWalletRepository'; -import { InMemoryTransactionRepository } from '../../../adapters/payments/persistence/inmemory/InMemoryTransactionRepository'; -import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher'; -import { GetLeagueWalletUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletUseCase'; -import { GetLeagueWalletBalanceUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletBalanceUseCase'; -import { GetLeagueWalletTransactionsUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletTransactionsUseCase'; -import { GetLeagueWalletTransactionDetailsUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletTransactionDetailsUseCase'; -import { GetLeagueWalletWithdrawalHistoryUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletWithdrawalHistoryUseCase'; -import { GetLeagueWalletDepositHistoryUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletDepositHistoryUseCase'; -import { GetLeagueWalletPayoutHistoryUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletPayoutHistoryUseCase'; -import { GetLeagueWalletRefundHistoryUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletRefundHistoryUseCase'; -import { GetLeagueWalletFeeHistoryUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletFeeHistoryUseCase'; -import { GetLeagueWalletPrizeHistoryUseCase } from '../../../core/leagues/use-cases/GetLeagueWalletPrizeHistoryUseCase'; -import { LeagueWalletQuery } from '../../../core/leagues/ports/LeagueWalletQuery'; -import { LeagueWalletBalanceQuery } from '../../../core/leagues/ports/LeagueWalletBalanceQuery'; -import { LeagueWalletTransactionsQuery } from '../../../core/leagues/ports/LeagueWalletTransactionsQuery'; -import { LeagueWalletTransactionDetailsQuery } from '../../../core/leagues/ports/LeagueWalletTransactionDetailsQuery'; -import { LeagueWalletWithdrawalHistoryQuery } from '../../../core/leagues/ports/LeagueWalletWithdrawalHistoryQuery'; -import { LeagueWalletDepositHistoryQuery } from '../../../core/leagues/ports/LeagueWalletDepositHistoryQuery'; -import { LeagueWalletPayoutHistoryQuery } from '../../../core/leagues/ports/LeagueWalletPayoutHistoryQuery'; -import { LeagueWalletRefundHistoryQuery } from '../../../core/leagues/ports/LeagueWalletRefundHistoryQuery'; -import { LeagueWalletFeeHistoryQuery } from '../../../core/leagues/ports/LeagueWalletFeeHistoryQuery'; -import { LeagueWalletPrizeHistoryQuery } from '../../../core/leagues/ports/LeagueWalletPrizeHistoryQuery'; - -describe('League Wallet Use Case Orchestration', () => { - let leagueRepository: InMemoryLeagueRepository; - let walletRepository: InMemoryWalletRepository; - let transactionRepository: InMemoryTransactionRepository; - let eventPublisher: InMemoryEventPublisher; - let getLeagueWalletUseCase: GetLeagueWalletUseCase; - let getLeagueWalletBalanceUseCase: GetLeagueWalletBalanceUseCase; - let getLeagueWalletTransactionsUseCase: GetLeagueWalletTransactionsUseCase; - let getLeagueWalletTransactionDetailsUseCase: GetLeagueWalletTransactionDetailsUseCase; - let getLeagueWalletWithdrawalHistoryUseCase: GetLeagueWalletWithdrawalHistoryUseCase; - let getLeagueWalletDepositHistoryUseCase: GetLeagueWalletDepositHistoryUseCase; - let getLeagueWalletPayoutHistoryUseCase: GetLeagueWalletPayoutHistoryUseCase; - let getLeagueWalletRefundHistoryUseCase: GetLeagueWalletRefundHistoryUseCase; - let getLeagueWalletFeeHistoryUseCase: GetLeagueWalletFeeHistoryUseCase; - let getLeagueWalletPrizeHistoryUseCase: GetLeagueWalletPrizeHistoryUseCase; - - beforeAll(() => { - // TODO: Initialize In-Memory repositories and event publisher - // leagueRepository = new InMemoryLeagueRepository(); - // walletRepository = new InMemoryWalletRepository(); - // transactionRepository = new InMemoryTransactionRepository(); - // eventPublisher = new InMemoryEventPublisher(); - // getLeagueWalletUseCase = new GetLeagueWalletUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - // getLeagueWalletBalanceUseCase = new GetLeagueWalletBalanceUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - // getLeagueWalletTransactionsUseCase = new GetLeagueWalletTransactionsUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - // getLeagueWalletTransactionDetailsUseCase = new GetLeagueWalletTransactionDetailsUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - // getLeagueWalletWithdrawalHistoryUseCase = new GetLeagueWalletWithdrawalHistoryUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - // getLeagueWalletDepositHistoryUseCase = new GetLeagueWalletDepositHistoryUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - // getLeagueWalletPayoutHistoryUseCase = new GetLeagueWalletPayoutHistoryUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - // getLeagueWalletRefundHistoryUseCase = new GetLeagueWalletRefundHistoryUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - // getLeagueWalletFeeHistoryUseCase = new GetLeagueWalletFeeHistoryUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - // getLeagueWalletPrizeHistoryUseCase = new GetLeagueWalletPrizeHistoryUseCase({ - // leagueRepository, - // walletRepository, - // transactionRepository, - // eventPublisher, - // }); - }); - - beforeEach(() => { - // TODO: Clear all In-Memory repositories before each test - // leagueRepository.clear(); - // walletRepository.clear(); - // transactionRepository.clear(); - // eventPublisher.clear(); - }); - - describe('GetLeagueWalletUseCase - Success Path', () => { - it('should retrieve league wallet overview', async () => { - // TODO: Implement test - // Scenario: Admin views league wallet overview - // Given: A league exists with a wallet - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show wallet overview - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve wallet balance', async () => { - // TODO: Implement test - // Scenario: Admin views wallet balance - // Given: A league exists with a wallet - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show current balance - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve transaction history', async () => { - // TODO: Implement test - // Scenario: Admin views transaction history - // Given: A league exists with transactions - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show transaction history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve withdrawal history', async () => { - // TODO: Implement test - // Scenario: Admin views withdrawal history - // Given: A league exists with withdrawals - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show withdrawal history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve deposit history', async () => { - // TODO: Implement test - // Scenario: Admin views deposit history - // Given: A league exists with deposits - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show deposit history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve payout history', async () => { - // TODO: Implement test - // Scenario: Admin views payout history - // Given: A league exists with payouts - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show payout history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve refund history', async () => { - // TODO: Implement test - // Scenario: Admin views refund history - // Given: A league exists with refunds - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show refund history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve fee history', async () => { - // TODO: Implement test - // Scenario: Admin views fee history - // Given: A league exists with fees - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show fee history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve prize history', async () => { - // TODO: Implement test - // Scenario: Admin views prize history - // Given: A league exists with prizes - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show prize history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve wallet statistics', async () => { - // TODO: Implement test - // Scenario: Admin views wallet statistics - // Given: A league exists with wallet statistics - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show wallet statistics - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve wallet activity log', async () => { - // TODO: Implement test - // Scenario: Admin views wallet activity log - // Given: A league exists with wallet activity - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show wallet activity log - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve wallet alerts', async () => { - // TODO: Implement test - // Scenario: Admin views wallet alerts - // Given: A league exists with wallet alerts - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show wallet alerts - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve wallet settings', async () => { - // TODO: Implement test - // Scenario: Admin views wallet settings - // Given: A league exists with wallet settings - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show wallet settings - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should retrieve wallet reports', async () => { - // TODO: Implement test - // Scenario: Admin views wallet reports - // Given: A league exists with wallet reports - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show wallet reports - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - }); - - describe('GetLeagueWalletUseCase - Edge Cases', () => { - it('should handle league with no transactions', async () => { - // TODO: Implement test - // Scenario: League with no transactions - // Given: A league exists - // And: The league has no transactions - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show empty transaction history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should handle league with no withdrawals', async () => { - // TODO: Implement test - // Scenario: League with no withdrawals - // Given: A league exists - // And: The league has no withdrawals - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show empty withdrawal history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should handle league with no deposits', async () => { - // TODO: Implement test - // Scenario: League with no deposits - // Given: A league exists - // And: The league has no deposits - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show empty deposit history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should handle league with no payouts', async () => { - // TODO: Implement test - // Scenario: League with no payouts - // Given: A league exists - // And: The league has no payouts - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show empty payout history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should handle league with no refunds', async () => { - // TODO: Implement test - // Scenario: League with no refunds - // Given: A league exists - // And: The league has no refunds - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show empty refund history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should handle league with no fees', async () => { - // TODO: Implement test - // Scenario: League with no fees - // Given: A league exists - // And: The league has no fees - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show empty fee history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should handle league with no prizes', async () => { - // TODO: Implement test - // Scenario: League with no prizes - // Given: A league exists - // And: The league has no prizes - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show empty prize history - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should handle league with no wallet alerts', async () => { - // TODO: Implement test - // Scenario: League with no wallet alerts - // Given: A league exists - // And: The league has no wallet alerts - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show no alerts - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - - it('should handle league with no wallet reports', async () => { - // TODO: Implement test - // Scenario: League with no wallet reports - // Given: A league exists - // And: The league has no wallet reports - // When: GetLeagueWalletUseCase.execute() is called with league ID - // Then: The result should show no reports - // And: EventPublisher should emit LeagueWalletAccessedEvent - }); - }); - - describe('GetLeagueWalletUseCase - Error Handling', () => { - it('should throw error when league does not exist', async () => { - // TODO: Implement test - // Scenario: Non-existent league - // Given: No league exists with the given ID - // When: GetLeagueWalletUseCase.execute() is called with non-existent league ID - // Then: Should throw LeagueNotFoundError - // And: EventPublisher should NOT emit any events - }); - - it('should throw error when league ID is invalid', async () => { - // TODO: Implement test - // Scenario: Invalid league ID - // Given: An invalid league ID (e.g., empty string, null, undefined) - // When: GetLeagueWalletUseCase.execute() is called with invalid league ID - // Then: Should throw ValidationError - // And: EventPublisher should NOT emit any events - }); - - it('should handle repository errors gracefully', async () => { - // TODO: Implement test - // Scenario: Repository throws error - // Given: A league exists - // And: WalletRepository throws an error during query - // When: GetLeagueWalletUseCase.execute() is called - // Then: Should propagate the error appropriately - // And: EventPublisher should NOT emit any events - }); - }); - - describe('League Wallet Data Orchestration', () => { - it('should correctly format wallet balance', async () => { - // TODO: Implement test - // Scenario: Wallet balance formatting - // Given: A league exists with a wallet - // When: GetLeagueWalletUseCase.execute() is called - // Then: Wallet balance should show: - // - Current balance - // - Available balance - // - Pending balance - // - Currency - }); - - it('should correctly format transaction history', async () => { - // TODO: Implement test - // Scenario: Transaction history formatting - // Given: A league exists with transactions - // When: GetLeagueWalletUseCase.execute() is called - // Then: Transaction history should show: - // - Transaction ID - // - Transaction type - // - Amount - // - Date - // - Status - // - Description - }); - - it('should correctly format withdrawal history', async () => { - // TODO: Implement test - // Scenario: Withdrawal history formatting - // Given: A league exists with withdrawals - // When: GetLeagueWalletUseCase.execute() is called - // Then: Withdrawal history should show: - // - Withdrawal ID - // - Amount - // - Date - // - Status - // - Destination - }); - - it('should correctly format deposit history', async () => { - // TODO: Implement test - // Scenario: Deposit history formatting - // Given: A league exists with deposits - // When: GetLeagueWalletUseCase.execute() is called - // Then: Deposit history should show: - // - Deposit ID - // - Amount - // - Date - // - Status - // - Source - }); - - it('should correctly format payout history', async () => { - // TODO: Implement test - // Scenario: Payout history formatting - // Given: A league exists with payouts - // When: GetLeagueWalletUseCase.execute() is called - // Then: Payout history should show: - // - Payout ID - // - Amount - // - Date - // - Status - // - Recipient - }); - - it('should correctly format refund history', async () => { - // TODO: Implement test - // Scenario: Refund history formatting - // Given: A league exists with refunds - // When: GetLeagueWalletUseCase.execute() is called - // Then: Refund history should show: - // - Refund ID - // - Amount - // - Date - // - Status - // - Reason - }); - - it('should correctly format fee history', async () => { - // TODO: Implement test - // Scenario: Fee history formatting - // Given: A league exists with fees - // When: GetLeagueWalletUseCase.execute() is called - // Then: Fee history should show: - // - Fee ID - // - Amount - // - Date - // - Type - // - Description - }); - - it('should correctly format prize history', async () => { - // TODO: Implement test - // Scenario: Prize history formatting - // Given: A league exists with prizes - // When: GetLeagueWalletUseCase.execute() is called - // Then: Prize history should show: - // - Prize ID - // - Amount - // - Date - // - Type - // - Recipient - }); - - it('should correctly format wallet statistics', async () => { - // TODO: Implement test - // Scenario: Wallet statistics formatting - // Given: A league exists with wallet statistics - // When: GetLeagueWalletUseCase.execute() is called - // Then: Wallet statistics should show: - // - Total deposits - // - Total withdrawals - // - Total payouts - // - Total fees - // - Total prizes - // - Net balance - }); - - it('should correctly format wallet activity log', async () => { - // TODO: Implement test - // Scenario: Wallet activity log formatting - // Given: A league exists with wallet activity - // When: GetLeagueWalletUseCase.execute() is called - // Then: Wallet activity log should show: - // - Timestamp - // - Action type - // - User - // - Details - }); - - it('should correctly format wallet alerts', async () => { - // TODO: Implement test - // Scenario: Wallet alerts formatting - // Given: A league exists with wallet alerts - // When: GetLeagueWalletUseCase.execute() is called - // Then: Wallet alerts should show: - // - Alert type - // - Timestamp - // - Details - }); - - it('should correctly format wallet settings', async () => { - // TODO: Implement test - // Scenario: Wallet settings formatting - // Given: A league exists with wallet settings - // When: GetLeagueWalletUseCase.execute() is called - // Then: Wallet settings should show: - // - Currency - // - Auto-payout settings - // - Fee settings - // - Prize settings - }); - - it('should correctly format wallet reports', async () => { - // TODO: Implement test - // Scenario: Wallet reports formatting - // Given: A league exists with wallet reports - // When: GetLeagueWalletUseCase.execute() is called - // Then: Wallet reports should show: - // - Report type - // - Report period - // - Key metrics - // - Recommendations - }); - }); - - describe('GetLeagueWalletBalanceUseCase - Success Path', () => { - it('should retrieve current wallet balance', async () => { - // TODO: Implement test - // Scenario: Admin views current wallet balance - // Given: A league exists with a wallet - // When: GetLeagueWalletBalanceUseCase.execute() is called with league ID - // Then: The result should show current balance - // And: EventPublisher should emit LeagueWalletBalanceAccessedEvent - }); - - it('should retrieve available balance', async () => { - // TODO: Implement test - // Scenario: Admin views available balance - // Given: A league exists with a wallet - // When: GetLeagueWalletBalanceUseCase.execute() is called with league ID - // Then: The result should show available balance - // And: EventPublisher should emit LeagueWalletBalanceAccessedEvent - }); - - it('should retrieve pending balance', async () => { - // TODO: Implement test - // Scenario: Admin views pending balance - // Given: A league exists with a wallet - // When: GetLeagueWalletBalanceUseCase.execute() is called with league ID - // Then: The result should show pending balance - // And: EventPublisher should emit LeagueWalletBalanceAccessedEvent - }); - - it('should retrieve balance in correct currency', async () => { - // TODO: Implement test - // Scenario: Admin views balance in correct currency - // Given: A league exists with a wallet - // When: GetLeagueWalletBalanceUseCase.execute() is called with league ID - // Then: The result should show balance in correct currency - // And: EventPublisher should emit LeagueWalletBalanceAccessedEvent - }); - }); - - describe('GetLeagueWalletTransactionsUseCase - Success Path', () => { - it('should retrieve transaction history with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views transaction history with pagination - // Given: A league exists with many transactions - // When: GetLeagueWalletTransactionsUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated transaction history - // And: EventPublisher should emit LeagueWalletTransactionsAccessedEvent - }); - - it('should retrieve transaction history filtered by type', async () => { - // TODO: Implement test - // Scenario: Admin views transaction history filtered by type - // Given: A league exists with transactions of different types - // When: GetLeagueWalletTransactionsUseCase.execute() is called with league ID and type filter - // Then: The result should show filtered transaction history - // And: EventPublisher should emit LeagueWalletTransactionsAccessedEvent - }); - - it('should retrieve transaction history filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views transaction history filtered by date range - // Given: A league exists with transactions over time - // When: GetLeagueWalletTransactionsUseCase.execute() is called with league ID and date range - // Then: The result should show filtered transaction history - // And: EventPublisher should emit LeagueWalletTransactionsAccessedEvent - }); - - it('should retrieve transaction history sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views transaction history sorted by date - // Given: A league exists with transactions - // When: GetLeagueWalletTransactionsUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted transaction history - // And: EventPublisher should emit LeagueWalletTransactionsAccessedEvent - }); - }); - - describe('GetLeagueWalletTransactionDetailsUseCase - Success Path', () => { - it('should retrieve transaction details', async () => { - // TODO: Implement test - // Scenario: Admin views transaction details - // Given: A league exists with a transaction - // When: GetLeagueWalletTransactionDetailsUseCase.execute() is called with league ID and transaction ID - // Then: The result should show transaction details - // And: EventPublisher should emit LeagueWalletTransactionDetailsAccessedEvent - }); - - it('should retrieve transaction with all metadata', async () => { - // TODO: Implement test - // Scenario: Admin views transaction with metadata - // Given: A league exists with a transaction - // When: GetLeagueWalletTransactionDetailsUseCase.execute() is called with league ID and transaction ID - // Then: The result should show transaction with all metadata - // And: EventPublisher should emit LeagueWalletTransactionDetailsAccessedEvent - }); - }); - - describe('GetLeagueWalletWithdrawalHistoryUseCase - Success Path', () => { - it('should retrieve withdrawal history with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views withdrawal history with pagination - // Given: A league exists with many withdrawals - // When: GetLeagueWalletWithdrawalHistoryUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated withdrawal history - // And: EventPublisher should emit LeagueWalletWithdrawalHistoryAccessedEvent - }); - - it('should retrieve withdrawal history filtered by status', async () => { - // TODO: Implement test - // Scenario: Admin views withdrawal history filtered by status - // Given: A league exists with withdrawals of different statuses - // When: GetLeagueWalletWithdrawalHistoryUseCase.execute() is called with league ID and status filter - // Then: The result should show filtered withdrawal history - // And: EventPublisher should emit LeagueWalletWithdrawalHistoryAccessedEvent - }); - - it('should retrieve withdrawal history filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views withdrawal history filtered by date range - // Given: A league exists with withdrawals over time - // When: GetLeagueWalletWithdrawalHistoryUseCase.execute() is called with league ID and date range - // Then: The result should show filtered withdrawal history - // And: EventPublisher should emit LeagueWalletWithdrawalHistoryAccessedEvent - }); - - it('should retrieve withdrawal history sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views withdrawal history sorted by date - // Given: A league exists with withdrawals - // When: GetLeagueWalletWithdrawalHistoryUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted withdrawal history - // And: EventPublisher should emit LeagueWalletWithdrawalHistoryAccessedEvent - }); - }); - - describe('GetLeagueWalletDepositHistoryUseCase - Success Path', () => { - it('should retrieve deposit history with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views deposit history with pagination - // Given: A league exists with many deposits - // When: GetLeagueWalletDepositHistoryUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated deposit history - // And: EventPublisher should emit LeagueWalletDepositHistoryAccessedEvent - }); - - it('should retrieve deposit history filtered by status', async () => { - // TODO: Implement test - // Scenario: Admin views deposit history filtered by status - // Given: A league exists with deposits of different statuses - // When: GetLeagueWalletDepositHistoryUseCase.execute() is called with league ID and status filter - // Then: The result should show filtered deposit history - // And: EventPublisher should emit LeagueWalletDepositHistoryAccessedEvent - }); - - it('should retrieve deposit history filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views deposit history filtered by date range - // Given: A league exists with deposits over time - // When: GetLeagueWalletDepositHistoryUseCase.execute() is called with league ID and date range - // Then: The result should show filtered deposit history - // And: EventPublisher should emit LeagueWalletDepositHistoryAccessedEvent - }); - - it('should retrieve deposit history sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views deposit history sorted by date - // Given: A league exists with deposits - // When: GetLeagueWalletDepositHistoryUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted deposit history - // And: EventPublisher should emit LeagueWalletDepositHistoryAccessedEvent - }); - }); - - describe('GetLeagueWalletPayoutHistoryUseCase - Success Path', () => { - it('should retrieve payout history with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views payout history with pagination - // Given: A league exists with many payouts - // When: GetLeagueWalletPayoutHistoryUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated payout history - // And: EventPublisher should emit LeagueWalletPayoutHistoryAccessedEvent - }); - - it('should retrieve payout history filtered by status', async () => { - // TODO: Implement test - // Scenario: Admin views payout history filtered by status - // Given: A league exists with payouts of different statuses - // When: GetLeagueWalletPayoutHistoryUseCase.execute() is called with league ID and status filter - // Then: The result should show filtered payout history - // And: EventPublisher should emit LeagueWalletPayoutHistoryAccessedEvent - }); - - it('should retrieve payout history filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views payout history filtered by date range - // Given: A league exists with payouts over time - // When: GetLeagueWalletPayoutHistoryUseCase.execute() is called with league ID and date range - // Then: The result should show filtered payout history - // And: EventPublisher should emit LeagueWalletPayoutHistoryAccessedEvent - }); - - it('should retrieve payout history sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views payout history sorted by date - // Given: A league exists with payouts - // When: GetLeagueWalletPayoutHistoryUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted payout history - // And: EventPublisher should emit LeagueWalletPayoutHistoryAccessedEvent - }); - }); - - describe('GetLeagueWalletRefundHistoryUseCase - Success Path', () => { - it('should retrieve refund history with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views refund history with pagination - // Given: A league exists with many refunds - // When: GetLeagueWalletRefundHistoryUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated refund history - // And: EventPublisher should emit LeagueWalletRefundHistoryAccessedEvent - }); - - it('should retrieve refund history filtered by status', async () => { - // TODO: Implement test - // Scenario: Admin views refund history filtered by status - // Given: A league exists with refunds of different statuses - // When: GetLeagueWalletRefundHistoryUseCase.execute() is called with league ID and status filter - // Then: The result should show filtered refund history - // And: EventPublisher should emit LeagueWalletRefundHistoryAccessedEvent - }); - - it('should retrieve refund history filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views refund history filtered by date range - // Given: A league exists with refunds over time - // When: GetLeagueWalletRefundHistoryUseCase.execute() is called with league ID and date range - // Then: The result should show filtered refund history - // And: EventPublisher should emit LeagueWalletRefundHistoryAccessedEvent - }); - - it('should retrieve refund history sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views refund history sorted by date - // Given: A league exists with refunds - // When: GetLeagueWalletRefundHistoryUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted refund history - // And: EventPublisher should emit LeagueWalletRefundHistoryAccessedEvent - }); - }); - - describe('GetLeagueWalletFeeHistoryUseCase - Success Path', () => { - it('should retrieve fee history with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views fee history with pagination - // Given: A league exists with many fees - // When: GetLeagueWalletFeeHistoryUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated fee history - // And: EventPublisher should emit LeagueWalletFeeHistoryAccessedEvent - }); - - it('should retrieve fee history filtered by type', async () => { - // TODO: Implement test - // Scenario: Admin views fee history filtered by type - // Given: A league exists with fees of different types - // When: GetLeagueWalletFeeHistoryUseCase.execute() is called with league ID and type filter - // Then: The result should show filtered fee history - // And: EventPublisher should emit LeagueWalletFeeHistoryAccessedEvent - }); - - it('should retrieve fee history filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views fee history filtered by date range - // Given: A league exists with fees over time - // When: GetLeagueWalletFeeHistoryUseCase.execute() is called with league ID and date range - // Then: The result should show filtered fee history - // And: EventPublisher should emit LeagueWalletFeeHistoryAccessedEvent - }); - - it('should retrieve fee history sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views fee history sorted by date - // Given: A league exists with fees - // When: GetLeagueWalletFeeHistoryUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted fee history - // And: EventPublisher should emit LeagueWalletFeeHistoryAccessedEvent - }); - }); - - describe('GetLeagueWalletPrizeHistoryUseCase - Success Path', () => { - it('should retrieve prize history with pagination', async () => { - // TODO: Implement test - // Scenario: Admin views prize history with pagination - // Given: A league exists with many prizes - // When: GetLeagueWalletPrizeHistoryUseCase.execute() is called with league ID and pagination - // Then: The result should show paginated prize history - // And: EventPublisher should emit LeagueWalletPrizeHistoryAccessedEvent - }); - - it('should retrieve prize history filtered by type', async () => { - // TODO: Implement test - // Scenario: Admin views prize history filtered by type - // Given: A league exists with prizes of different types - // When: GetLeagueWalletPrizeHistoryUseCase.execute() is called with league ID and type filter - // Then: The result should show filtered prize history - // And: EventPublisher should emit LeagueWalletPrizeHistoryAccessedEvent - }); - - it('should retrieve prize history filtered by date range', async () => { - // TODO: Implement test - // Scenario: Admin views prize history filtered by date range - // Given: A league exists with prizes over time - // When: GetLeagueWalletPrizeHistoryUseCase.execute() is called with league ID and date range - // Then: The result should show filtered prize history - // And: EventPublisher should emit LeagueWalletPrizeHistoryAccessedEvent - }); - - it('should retrieve prize history sorted by date', async () => { - // TODO: Implement test - // Scenario: Admin views prize history sorted by date - // Given: A league exists with prizes - // When: GetLeagueWalletPrizeHistoryUseCase.execute() is called with league ID and sort order - // Then: The result should show sorted prize history - // And: EventPublisher should emit LeagueWalletPrizeHistoryAccessedEvent - }); - }); -}); diff --git a/tests/integration/leagues/schedule/GetLeagueSchedule.test.ts b/tests/integration/leagues/schedule/GetLeagueSchedule.test.ts new file mode 100644 index 000000000..1e591f1cf --- /dev/null +++ b/tests/integration/leagues/schedule/GetLeagueSchedule.test.ts @@ -0,0 +1,239 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { League as RacingLeague } from '../../../../core/racing/domain/entities/League'; +import { Season } from '../../../../core/racing/domain/entities/season/Season'; +import { Race } from '../../../../core/racing/domain/entities/Race'; + +describe('League Schedule - GetLeagueScheduleUseCase', () => { + let context: LeaguesTestContext; + + beforeEach(() => { + context = new LeaguesTestContext(); + context.clear(); + }); + + const seedRacingLeague = async (params: { leagueId: string }) => { + const league = RacingLeague.create({ + id: params.leagueId, + name: 'Racing League', + description: 'League used for schedule integration tests', + ownerId: 'driver-123', + }); + + await context.racingLeagueRepository.create(league); + return league; + }; + + const seedSeason = async (params: { + seasonId: string; + leagueId: string; + startDate: Date; + endDate: Date; + status?: 'planned' | 'active' | 'completed' | 'archived' | 'cancelled'; + schedulePublished?: boolean; + }) => { + const season = Season.create({ + id: params.seasonId, + leagueId: params.leagueId, + gameId: 'iracing', + name: 'Season 1', + status: params.status ?? 'active', + startDate: params.startDate, + endDate: params.endDate, + ...(params.schedulePublished !== undefined ? { schedulePublished: params.schedulePublished } : {}), + }); + + await context.seasonRepository.add(season); + return season; + }; + + it('returns schedule for active season and races within season window', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + const seasonId = 'season-jan'; + await seedSeason({ + seasonId, + leagueId, + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + status: 'active', + }); + + const createRace1 = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Track A', + car: 'Car A', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + }); + expect(createRace1.isOk()).toBe(true); + + const createRace2 = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Track B', + car: 'Car B', + scheduledAt: new Date('2025-01-20T20:00:00Z'), + }); + expect(createRace2.isOk()).toBe(true); + + const result = await context.getLeagueScheduleUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + const value = result.unwrap(); + expect(value.league.id.toString()).toBe(leagueId); + expect(value.seasonId).toBe(seasonId); + expect(value.published).toBe(false); + expect(value.races.map(r => r.race.track)).toEqual(['Track A', 'Track B']); + }); + + it('scopes schedule by seasonId (no season date bleed)', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + const janSeasonId = 'season-jan'; + const febSeasonId = 'season-feb'; + + await seedSeason({ + seasonId: janSeasonId, + leagueId, + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + status: 'active', + }); + + await seedSeason({ + seasonId: febSeasonId, + leagueId, + startDate: new Date('2025-02-01T00:00:00Z'), + endDate: new Date('2025-02-28T23:59:59Z'), + status: 'planned', + }); + + const janRace = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId: janSeasonId, + track: 'Track Jan', + car: 'Car Jan', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + }); + expect(janRace.isOk()).toBe(true); + + const febRace = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId: febSeasonId, + track: 'Track Feb', + car: 'Car Feb', + scheduledAt: new Date('2025-02-10T20:00:00Z'), + }); + expect(febRace.isOk()).toBe(true); + + const janResult = await context.getLeagueScheduleUseCase.execute({ + leagueId, + seasonId: janSeasonId, + }); + + expect(janResult.isOk()).toBe(true); + const janValue = janResult.unwrap(); + expect(janValue.seasonId).toBe(janSeasonId); + expect(janValue.races.map(r => r.race.track)).toEqual(['Track Jan']); + + const febResult = await context.getLeagueScheduleUseCase.execute({ + leagueId, + seasonId: febSeasonId, + }); + + expect(febResult.isOk()).toBe(true); + const febValue = febResult.unwrap(); + expect(febValue.seasonId).toBe(febSeasonId); + expect(febValue.races.map(r => r.race.track)).toEqual(['Track Feb']); + }); + + it('returns all races when no seasons exist for league', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + await context.raceRepository.create( + Race.create({ + id: 'race-1', + leagueId, + scheduledAt: new Date('2025-01-10T20:00:00Z'), + track: 'Track 1', + car: 'Car 1', + }), + ); + + await context.raceRepository.create( + Race.create({ + id: 'race-2', + leagueId, + scheduledAt: new Date('2025-01-15T20:00:00Z'), + track: 'Track 2', + car: 'Car 2', + }), + ); + + const result = await context.getLeagueScheduleUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + const value = result.unwrap(); + expect(value.seasonId).toBe('no-season'); + expect(value.published).toBe(false); + expect(value.races.map(r => r.race.track)).toEqual(['Track 1', 'Track 2']); + }); + + it('reflects schedule published state from the selected season', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + const seasonId = 'season-1'; + await seedSeason({ + seasonId, + leagueId, + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + status: 'active', + schedulePublished: false, + }); + + const pre = await context.getLeagueScheduleUseCase.execute({ leagueId }); + expect(pre.isOk()).toBe(true); + expect(pre.unwrap().published).toBe(false); + + const publish = await context.publishLeagueSeasonScheduleUseCase.execute({ leagueId, seasonId }); + expect(publish.isOk()).toBe(true); + + const post = await context.getLeagueScheduleUseCase.execute({ leagueId }); + expect(post.isOk()).toBe(true); + expect(post.unwrap().published).toBe(true); + }); + + it('returns LEAGUE_NOT_FOUND when league does not exist', async () => { + const result = await context.getLeagueScheduleUseCase.execute({ leagueId: 'missing-league' }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('LEAGUE_NOT_FOUND'); + }); + + it('returns SEASON_NOT_FOUND when requested season does not belong to the league', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + await seedSeason({ + seasonId: 'season-other', + leagueId: 'league-other', + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + status: 'active', + }); + + const result = await context.getLeagueScheduleUseCase.execute({ + leagueId, + seasonId: 'season-other', + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('SEASON_NOT_FOUND'); + }); +}); diff --git a/tests/integration/leagues/schedule/RaceManagement.test.ts b/tests/integration/leagues/schedule/RaceManagement.test.ts new file mode 100644 index 000000000..ba6af5403 --- /dev/null +++ b/tests/integration/leagues/schedule/RaceManagement.test.ts @@ -0,0 +1,174 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { League as RacingLeague } from '../../../../core/racing/domain/entities/League'; +import { Season } from '../../../../core/racing/domain/entities/season/Season'; + +describe('League Schedule - Race Management', () => { + let context: LeaguesTestContext; + + beforeEach(() => { + context = new LeaguesTestContext(); + context.clear(); + }); + + const seedRacingLeague = async (params: { leagueId: string }) => { + const league = RacingLeague.create({ + id: params.leagueId, + name: 'Racing League', + description: 'League used for schedule integration tests', + ownerId: 'driver-123', + }); + + await context.racingLeagueRepository.create(league); + return league; + }; + + const seedSeason = async (params: { + seasonId: string; + leagueId: string; + startDate: Date; + endDate: Date; + status?: 'planned' | 'active' | 'completed' | 'archived' | 'cancelled'; + }) => { + const season = Season.create({ + id: params.seasonId, + leagueId: params.leagueId, + gameId: 'iracing', + name: 'Season 1', + status: params.status ?? 'active', + startDate: params.startDate, + endDate: params.endDate, + }); + + await context.seasonRepository.add(season); + return season; + }; + + it('creates a race in a season schedule', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + const seasonId = 'season-1'; + await seedSeason({ + seasonId, + leagueId, + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + }); + + const create = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Monza', + car: 'GT3', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + }); + + expect(create.isOk()).toBe(true); + const { raceId } = create.unwrap(); + + const schedule = await context.getLeagueScheduleUseCase.execute({ leagueId, seasonId }); + expect(schedule.isOk()).toBe(true); + expect(schedule.unwrap().races.map(r => r.race.id)).toEqual([raceId]); + }); + + it('updates an existing scheduled race (track/car/when)', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + const seasonId = 'season-1'; + await seedSeason({ + seasonId, + leagueId, + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + }); + + const create = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Old Track', + car: 'Old Car', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + }); + expect(create.isOk()).toBe(true); + const { raceId } = create.unwrap(); + + const update = await context.updateLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + raceId, + track: 'New Track', + car: 'New Car', + scheduledAt: new Date('2025-01-20T20:00:00Z'), + }); + + expect(update.isOk()).toBe(true); + + const schedule = await context.getLeagueScheduleUseCase.execute({ leagueId, seasonId }); + expect(schedule.isOk()).toBe(true); + const race = schedule.unwrap().races[0]?.race; + expect(race?.id).toBe(raceId); + expect(race?.track).toBe('New Track'); + expect(race?.car).toBe('New Car'); + expect(race?.scheduledAt.toISOString()).toBe('2025-01-20T20:00:00.000Z'); + }); + + it('deletes a scheduled race from the season', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + const seasonId = 'season-1'; + await seedSeason({ + seasonId, + leagueId, + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + }); + + const create = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Track 1', + car: 'Car 1', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + }); + expect(create.isOk()).toBe(true); + const { raceId } = create.unwrap(); + + const pre = await context.getLeagueScheduleUseCase.execute({ leagueId, seasonId }); + expect(pre.isOk()).toBe(true); + expect(pre.unwrap().races.map(r => r.race.id)).toEqual([raceId]); + + const del = await context.deleteLeagueSeasonScheduleRaceUseCase.execute({ leagueId, seasonId, raceId }); + expect(del.isOk()).toBe(true); + + const post = await context.getLeagueScheduleUseCase.execute({ leagueId, seasonId }); + expect(post.isOk()).toBe(true); + expect(post.unwrap().races).toHaveLength(0); + }); + + it('rejects creating a race outside the season window', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + const seasonId = 'season-1'; + await seedSeason({ + seasonId, + leagueId, + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + }); + + const create = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Track', + car: 'Car', + scheduledAt: new Date('2025-02-10T20:00:00Z'), + }); + + expect(create.isErr()).toBe(true); + expect(create.unwrapErr().code).toBe('RACE_OUTSIDE_SEASON_WINDOW'); + }); +}); diff --git a/tests/integration/leagues/schedule/RaceRegistration.test.ts b/tests/integration/leagues/schedule/RaceRegistration.test.ts new file mode 100644 index 000000000..6cbf07ad9 --- /dev/null +++ b/tests/integration/leagues/schedule/RaceRegistration.test.ts @@ -0,0 +1,178 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { League as RacingLeague } from '../../../../core/racing/domain/entities/League'; +import { Season } from '../../../../core/racing/domain/entities/season/Season'; +import { LeagueMembership } from '../../../../core/racing/domain/entities/LeagueMembership'; + +// Note: the current racing module does not expose explicit "open/close registration" use-cases. +// Registration is modeled via membership + registrations repository interactions. + +describe('League Schedule - Race Registration', () => { + let context: LeaguesTestContext; + + beforeEach(() => { + context = new LeaguesTestContext(); + context.clear(); + }); + + const seedRacingLeague = async (params: { leagueId: string }) => { + const league = RacingLeague.create({ + id: params.leagueId, + name: 'Racing League', + description: 'League used for registration integration tests', + ownerId: 'driver-123', + }); + + await context.racingLeagueRepository.create(league); + return league; + }; + + const seedSeason = async (params: { + seasonId: string; + leagueId: string; + startDate?: Date; + endDate?: Date; + }) => { + const season = Season.create({ + id: params.seasonId, + leagueId: params.leagueId, + gameId: 'iracing', + name: 'Season 1', + status: 'active', + startDate: params.startDate ?? new Date('2025-01-01T00:00:00Z'), + endDate: params.endDate ?? new Date('2025-01-31T23:59:59Z'), + }); + + await context.seasonRepository.add(season); + return season; + }; + + const seedActiveMembership = async (params: { leagueId: string; driverId: string }) => { + const membership = LeagueMembership.create({ + leagueId: params.leagueId, + driverId: params.driverId, + role: 'member', + status: 'active', + }); + + await context.leagueMembershipRepository.saveMembership(membership); + return membership; + }; + + it('registers an active league member for a race', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + const driverId = 'driver-1'; + + await seedRacingLeague({ leagueId }); + await seedSeason({ leagueId, seasonId }); + await seedActiveMembership({ leagueId, driverId }); + + const createRace = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Monza', + car: 'GT3', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + }); + expect(createRace.isOk()).toBe(true); + const { raceId } = createRace.unwrap(); + + const register = await context.registerForRaceUseCase.execute({ + leagueId, + raceId, + driverId, + }); + + expect(register.isOk()).toBe(true); + expect(register.unwrap()).toEqual({ raceId, driverId, status: 'registered' }); + + const isRegistered = await context.raceRegistrationRepository.isRegistered(raceId, driverId); + expect(isRegistered).toBe(true); + }); + + it('rejects registration when driver is not an active member', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + + await seedRacingLeague({ leagueId }); + await seedSeason({ leagueId, seasonId }); + + const createRace = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Monza', + car: 'GT3', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + }); + expect(createRace.isOk()).toBe(true); + + const { raceId } = createRace.unwrap(); + const result = await context.registerForRaceUseCase.execute({ leagueId, raceId, driverId: 'driver-missing' }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('NOT_ACTIVE_MEMBER'); + }); + + it('rejects duplicate registration', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + const driverId = 'driver-1'; + + await seedRacingLeague({ leagueId }); + await seedSeason({ leagueId, seasonId }); + await seedActiveMembership({ leagueId, driverId }); + + const createRace = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Monza', + car: 'GT3', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + }); + expect(createRace.isOk()).toBe(true); + const { raceId } = createRace.unwrap(); + + const first = await context.registerForRaceUseCase.execute({ leagueId, raceId, driverId }); + expect(first.isOk()).toBe(true); + + const second = await context.registerForRaceUseCase.execute({ leagueId, raceId, driverId }); + expect(second.isErr()).toBe(true); + expect(second.unwrapErr().code).toBe('ALREADY_REGISTERED'); + }); + + it('withdraws an existing registration for an upcoming race', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + const driverId = 'driver-1'; + + await seedRacingLeague({ leagueId }); + await seedSeason({ + leagueId, + seasonId, + startDate: new Date('2000-01-01T00:00:00Z'), + endDate: new Date('2100-12-31T23:59:59Z'), + }); + await seedActiveMembership({ leagueId, driverId }); + + const createRace = await context.createLeagueSeasonScheduleRaceUseCase.execute({ + leagueId, + seasonId, + track: 'Monza', + car: 'GT3', + scheduledAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }); + expect(createRace.isOk()).toBe(true); + const { raceId } = createRace.unwrap(); + + const register = await context.registerForRaceUseCase.execute({ leagueId, raceId, driverId }); + expect(register.isOk()).toBe(true); + + const withdraw = await context.withdrawFromRaceUseCase.execute({ raceId, driverId }); + expect(withdraw.isOk()).toBe(true); + expect(withdraw.unwrap()).toEqual({ raceId, driverId, status: 'withdrawn' }); + + const isRegistered = await context.raceRegistrationRepository.isRegistered(raceId, driverId); + expect(isRegistered).toBe(false); + }); +}); diff --git a/tests/integration/leagues/sponsorships/GetLeagueSponsorships.test.ts b/tests/integration/leagues/sponsorships/GetLeagueSponsorships.test.ts new file mode 100644 index 000000000..1fc116d90 --- /dev/null +++ b/tests/integration/leagues/sponsorships/GetLeagueSponsorships.test.ts @@ -0,0 +1,175 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { League } from '../../../../core/racing/domain/entities/League'; +import { Season } from '../../../../core/racing/domain/entities/season/Season'; +import { SeasonSponsorship } from '../../../../core/racing/domain/entities/season/SeasonSponsorship'; +import { Money } from '../../../../core/racing/domain/value-objects/Money'; +import { GetSeasonSponsorshipsUseCase } from '../../../../core/racing/application/use-cases/GetSeasonSponsorshipsUseCase'; +import { LeagueMembership } from '../../../../core/racing/domain/entities/LeagueMembership'; +import { Race } from '../../../../core/racing/domain/entities/Race'; + +describe('League Sponsorships - GetSeasonSponsorshipsUseCase', () => { + let context: LeaguesTestContext; + let useCase: GetSeasonSponsorshipsUseCase; + + beforeEach(() => { + context = new LeaguesTestContext(); + context.clear(); + + useCase = new GetSeasonSponsorshipsUseCase( + context.seasonSponsorshipRepository, + context.seasonRepository, + context.racingLeagueRepository, + context.leagueMembershipRepository, + context.raceRepository, + ); + }); + + const seedLeague = async (params: { leagueId: string }) => { + const league = League.create({ + id: params.leagueId, + name: 'League 1', + description: 'League used for sponsorship integration tests', + ownerId: 'owner-1', + }); + + await context.racingLeagueRepository.create(league); + return league; + }; + + const seedSeason = async (params: { seasonId: string; leagueId: string }) => { + const season = Season.create({ + id: params.seasonId, + leagueId: params.leagueId, + gameId: 'iracing', + name: 'Season 1', + status: 'active', + startDate: new Date('2025-01-01T00:00:00.000Z'), + endDate: new Date('2025-02-01T00:00:00.000Z'), + }); + + await context.seasonRepository.create(season); + return season; + }; + + const seedLeagueMembers = async (params: { leagueId: string; count: number }) => { + for (let i = 0; i < params.count; i++) { + const membership = LeagueMembership.create({ + id: `membership-${i + 1}`, + leagueId: params.leagueId, + driverId: `driver-${i + 1}`, + role: 'member', + status: 'active', + }); + + await context.leagueMembershipRepository.saveMembership(membership); + } + }; + + const seedRaces = async (params: { leagueId: string }) => { + await context.raceRepository.create( + Race.create({ + id: 'race-1', + leagueId: params.leagueId, + track: 'Track 1', + car: 'GT3', + scheduledAt: new Date('2025-01-10T20:00:00.000Z'), + status: 'completed', + }), + ); + + await context.raceRepository.create( + Race.create({ + id: 'race-2', + leagueId: params.leagueId, + track: 'Track 2', + car: 'GT3', + scheduledAt: new Date('2025-01-20T20:00:00.000Z'), + status: 'completed', + }), + ); + + await context.raceRepository.create( + Race.create({ + id: 'race-3', + leagueId: params.leagueId, + track: 'Track 3', + car: 'GT3', + scheduledAt: new Date('2025-01-25T20:00:00.000Z'), + status: 'planned', + }), + ); + }; + + it('returns sponsorships with computed league/season metrics', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + + await seedLeague({ leagueId }); + await seedSeason({ seasonId, leagueId }); + await seedLeagueMembers({ leagueId, count: 3 }); + await seedRaces({ leagueId }); + + const sponsorship = SeasonSponsorship.create({ + id: 'sponsorship-1', + seasonId, + leagueId, + sponsorId: 'sponsor-1', + tier: 'main', + pricing: Money.create(1000, 'USD'), + status: 'active', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + activatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + await context.seasonSponsorshipRepository.create(sponsorship); + + const result = await useCase.execute({ seasonId }); + + expect(result.isOk()).toBe(true); + const view = result.unwrap(); + + expect(view.seasonId).toBe(seasonId); + expect(view.sponsorships).toHaveLength(1); + + const detail = view.sponsorships[0]!; + expect(detail.id).toBe('sponsorship-1'); + expect(detail.leagueId).toBe(leagueId); + expect(detail.leagueName).toBe('League 1'); + expect(detail.seasonId).toBe(seasonId); + expect(detail.seasonName).toBe('Season 1'); + + expect(detail.metrics.drivers).toBe(3); + expect(detail.metrics.races).toBe(3); + expect(detail.metrics.completedRaces).toBe(2); + expect(detail.metrics.impressions).toBe(2 * 3 * 100); + + expect(detail.pricing).toEqual({ amount: 1000, currency: 'USD' }); + expect(detail.platformFee).toEqual({ amount: 100, currency: 'USD' }); + expect(detail.netAmount).toEqual({ amount: 900, currency: 'USD' }); + }); + + it('returns SEASON_NOT_FOUND when season does not exist', async () => { + const result = await useCase.execute({ seasonId: 'missing-season' }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('SEASON_NOT_FOUND'); + }); + + it('returns LEAGUE_NOT_FOUND when league for season does not exist', async () => { + await context.seasonRepository.create( + Season.create({ + id: 'season-1', + leagueId: 'missing-league', + gameId: 'iracing', + name: 'Season 1', + status: 'active', + }), + ); + + const result = await useCase.execute({ seasonId: 'season-1' }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('LEAGUE_NOT_FOUND'); + }); +}); diff --git a/tests/integration/leagues/sponsorships/SponsorshipApplications.test.ts b/tests/integration/leagues/sponsorships/SponsorshipApplications.test.ts new file mode 100644 index 000000000..a3945d683 --- /dev/null +++ b/tests/integration/leagues/sponsorships/SponsorshipApplications.test.ts @@ -0,0 +1,236 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { NotificationService } from '../../../../core/notifications/application/ports/NotificationService'; +import type { WalletRepository } from '../../../../core/payments/domain/repositories/WalletRepository'; +import type { Logger } from '../../../../core/shared/domain/Logger'; +import { LeagueWallet } from '../../../../core/racing/domain/entities/league-wallet/LeagueWallet'; +import { League } from '../../../../core/racing/domain/entities/League'; +import { Season } from '../../../../core/racing/domain/entities/season/Season'; +import { SponsorshipRequest } from '../../../../core/racing/domain/entities/SponsorshipRequest'; +import { Money } from '../../../../core/racing/domain/value-objects/Money'; +import { Sponsor } from '../../../../core/racing/domain/entities/sponsor/Sponsor'; +import { SponsorshipPricing } from '../../../../core/racing/domain/value-objects/SponsorshipPricing'; + +import { ApplyForSponsorshipUseCase } from '../../../../core/racing/application/use-cases/ApplyForSponsorshipUseCase'; +import { GetPendingSponsorshipRequestsUseCase } from '../../../../core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; +import { AcceptSponsorshipRequestUseCase } from '../../../../core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; +import { RejectSponsorshipRequestUseCase } from '../../../../core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; + +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { SponsorTestContext } from '../../sponsor/SponsorTestContext'; +import { InMemorySponsorshipRequestRepository } from '../../../../adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository'; +import { InMemoryLeagueWalletRepository } from '../../../../adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository'; +import { InMemoryWalletRepository } from '../../../../adapters/payments/persistence/inmemory/InMemoryWalletRepository'; + +const createNoopNotificationService = (): NotificationService => + ({ + sendNotification: vi.fn(async () => undefined), + }) as unknown as NotificationService; + +const createNoopLogger = (): Logger => + ({ + info: () => {}, + debug: () => {}, + warn: () => {}, + error: () => {}, + }) as unknown as Logger; + +describe('League Sponsorships - Sponsorship Applications', () => { + let leagues: LeaguesTestContext; + let sponsors: SponsorTestContext; + + let sponsorshipRequestRepo: InMemorySponsorshipRequestRepository; + let sponsorWalletRepo: WalletRepository; + let leagueWalletRepo: InMemoryLeagueWalletRepository; + + beforeEach(() => { + leagues = new LeaguesTestContext(); + leagues.clear(); + + sponsors = new SponsorTestContext(); + sponsors.clear(); + + sponsorshipRequestRepo = new InMemorySponsorshipRequestRepository(createNoopLogger()); + sponsorWalletRepo = new InMemoryWalletRepository(createNoopLogger()); + leagueWalletRepo = new InMemoryLeagueWalletRepository(createNoopLogger()); + }); + + const seedLeagueAndSeason = async (params: { leagueId: string; seasonId: string }) => { + const league = League.create({ + id: params.leagueId, + name: 'League 1', + description: 'League used for sponsorship integration tests', + ownerId: 'owner-1', + }); + await leagues.racingLeagueRepository.create(league); + + const season = Season.create({ + id: params.seasonId, + leagueId: params.leagueId, + gameId: 'iracing', + name: 'Season 1', + status: 'active', + startDate: new Date('2025-01-01T00:00:00.000Z'), + endDate: new Date('2025-02-01T00:00:00.000Z'), + }); + await leagues.seasonRepository.create(season); + + return { league, season }; + }; + + it('allows a sponsor to apply for a season sponsorship and lists it as pending', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + const sponsorId = 'sponsor-1'; + + await seedLeagueAndSeason({ leagueId, seasonId }); + + const sponsor = Sponsor.create({ + id: sponsorId, + name: 'Acme', + contactEmail: 'acme@example.com', + }); + await sponsors.sponsorRepository.create(sponsor); + + const pricing = SponsorshipPricing.create({ + acceptingApplications: true, + }) + .updateMainSlot({ + available: true, + maxSlots: 1, + price: Money.create(1000, 'USD'), + benefits: ['logo'], + }) + .updateSecondarySlot({ + available: true, + maxSlots: 2, + price: Money.create(500, 'USD'), + benefits: ['mention'], + }); + + await sponsors.sponsorshipPricingRepository.save('season', seasonId, pricing); + + const applyUseCase = new ApplyForSponsorshipUseCase( + sponsorshipRequestRepo, + sponsors.sponsorshipPricingRepository, + sponsors.sponsorRepository, + sponsors.logger, + ); + + const apply = await applyUseCase.execute({ + sponsorId, + entityType: 'season', + entityId: seasonId, + tier: 'main', + offeredAmount: 1000, + currency: 'USD', + message: 'We would like to sponsor', + }); + + expect(apply.isOk()).toBe(true); + + const getPending = new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepo, sponsors.sponsorRepository); + const pending = await getPending.execute({ entityType: 'season', entityId: seasonId }); + + expect(pending.isOk()).toBe(true); + const value = pending.unwrap(); + expect(value.totalCount).toBe(1); + expect(value.requests[0]!.request.status).toBe('pending'); + expect(value.requests[0]!.sponsor?.id.toString()).toBe(sponsorId); + }); + + it('accepts a pending season sponsorship request, creates a sponsorship, and updates wallets', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + const sponsorId = 'sponsor-1'; + + await seedLeagueAndSeason({ leagueId, seasonId }); + + const sponsor = Sponsor.create({ + id: sponsorId, + name: 'Acme', + contactEmail: 'acme@example.com', + }); + await sponsors.sponsorRepository.create(sponsor); + + const request = SponsorshipRequest.create({ + id: 'req-1', + sponsorId, + entityType: 'season', + entityId: seasonId, + tier: 'main', + offeredAmount: Money.create(1000, 'USD'), + message: 'Please accept', + }); + await sponsorshipRequestRepo.create(request); + + await sponsorWalletRepo.create({ + id: sponsorId, + leagueId: 'n/a', + balance: 1500, + totalRevenue: 0, + totalPlatformFees: 0, + totalWithdrawn: 0, + currency: 'USD', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const leagueWallet = LeagueWallet.create({ + id: leagueId, + leagueId, + balance: Money.create(0, 'USD'), + }); + await leagueWalletRepo.create(leagueWallet); + + const notificationService = createNoopNotificationService(); + + const acceptUseCase = new AcceptSponsorshipRequestUseCase( + sponsorshipRequestRepo, + leagues.seasonSponsorshipRepository, + leagues.seasonRepository, + notificationService, + async () => ({ success: true, transactionId: 'tx-1' }), + sponsorWalletRepo, + leagueWalletRepo, + createNoopLogger(), + ); + + const result = await acceptUseCase.execute({ requestId: 'req-1', respondedBy: 'owner-1' }); + + expect(result.isOk()).toBe(true); + + const updatedSponsorWallet = await sponsorWalletRepo.findById(sponsorId); + expect(updatedSponsorWallet?.balance).toBe(500); + + const updatedLeagueWallet = await leagueWalletRepo.findById(leagueId); + expect(updatedLeagueWallet?.balance.amount).toBe(900); + + expect((notificationService.sendNotification as unknown as ReturnType)).toHaveBeenCalledTimes(1); + + const sponsorships = await leagues.seasonSponsorshipRepository.findBySeasonId(seasonId); + expect(sponsorships).toHaveLength(1); + expect(sponsorships[0]!.status).toBe('active'); + }); + + it('rejects a pending sponsorship request', async () => { + const sponsorId = 'sponsor-1'; + + const request = SponsorshipRequest.create({ + id: 'req-1', + sponsorId, + entityType: 'season', + entityId: 'season-1', + tier: 'main', + offeredAmount: Money.create(1000, 'USD'), + }); + await sponsorshipRequestRepo.create(request); + + const rejectUseCase = new RejectSponsorshipRequestUseCase(sponsorshipRequestRepo, createNoopLogger()); + + const result = await rejectUseCase.execute({ requestId: 'req-1', respondedBy: 'owner-1', reason: 'Not a fit' }); + + expect(result.isOk()).toBe(true); + + const updated = await sponsorshipRequestRepo.findById('req-1'); + expect(updated?.status).toBe('rejected'); + }); +}); diff --git a/tests/integration/leagues/sponsorships/SponsorshipManagement.test.ts b/tests/integration/leagues/sponsorships/SponsorshipManagement.test.ts new file mode 100644 index 000000000..55fff83ed --- /dev/null +++ b/tests/integration/leagues/sponsorships/SponsorshipManagement.test.ts @@ -0,0 +1,116 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { League } from '../../../../core/racing/domain/entities/League'; +import { Season } from '../../../../core/racing/domain/entities/season/Season'; +import { SeasonSponsorship } from '../../../../core/racing/domain/entities/season/SeasonSponsorship'; +import { Money } from '../../../../core/racing/domain/value-objects/Money'; + +describe('League Sponsorships - Sponsorship Management', () => { + let context: LeaguesTestContext; + + beforeEach(() => { + context = new LeaguesTestContext(); + context.clear(); + }); + + const seedLeagueAndSeason = async (params: { leagueId: string; seasonId: string }) => { + const league = League.create({ + id: params.leagueId, + name: 'League 1', + description: 'League used for sponsorship integration tests', + ownerId: 'owner-1', + }); + await context.racingLeagueRepository.create(league); + + const season = Season.create({ + id: params.seasonId, + leagueId: params.leagueId, + gameId: 'iracing', + name: 'Season 1', + status: 'active', + startDate: new Date('2025-01-01T00:00:00.000Z'), + endDate: new Date('2025-02-01T00:00:00.000Z'), + }); + await context.seasonRepository.create(season); + + return { league, season }; + }; + + it('adds a season sponsorship to the repository', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + + await seedLeagueAndSeason({ leagueId, seasonId }); + + const sponsorship = SeasonSponsorship.create({ + id: 'sponsorship-1', + seasonId, + leagueId, + sponsorId: 'sponsor-1', + tier: 'main', + pricing: Money.create(1000, 'USD'), + status: 'active', + }); + + await context.seasonSponsorshipRepository.create(sponsorship); + + const found = await context.seasonSponsorshipRepository.findById('sponsorship-1'); + expect(found).not.toBeNull(); + expect(found?.id).toBe('sponsorship-1'); + expect(found?.seasonId).toBe(seasonId); + expect(found?.leagueId).toBe(leagueId); + }); + + it('edits sponsorship pricing via repository update', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + + await seedLeagueAndSeason({ leagueId, seasonId }); + + const sponsorship = SeasonSponsorship.create({ + id: 'sponsorship-1', + seasonId, + leagueId, + sponsorId: 'sponsor-1', + tier: 'main', + pricing: Money.create(1000, 'USD'), + status: 'active', + }); + + await context.seasonSponsorshipRepository.create(sponsorship); + + const updated = sponsorship.withPricing(Money.create(1500, 'USD')); + await context.seasonSponsorshipRepository.update(updated); + + const found = await context.seasonSponsorshipRepository.findById('sponsorship-1'); + expect(found).not.toBeNull(); + expect(found?.pricing.amount).toBe(1500); + expect(found?.pricing.currency).toBe('USD'); + }); + + it('deletes a sponsorship from the repository', async () => { + const leagueId = 'league-1'; + const seasonId = 'season-1'; + + await seedLeagueAndSeason({ leagueId, seasonId }); + + const sponsorship = SeasonSponsorship.create({ + id: 'sponsorship-1', + seasonId, + leagueId, + sponsorId: 'sponsor-1', + tier: 'main', + pricing: Money.create(1000, 'USD'), + status: 'active', + }); + + await context.seasonSponsorshipRepository.create(sponsorship); + expect(await context.seasonSponsorshipRepository.exists('sponsorship-1')).toBe(true); + + await context.seasonSponsorshipRepository.delete('sponsorship-1'); + + expect(await context.seasonSponsorshipRepository.exists('sponsorship-1')).toBe(false); + const found = await context.seasonSponsorshipRepository.findById('sponsorship-1'); + expect(found).toBeNull(); + }); +}); diff --git a/tests/integration/leagues/standings/GetLeagueStandings.test.ts b/tests/integration/leagues/standings/GetLeagueStandings.test.ts new file mode 100644 index 000000000..65a3e180b --- /dev/null +++ b/tests/integration/leagues/standings/GetLeagueStandings.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { Driver } from '../../../../core/racing/domain/entities/Driver'; +import { Standing } from '../../../../core/racing/domain/entities/Standing'; + +describe('GetLeagueStandings', () => { + let context: LeaguesTestContext; + + beforeEach(() => { + context = new LeaguesTestContext(); + }); + + describe('Success Path', () => { + it('should retrieve championship standings with all driver statistics', async () => { + // Given: A league exists with multiple drivers + const leagueId = 'league-123'; + const driver1Id = 'driver-1'; + const driver2Id = 'driver-2'; + + await context.racingDriverRepository.create(Driver.create({ + id: driver1Id, + name: 'Driver One', + iracingId: 'ir-1', + country: 'US', + })); + + await context.racingDriverRepository.create(Driver.create({ + id: driver2Id, + name: 'Driver Two', + iracingId: 'ir-2', + country: 'DE', + })); + + // And: Each driver has points + await context.standingRepository.save(Standing.create({ + leagueId, + driverId: driver1Id, + points: 100, + position: 1, + })); + + await context.standingRepository.save(Standing.create({ + leagueId, + driverId: driver2Id, + points: 80, + position: 2, + })); + + // When: GetLeagueStandingsUseCase.execute() is called with league ID + const result = await context.getLeagueStandingsUseCase.execute({ leagueId }); + + // Then: The result should contain all drivers ranked by points + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.standings).toHaveLength(2); + expect(data.standings[0].driverId).toBe(driver1Id); + expect(data.standings[0].points).toBe(100); + expect(data.standings[0].rank).toBe(1); + expect(data.standings[1].driverId).toBe(driver2Id); + expect(data.standings[1].points).toBe(80); + expect(data.standings[1].rank).toBe(2); + }); + + it('should retrieve standings with minimal driver statistics', async () => { + const leagueId = 'league-123'; + const driverId = 'driver-1'; + + await context.racingDriverRepository.create(Driver.create({ + id: driverId, + name: 'Driver One', + iracingId: 'ir-1', + country: 'US', + })); + + await context.standingRepository.save(Standing.create({ + leagueId, + driverId, + points: 10, + position: 1, + })); + + const result = await context.getLeagueStandingsUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + expect(result.unwrap().standings).toHaveLength(1); + }); + }); + + describe('Edge Cases', () => { + it('should handle drivers with no championship standings', async () => { + const leagueId = 'league-empty'; + const result = await context.getLeagueStandingsUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + expect(result.unwrap().standings).toHaveLength(0); + }); + }); + + describe('Error Handling', () => { + it('should handle repository errors gracefully', async () => { + // Mock repository error + context.standingRepository.findByLeagueId = async () => { + throw new Error('Database error'); + }; + + const result = await context.getLeagueStandingsUseCase.execute({ leagueId: 'any' }); + + expect(result.isErr()).toBe(true); + // The Result class in this project seems to use .error for the error value + expect((result as any).error.code).toBe('REPOSITORY_ERROR'); + }); + }); +}); diff --git a/tests/integration/leagues/standings/StandingsCalculation.test.ts b/tests/integration/leagues/standings/StandingsCalculation.test.ts new file mode 100644 index 000000000..1ffca5cc4 --- /dev/null +++ b/tests/integration/leagues/standings/StandingsCalculation.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { Driver } from '../../../../core/racing/domain/entities/Driver'; +import { Standing } from '../../../../core/racing/domain/entities/Standing'; +import { Result } from '../../../../core/racing/domain/entities/result/Result'; +import { Race } from '../../../../core/racing/domain/entities/Race'; +import { League } from '../../../../core/racing/domain/entities/League'; + +describe('StandingsCalculation', () => { + let context: LeaguesTestContext; + + beforeEach(() => { + context = new LeaguesTestContext(); + }); + + it('should correctly calculate driver statistics from race results', async () => { + // Given: A league exists + const leagueId = 'league-123'; + const driverId = 'driver-1'; + + await context.racingLeagueRepository.create(League.create({ + id: leagueId, + name: 'Test League', + description: 'Test Description', + ownerId: 'owner-1', + })); + + await context.racingDriverRepository.create(Driver.create({ + id: driverId, + name: 'Driver One', + iracingId: 'ir-1', + country: 'US', + })); + + // And: A driver has completed races + const race1Id = 'race-1'; + const race2Id = 'race-2'; + + await context.raceRepository.create(Race.create({ + id: race1Id, + leagueId, + scheduledAt: new Date(), + track: 'Daytona', + car: 'GT3', + status: 'completed', + })); + + await context.raceRepository.create(Race.create({ + id: race2Id, + leagueId, + scheduledAt: new Date(), + track: 'Sebring', + car: 'GT3', + status: 'completed', + })); + + // And: The driver has results (1 win, 1 podium) + await context.resultRepository.create(Result.create({ + id: 'res-1', + raceId: race1Id, + driverId, + position: 1, + fastestLap: 120000, + incidents: 0, + startPosition: 1, + })); + + await context.resultRepository.create(Result.create({ + id: 'res-2', + raceId: race2Id, + driverId, + position: 3, + fastestLap: 121000, + incidents: 2, + startPosition: 5, + })); + + // When: Standings are recalculated + await context.standingRepository.recalculate(leagueId); + + // Then: Driver statistics should show correct values + const standings = await context.standingRepository.findByLeagueId(leagueId); + const driverStanding = standings.find(s => s.driverId.toString() === driverId); + + expect(driverStanding).toBeDefined(); + expect(driverStanding?.wins).toBe(1); + expect(driverStanding?.racesCompleted).toBe(2); + // Points depend on the points system (default f1-2024: 1st=25, 3rd=15) + expect(driverStanding?.points.toNumber()).toBe(40); + }); +}); diff --git a/tests/integration/leagues/stewarding/GetLeagueStewarding.test.ts b/tests/integration/leagues/stewarding/GetLeagueStewarding.test.ts new file mode 100644 index 000000000..9770831b1 --- /dev/null +++ b/tests/integration/leagues/stewarding/GetLeagueStewarding.test.ts @@ -0,0 +1,414 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { League as RacingLeague } from '../../../../core/racing/domain/entities/League'; +import { Race } from '../../../../core/racing/domain/entities/Race'; +import { Driver } from '../../../../core/racing/domain/entities/Driver'; +import { Protest } from '../../../../core/racing/domain/entities/Protest'; +import { Penalty } from '../../../../core/racing/domain/entities/penalty/Penalty'; +import { GetLeagueProtestsUseCase } from '../../../../core/racing/application/use-cases/GetLeagueProtestsUseCase'; +import { GetRacePenaltiesUseCase } from '../../../../core/racing/application/use-cases/GetRacePenaltiesUseCase'; + +describe('League Stewarding - GetLeagueStewarding', () => { + let context: LeaguesTestContext; + let getLeagueProtestsUseCase: GetLeagueProtestsUseCase; + let getRacePenaltiesUseCase: GetRacePenaltiesUseCase; + + beforeEach(() => { + context = new LeaguesTestContext(); + context.clear(); + + getLeagueProtestsUseCase = new GetLeagueProtestsUseCase( + context.raceRepository, + context.protestRepository, + context.racingDriverRepository, + context.racingLeagueRepository, + ); + + getRacePenaltiesUseCase = new GetRacePenaltiesUseCase( + context.penaltyRepository, + context.racingDriverRepository, + ); + }); + + const seedRacingLeague = async (params: { leagueId: string }) => { + const league = RacingLeague.create({ + id: params.leagueId, + name: 'Racing League', + description: 'League used for stewarding integration tests', + ownerId: 'driver-123', + }); + + await context.racingLeagueRepository.create(league); + return league; + }; + + const seedRace = async (params: { raceId: string; leagueId: string }) => { + const race = Race.create({ + id: params.raceId, + leagueId: params.leagueId, + track: 'Track 1', + car: 'GT3', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + status: 'completed', + }); + + await context.raceRepository.create(race); + return race; + }; + + const seedDriver = async (params: { driverId: string; iracingId?: string }) => { + const driver = Driver.create({ + id: params.driverId, + name: 'Driver Name', + iracingId: params.iracingId || `ir-${params.driverId}`, + country: 'US', + }); + + await context.racingDriverRepository.create(driver); + return driver; + }; + + const seedProtest = async (params: { + protestId: string; + raceId: string; + protestingDriverId: string; + accusedDriverId: string; + status?: string; + }) => { + const protest = Protest.create({ + id: params.protestId, + raceId: params.raceId, + protestingDriverId: params.protestingDriverId, + accusedDriverId: params.accusedDriverId, + incident: { + lap: 5, + description: 'Contact on corner entry', + }, + status: params.status || 'pending', + }); + + await context.protestRepository.create(protest); + return protest; + }; + + const seedPenalty = async (params: { + penaltyId: string; + leagueId: string; + driverId: string; + raceId?: string; + status?: string; + }) => { + const penalty = Penalty.create({ + id: params.penaltyId, + leagueId: params.leagueId, + driverId: params.driverId, + type: 'time_penalty', + value: 5, + reason: 'Contact on corner entry', + issuedBy: 'steward-1', + status: params.status || 'pending', + ...(params.raceId && { raceId: params.raceId }), + }); + + await context.penaltyRepository.create(penalty); + return penalty; + }; + + describe('Success Path', () => { + it('should retrieve league protests with driver and race details', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'pending', + }); + + const result = await getLeagueProtestsUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.league.id.toString()).toBe(leagueId); + expect(data.protests).toHaveLength(1); + expect(data.protests[0].protest.id.toString()).toBe('protest-1'); + expect(data.protests[0].protest.status.toString()).toBe('pending'); + expect(data.protests[0].race?.id.toString()).toBe(raceId); + expect(data.protests[0].protestingDriver?.id.toString()).toBe(protestingDriverId); + expect(data.protests[0].accusedDriver?.id.toString()).toBe(accusedDriverId); + }); + + it('should retrieve penalties with driver details', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const driverId = 'driver-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId }); + await seedPenalty({ + penaltyId: 'penalty-1', + leagueId, + driverId, + raceId, + status: 'pending', + }); + + const result = await getRacePenaltiesUseCase.execute({ raceId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.penalties).toHaveLength(1); + expect(data.penalties[0].id.toString()).toBe('penalty-1'); + expect(data.penalties[0].status.toString()).toBe('pending'); + expect(data.drivers).toHaveLength(1); + expect(data.drivers[0].id.toString()).toBe(driverId); + }); + + it('should retrieve multiple protests for a league', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'pending', + }); + await seedProtest({ + protestId: 'protest-2', + raceId, + protestingDriverId: accusedDriverId, + accusedDriverId: protestingDriverId, + status: 'under_review', + }); + + const result = await getLeagueProtestsUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protests).toHaveLength(2); + expect(data.protests.map(p => p.protest.id.toString()).sort()).toEqual(['protest-1', 'protest-2']); + }); + + it('should retrieve multiple penalties for a race', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const driverId1 = 'driver-1'; + const driverId2 = 'driver-2'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: driverId1 }); + await seedDriver({ driverId: driverId2 }); + await seedPenalty({ + penaltyId: 'penalty-1', + leagueId, + driverId: driverId1, + raceId, + status: 'pending', + }); + await seedPenalty({ + penaltyId: 'penalty-2', + leagueId, + driverId: driverId2, + raceId, + status: 'applied', + }); + + const result = await getRacePenaltiesUseCase.execute({ raceId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.penalties).toHaveLength(2); + expect(data.penalties.map(p => p.id.toString()).sort()).toEqual(['penalty-1', 'penalty-2']); + expect(data.drivers).toHaveLength(2); + }); + + it('should retrieve resolved protests', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'upheld', + }); + await seedProtest({ + protestId: 'protest-2', + raceId, + protestingDriverId, + accusedDriverId, + status: 'dismissed', + }); + + const result = await getLeagueProtestsUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protests).toHaveLength(2); + expect(data.protests.filter(p => p.protest.status.toString() === 'upheld')).toHaveLength(1); + expect(data.protests.filter(p => p.protest.status.toString() === 'dismissed')).toHaveLength(1); + }); + + it('should retrieve applied penalties', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const driverId = 'driver-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId }); + await seedPenalty({ + penaltyId: 'penalty-1', + leagueId, + driverId, + raceId, + status: 'applied', + }); + + const result = await getRacePenaltiesUseCase.execute({ raceId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.penalties).toHaveLength(1); + expect(data.penalties[0].status.toString()).toBe('applied'); + }); + }); + + describe('Edge Cases', () => { + it('should handle league with no protests', async () => { + const leagueId = 'league-empty'; + await seedRacingLeague({ leagueId }); + + const result = await getLeagueProtestsUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protests).toHaveLength(0); + }); + + it('should handle race with no penalties', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + + const result = await getRacePenaltiesUseCase.execute({ raceId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.penalties).toHaveLength(0); + expect(data.drivers).toHaveLength(0); + }); + + it('should handle league with no races', async () => { + const leagueId = 'league-empty'; + await seedRacingLeague({ leagueId }); + + const result = await getLeagueProtestsUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protests).toHaveLength(0); + }); + + it('should handle protest with missing driver details', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + // Don't seed drivers + await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'pending', + }); + + const result = await getLeagueProtestsUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protests).toHaveLength(1); + expect(data.protests[0].protestingDriver).toBeNull(); + expect(data.protests[0].accusedDriver).toBeNull(); + }); + + it('should handle penalty with missing driver details', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const driverId = 'driver-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + // Don't seed driver + await seedPenalty({ + penaltyId: 'penalty-1', + leagueId, + driverId, + raceId, + status: 'pending', + }); + + const result = await getRacePenaltiesUseCase.execute({ raceId }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.penalties).toHaveLength(1); + expect(data.drivers).toHaveLength(0); + }); + }); + + describe('Error Handling', () => { + it('should return LEAGUE_NOT_FOUND when league does not exist', async () => { + const result = await getLeagueProtestsUseCase.execute({ leagueId: 'missing-league' }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('LEAGUE_NOT_FOUND'); + }); + + it('should handle repository errors gracefully', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + // Mock repository error + context.raceRepository.findByLeagueId = async () => { + throw new Error('Database error'); + }; + + const result = await getLeagueProtestsUseCase.execute({ leagueId }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR'); + }); + }); +}); diff --git a/tests/integration/leagues/stewarding/StewardingManagement.test.ts b/tests/integration/leagues/stewarding/StewardingManagement.test.ts new file mode 100644 index 000000000..8d66b2977 --- /dev/null +++ b/tests/integration/leagues/stewarding/StewardingManagement.test.ts @@ -0,0 +1,767 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { League as RacingLeague } from '../../../../core/racing/domain/entities/League'; +import { Race } from '../../../../core/racing/domain/entities/Race'; +import { Driver } from '../../../../core/racing/domain/entities/Driver'; +import { Protest } from '../../../../core/racing/domain/entities/Protest'; +import { Penalty } from '../../../../core/racing/domain/entities/penalty/Penalty'; +import { LeagueMembership } from '../../../../core/racing/domain/entities/LeagueMembership'; +import { ReviewProtestUseCase } from '../../../../core/racing/application/use-cases/ReviewProtestUseCase'; +import { ApplyPenaltyUseCase } from '../../../../core/racing/application/use-cases/ApplyPenaltyUseCase'; +import { QuickPenaltyUseCase } from '../../../../core/racing/application/use-cases/QuickPenaltyUseCase'; +import { FileProtestUseCase } from '../../../../core/racing/application/use-cases/FileProtestUseCase'; +import { RequestProtestDefenseUseCase } from '../../../../core/racing/application/use-cases/RequestProtestDefenseUseCase'; +import { SubmitProtestDefenseUseCase } from '../../../../core/racing/application/use-cases/SubmitProtestDefenseUseCase'; + +describe('League Stewarding - StewardingManagement', () => { + let context: LeaguesTestContext; + let reviewProtestUseCase: ReviewProtestUseCase; + let applyPenaltyUseCase: ApplyPenaltyUseCase; + let quickPenaltyUseCase: QuickPenaltyUseCase; + let fileProtestUseCase: FileProtestUseCase; + let requestProtestDefenseUseCase: RequestProtestDefenseUseCase; + let submitProtestDefenseUseCase: SubmitProtestDefenseUseCase; + + beforeEach(() => { + context = new LeaguesTestContext(); + context.clear(); + + reviewProtestUseCase = new ReviewProtestUseCase( + context.protestRepository, + context.raceRepository, + context.leagueMembershipRepository, + context.logger, + ); + + applyPenaltyUseCase = new ApplyPenaltyUseCase( + context.penaltyRepository, + context.protestRepository, + context.raceRepository, + context.leagueMembershipRepository, + context.logger, + ); + + quickPenaltyUseCase = new QuickPenaltyUseCase( + context.penaltyRepository, + context.raceRepository, + context.leagueMembershipRepository, + context.logger, + ); + + fileProtestUseCase = new FileProtestUseCase( + context.protestRepository, + context.raceRepository, + context.leagueMembershipRepository, + context.racingDriverRepository, + ); + + requestProtestDefenseUseCase = new RequestProtestDefenseUseCase( + context.protestRepository, + context.raceRepository, + context.leagueMembershipRepository, + context.logger, + ); + + submitProtestDefenseUseCase = new SubmitProtestDefenseUseCase( + context.racingLeagueRepository, + context.protestRepository, + context.logger, + ); + }); + + const seedRacingLeague = async (params: { leagueId: string }) => { + const league = RacingLeague.create({ + id: params.leagueId, + name: 'Racing League', + description: 'League used for stewarding integration tests', + ownerId: 'driver-123', + }); + + await context.racingLeagueRepository.create(league); + return league; + }; + + const seedRace = async (params: { raceId: string; leagueId: string }) => { + const race = Race.create({ + id: params.raceId, + leagueId: params.leagueId, + track: 'Track 1', + car: 'GT3', + scheduledAt: new Date('2025-01-10T20:00:00Z'), + status: 'completed', + }); + + await context.raceRepository.create(race); + return race; + }; + + const seedDriver = async (params: { driverId: string; iracingId?: string }) => { + const driver = Driver.create({ + id: params.driverId, + name: 'Driver Name', + iracingId: params.iracingId || `ir-${params.driverId}`, + country: 'US', + }); + + await context.racingDriverRepository.create(driver); + return driver; + }; + + const seedProtest = async (params: { + protestId: string; + raceId: string; + protestingDriverId: string; + accusedDriverId: string; + status?: string; + }) => { + const protest = Protest.create({ + id: params.protestId, + raceId: params.raceId, + protestingDriverId: params.protestingDriverId, + accusedDriverId: params.accusedDriverId, + incident: { + lap: 5, + description: 'Contact on corner entry', + }, + status: params.status || 'pending', + }); + + await context.protestRepository.create(protest); + return protest; + }; + + const seedPenalty = async (params: { + penaltyId: string; + leagueId: string; + driverId: string; + raceId?: string; + status?: string; + }) => { + const penalty = Penalty.create({ + id: params.penaltyId, + leagueId: params.leagueId, + driverId: params.driverId, + type: 'time_penalty', + value: 5, + reason: 'Contact on corner entry', + issuedBy: 'steward-1', + status: params.status || 'pending', + ...(params.raceId && { raceId: params.raceId }), + }); + + await context.penaltyRepository.create(penalty); + return penalty; + }; + + describe('Review Protest', () => { + it('should review a pending protest and mark it as under review', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + const stewardId = 'steward-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + await seedDriver({ driverId: stewardId }); + + // Add steward as admin + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: stewardId, + role: 'admin', + status: 'active', + }) + ); + + const protest = await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'pending', + }); + + const result = await reviewProtestUseCase.execute({ + protestId: 'protest-1', + stewardId, + decision: 'uphold', + decisionNotes: 'Contact was avoidable', + }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protestId).toBe('protest-1'); + expect(data.leagueId).toBe(leagueId); + + const updatedProtest = await context.protestRepository.findById('protest-1'); + expect(updatedProtest?.status.toString()).toBe('upheld'); + expect(updatedProtest?.reviewedBy).toBe('steward-1'); + }); + + it('should uphold a protest and create a penalty', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + const stewardId = 'steward-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + await seedDriver({ driverId: stewardId }); + + // Add steward as admin + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: stewardId, + role: 'admin', + status: 'active', + }) + ); + + const protest = await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'under_review', + }); + + const result = await reviewProtestUseCase.execute({ + protestId: protest.id.toString(), + stewardId, + decision: 'uphold', + decisionNotes: 'Contact was avoidable', + }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protestId).toBe('protest-1'); + expect(data.leagueId).toBe(leagueId); + + const updatedProtest = await context.protestRepository.findById('protest-1'); + expect(updatedProtest?.status.toString()).toBe('upheld'); + expect(updatedProtest?.reviewedBy).toBe('steward-1'); + expect(updatedProtest?.decisionNotes).toBe('Contact was avoidable'); + }); + + it('should dismiss a protest', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + const stewardId = 'steward-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + await seedDriver({ driverId: stewardId }); + + // Add steward as admin + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: stewardId, + role: 'admin', + status: 'active', + }) + ); + + const protest = await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'under_review', + }); + + const result = await reviewProtestUseCase.execute({ + protestId: protest.id.toString(), + stewardId, + decision: 'dismiss', + decisionNotes: 'No contact occurred', + }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protestId).toBe('protest-1'); + expect(data.leagueId).toBe(leagueId); + + const updatedProtest = await context.protestRepository.findById('protest-1'); + expect(updatedProtest?.status.toString()).toBe('dismissed'); + expect(updatedProtest?.reviewedBy).toBe('steward-1'); + expect(updatedProtest?.decisionNotes).toBe('No contact occurred'); + }); + + it('should return PROTEST_NOT_FOUND when protest does not exist', async () => { + const result = await reviewProtestUseCase.execute({ + protestId: 'missing-protest', + stewardId: 'steward-1', + decision: 'uphold', + decisionNotes: 'Notes', + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('PROTEST_NOT_FOUND'); + }); + + it('should return RACE_NOT_FOUND when race does not exist', async () => { + const protest = Protest.create({ + id: 'protest-1', + raceId: 'missing-race', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact on corner entry', + }, + status: 'pending', + }); + + await context.protestRepository.create(protest); + + const result = await reviewProtestUseCase.execute({ + protestId: 'protest-1', + stewardId: 'steward-1', + decision: 'uphold', + decisionNotes: 'Notes', + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND'); + }); + }); + + describe('Apply Penalty', () => { + it('should apply a penalty to a driver', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const driverId = 'driver-1'; + const stewardId = 'steward-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId }); + await seedDriver({ driverId: stewardId }); + + // Add steward as admin + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: stewardId, + role: 'admin', + status: 'active', + }) + ); + + const result = await applyPenaltyUseCase.execute({ + raceId, + driverId, + type: 'time_penalty', + value: 5, + reason: 'Contact on corner entry', + stewardId, + }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.penaltyId).toBeDefined(); + + const penalty = await context.penaltyRepository.findById(data.penaltyId); + expect(penalty).not.toBeNull(); + expect(penalty?.type.toString()).toBe('time_penalty'); + expect(penalty?.value).toBe(5); + expect(penalty?.reason.toString()).toBe('Contact on corner entry'); + expect(penalty?.issuedBy).toBe('steward-1'); + expect(penalty?.status.toString()).toBe('pending'); + }); + + it('should apply a penalty linked to a protest', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + const stewardId = 'steward-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + await seedDriver({ driverId: stewardId }); + + // Add steward as admin + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: stewardId, + role: 'admin', + status: 'active', + }) + ); + + const protest = await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'upheld', + }); + + const result = await applyPenaltyUseCase.execute({ + raceId, + driverId: accusedDriverId, + type: 'time_penalty', + value: 10, + reason: 'Contact on corner entry', + stewardId, + protestId: protest.id.toString(), + }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.penaltyId).toBeDefined(); + + const penalty = await context.penaltyRepository.findById(data.penaltyId); + expect(penalty).not.toBeNull(); + expect(penalty?.protestId?.toString()).toBe('protest-1'); + }); + + it('should return RACE_NOT_FOUND when race does not exist', async () => { + const result = await applyPenaltyUseCase.execute({ + raceId: 'missing-race', + driverId: 'driver-1', + type: 'time_penalty', + value: 5, + reason: 'Contact on corner entry', + stewardId: 'steward-1', + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND'); + }); + + it('should return INSUFFICIENT_AUTHORITY when steward is not admin', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const driverId = 'driver-1'; + const stewardId = 'steward-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId }); + await seedDriver({ driverId: stewardId }); + + const result = await applyPenaltyUseCase.execute({ + raceId, + driverId, + type: 'time_penalty', + value: 5, + reason: 'Contact on corner entry', + stewardId, + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('INSUFFICIENT_AUTHORITY'); + }); + }); + + describe('Quick Penalty', () => { + it('should create a quick penalty without a protest', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const driverId = 'driver-1'; + const adminId = 'steward-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId }); + await seedDriver({ driverId: adminId }); + + // Add admin as admin + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: adminId, + role: 'admin', + status: 'active', + }) + ); + + const result = await quickPenaltyUseCase.execute({ + raceId, + driverId, + adminId, + infractionType: 'unsafe_rejoin', + severity: 'minor', + notes: 'Speeding in pit lane', + }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.penaltyId).toBeDefined(); + expect(data.raceId).toBe(raceId); + expect(data.driverId).toBe(driverId); + + const penalty = await context.penaltyRepository.findById(data.penaltyId); + expect(penalty).not.toBeNull(); + expect(penalty?.raceId?.toString()).toBe(raceId); + expect(penalty?.status.toString()).toBe('applied'); + }); + + it('should return RACE_NOT_FOUND when race does not exist', async () => { + const result = await quickPenaltyUseCase.execute({ + raceId: 'missing-race', + driverId: 'driver-1', + adminId: 'steward-1', + infractionType: 'track_limits', + severity: 'minor', + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND'); + }); + }); + + describe('File Protest', () => { + it('should file a new protest', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + + // Add drivers as members + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: protestingDriverId, + role: 'driver', + status: 'active', + }) + ); + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: accusedDriverId, + role: 'driver', + status: 'active', + }) + ); + + const result = await fileProtestUseCase.execute({ + raceId, + protestingDriverId, + accusedDriverId, + incident: { + lap: 5, + description: 'Contact on corner entry', + }, + comment: 'This was a dangerous move', + proofVideoUrl: 'https://example.com/video.mp4', + }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protest.id).toBeDefined(); + expect(data.protest.raceId).toBe(raceId); + + const protest = await context.protestRepository.findById(data.protest.id); + expect(protest).not.toBeNull(); + expect(protest?.raceId.toString()).toBe(raceId); + expect(protest?.protestingDriverId.toString()).toBe(protestingDriverId); + expect(protest?.accusedDriverId.toString()).toBe(accusedDriverId); + expect(protest?.status.toString()).toBe('pending'); + expect(protest?.comment).toBe('This was a dangerous move'); + expect(protest?.proofVideoUrl).toBe('https://example.com/video.mp4'); + }); + + it('should return RACE_NOT_FOUND when race does not exist', async () => { + const result = await fileProtestUseCase.execute({ + raceId: 'missing-race', + protestingDriverId: 'driver-1', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact on corner entry', + }, + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND'); + }); + + it('should return DRIVER_NOT_FOUND when protesting driver does not exist', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + + const result = await fileProtestUseCase.execute({ + raceId, + protestingDriverId: 'missing-driver', + accusedDriverId: 'driver-2', + incident: { + lap: 5, + description: 'Contact on corner entry', + }, + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('NOT_MEMBER'); + }); + + it('should return DRIVER_NOT_FOUND when accused driver does not exist', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + + // Add protesting driver as member + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: protestingDriverId, + role: 'driver', + status: 'active', + }) + ); + + const result = await fileProtestUseCase.execute({ + raceId, + protestingDriverId, + accusedDriverId: 'missing-driver', + incident: { + lap: 5, + description: 'Contact on corner entry', + }, + }); + + expect(result.isOk()).toBe(true); + }); + }); + + describe('Request Protest Defense', () => { + it('should request defense for a pending protest', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + const stewardId = 'steward-1'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + await seedDriver({ driverId: stewardId }); + + // Add steward as admin + await context.leagueMembershipRepository.saveMembership( + LeagueMembership.create({ + leagueId, + driverId: stewardId, + role: 'admin', + status: 'active', + }) + ); + + const protest = await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'pending', + }); + + const result = await requestProtestDefenseUseCase.execute({ + protestId: protest.id.toString(), + stewardId, + }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protestId).toBe('protest-1'); + + const updatedProtest = await context.protestRepository.findById('protest-1'); + expect(updatedProtest?.status.toString()).toBe('awaiting_defense'); + expect(updatedProtest?.defenseRequestedBy).toBe('steward-1'); + }); + + it('should return PROTEST_NOT_FOUND when protest does not exist', async () => { + const result = await requestProtestDefenseUseCase.execute({ + protestId: 'missing-protest', + stewardId: 'steward-1', + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('PROTEST_NOT_FOUND'); + }); + }); + + describe('Submit Protest Defense', () => { + it('should submit defense for a protest awaiting defense', async () => { + const leagueId = 'league-1'; + const raceId = 'race-1'; + const protestingDriverId = 'driver-1'; + const accusedDriverId = 'driver-2'; + + await seedRacingLeague({ leagueId }); + await seedRace({ raceId, leagueId }); + await seedDriver({ driverId: protestingDriverId }); + await seedDriver({ driverId: accusedDriverId }); + const protest = await seedProtest({ + protestId: 'protest-1', + raceId, + protestingDriverId, + accusedDriverId, + status: 'awaiting_defense', + }); + + const result = await submitProtestDefenseUseCase.execute({ + leagueId, + protestId: protest.id, + driverId: accusedDriverId, + defenseText: 'I was not at fault', + videoUrl: 'https://example.com/defense.mp4', + }); + + expect(result.isOk()).toBe(true); + const data = result.unwrap(); + expect(data.protestId).toBe('protest-1'); + + const updatedProtest = await context.protestRepository.findById('protest-1'); + expect(updatedProtest?.status.toString()).toBe('under_review'); + expect(updatedProtest?.defense?.statement.toString()).toBe('I was not at fault'); + expect(updatedProtest?.defense?.videoUrl?.toString()).toBe('https://example.com/defense.mp4'); + }); + + it('should return PROTEST_NOT_FOUND when protest does not exist', async () => { + const leagueId = 'league-1'; + await seedRacingLeague({ leagueId }); + + const result = await submitProtestDefenseUseCase.execute({ + leagueId, + protestId: 'missing-protest', + driverId: 'driver-2', + defenseText: 'I was not at fault', + }); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().code).toBe('PROTEST_NOT_FOUND'); + }); + }); +}); diff --git a/tests/integration/leagues/wallet/WalletManagement.test.ts b/tests/integration/leagues/wallet/WalletManagement.test.ts new file mode 100644 index 000000000..23f74a069 --- /dev/null +++ b/tests/integration/leagues/wallet/WalletManagement.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { LeaguesTestContext } from '../LeaguesTestContext'; +import { League } from '../../../../core/racing/domain/entities/League'; +import { LeagueWallet } from '../../../../core/racing/domain/entities/league-wallet/LeagueWallet'; +import { Transaction } from '../../../../core/racing/domain/entities/league-wallet/Transaction'; +import { Money } from '../../../../core/racing/domain/value-objects/Money'; + +describe('WalletManagement', () => { + let context: LeaguesTestContext; + + beforeEach(() => { + context = new LeaguesTestContext(); + context.walletRepository.clear(); + context.transactionRepository.clear(); + }); + + describe('GetLeagueWalletUseCase - Success Path', () => { + it('should retrieve current wallet balance', async () => { + const leagueId = 'league-123'; + const ownerId = 'owner-1'; + + await context.racingLeagueRepository.create(League.create({ + id: leagueId, + name: 'Test League', + description: 'Test league description', + ownerId: ownerId, + })); + + const balance = Money.create(1000, 'USD'); + await context.walletRepository.create(LeagueWallet.create({ + id: 'wallet-1', + leagueId, + balance, + })); + + const result = await context.getLeagueWalletUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + expect(result.unwrap().aggregates.balance.amount).toBe(1000); + }); + + it('should retrieve transaction history', async () => { + const leagueId = 'league-123'; + const ownerId = 'owner-1'; + + await context.racingLeagueRepository.create(League.create({ + id: leagueId, + name: 'Test League', + description: 'Test league description', + ownerId: ownerId, + })); + + const wallet = LeagueWallet.create({ + id: 'wallet-1', + leagueId, + balance: Money.create(1000, 'USD'), + }); + await context.walletRepository.create(wallet); + + const tx = Transaction.create({ + id: 'tx1', + walletId: wallet.id, + type: 'sponsorship_payment', + amount: Money.create(1000, 'USD'), + description: 'Deposit', + }); + await context.transactionRepository.create(tx); + + const result = await context.getLeagueWalletUseCase.execute({ leagueId }); + + expect(result.isOk()).toBe(true); + expect(result.unwrap().transactions).toHaveLength(1); + expect(result.unwrap().transactions[0].id.toString()).toBe('tx1'); + }); + }); + + describe('WithdrawFromLeagueWalletUseCase - Success Path', () => { + it('should allow owner to withdraw funds', async () => { + const leagueId = 'league-123'; + const ownerId = 'owner-1'; + + await context.racingLeagueRepository.create(League.create({ + id: leagueId, + name: 'Test League', + description: 'Test league description', + ownerId: ownerId, + })); + + const wallet = LeagueWallet.create({ + id: 'wallet-1', + leagueId, + balance: Money.create(1000, 'USD'), + }); + await context.walletRepository.create(wallet); + + const result = await context.withdrawFromLeagueWalletUseCase.execute({ + leagueId, + requestedById: ownerId, + amount: 500, + currency: 'USD', + reason: 'Test withdrawal' + }); + + expect(result.isOk()).toBe(true); + expect(result.unwrap().walletBalanceAfter.amount).toBe(500); + + const walletAfter = await context.walletRepository.findByLeagueId(leagueId); + expect(walletAfter?.balance.amount).toBe(500); + }); + }); + + describe('WalletManagement - Error Handling', () => { + it('should return error when league does not exist', async () => { + const result = await context.getLeagueWalletUseCase.execute({ leagueId: 'non-existent' }); + + expect(result.isErr()).toBe(true); + expect((result as any).error.code).toBe('LEAGUE_NOT_FOUND'); + }); + + it('should return error when wallet does not exist', async () => { + const leagueId = 'league-123'; + await context.racingLeagueRepository.create(League.create({ + id: leagueId, + name: 'Test League', + description: 'Test league description', + ownerId: 'owner-1', + })); + + const result = await context.getLeagueWalletUseCase.execute({ leagueId }); + + expect(result.isErr()).toBe(true); + expect((result as any).error.code).toBe('WALLET_NOT_FOUND'); + }); + + it('should prevent non-owner from withdrawing', async () => { + const leagueId = 'league-123'; + const ownerId = 'owner-1'; + const otherId = 'other-user'; + + await context.racingLeagueRepository.create(League.create({ + id: leagueId, + name: 'Test League', + description: 'Test league description', + ownerId: ownerId, + })); + + await context.walletRepository.create(LeagueWallet.create({ + id: 'wallet-1', + leagueId, + balance: Money.create(1000, 'USD'), + })); + + const result = await context.withdrawFromLeagueWalletUseCase.execute({ + leagueId, + requestedById: otherId, + amount: 500, + currency: 'USD' + }); + + expect(result.isErr()).toBe(true); + expect((result as any).error.code).toBe('UNAUTHORIZED_WITHDRAWAL'); + }); + }); +});