integration tests
This commit is contained in:
@@ -92,4 +92,9 @@ export class InMemoryLeagueMembershipRepository implements LeagueMembershipRepos
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.memberships.clear();
|
||||
this.joinRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ export class InMemoryLeagueRepository implements LeagueRepository {
|
||||
this.logger.info('InMemoryLeagueRepository initialized');
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.leagues.clear();
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<League | null> {
|
||||
this.logger.debug(`Attempting to find league with ID: ${id}.`);
|
||||
try {
|
||||
|
||||
@@ -105,4 +105,8 @@ export class InMemoryRaceRepository implements RaceRepository {
|
||||
this.logger.debug(`[InMemoryRaceRepository] Checking existence of race with ID: ${id}.`);
|
||||
return Promise.resolve(this.races.has(id));
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.races.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,4 +83,8 @@ export class InMemorySeasonRepository implements SeasonRepository {
|
||||
);
|
||||
return Promise.resolve(activeSeasons);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.seasons.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,4 +95,9 @@ export class InMemorySponsorRepository implements SponsorRepository {
|
||||
this.logger.debug(`[InMemorySponsorRepository] Checking existence of sponsor with ID: ${id}`);
|
||||
return Promise.resolve(this.sponsors.has(id));
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.sponsors.clear();
|
||||
this.emailIndex.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,4 +109,8 @@ export class InMemorySponsorshipRequestRepository implements SponsorshipRequestR
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Checking existence of request with ID: ${id}.`);
|
||||
return Promise.resolve(this.requests.has(id));
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.requests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* Integration Test: GetProfileOverviewUseCase Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of GetProfileOverviewUseCase:
|
||||
* Integration Test: Driver Profile Use Cases Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of driver profile-related Use Cases:
|
||||
* - GetProfileOverviewUseCase: Retrieves driver profile overview with statistics, teams, friends, and extended info
|
||||
* - UpdateDriverProfileUseCase: Updates driver profile information
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Providers, other Use Cases)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
@@ -17,13 +18,14 @@ import { InMemorySocialGraphRepository } from '../../../adapters/social/persiste
|
||||
import { InMemoryDriverExtendedProfileProvider } from '../../../adapters/racing/ports/InMemoryDriverExtendedProfileProvider';
|
||||
import { InMemoryDriverStatsRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository';
|
||||
import { GetProfileOverviewUseCase } from '../../../core/racing/application/use-cases/GetProfileOverviewUseCase';
|
||||
import { UpdateDriverProfileUseCase } from '../../../core/racing/application/use-cases/UpdateDriverProfileUseCase';
|
||||
import { DriverStatsUseCase } from '../../../core/racing/application/use-cases/DriverStatsUseCase';
|
||||
import { RankingUseCase } from '../../../core/racing/application/use-cases/RankingUseCase';
|
||||
import { Driver } from '../../../core/racing/domain/entities/Driver';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('GetProfileOverviewUseCase Orchestration', () => {
|
||||
describe('Driver Profile Use Cases Orchestration', () => {
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let teamMembershipRepository: InMemoryTeamMembershipRepository;
|
||||
@@ -33,6 +35,7 @@ describe('GetProfileOverviewUseCase Orchestration', () => {
|
||||
let driverStatsUseCase: DriverStatsUseCase;
|
||||
let rankingUseCase: RankingUseCase;
|
||||
let getProfileOverviewUseCase: GetProfileOverviewUseCase;
|
||||
let updateDriverProfileUseCase: UpdateDriverProfileUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
@@ -73,6 +76,8 @@ describe('GetProfileOverviewUseCase Orchestration', () => {
|
||||
driverStatsUseCase,
|
||||
rankingUseCase
|
||||
);
|
||||
|
||||
updateDriverProfileUseCase = new UpdateDriverProfileUseCase(driverRepository, mockLogger);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -84,6 +89,230 @@ describe('GetProfileOverviewUseCase Orchestration', () => {
|
||||
driverStatsRepository.clear();
|
||||
});
|
||||
|
||||
describe('UpdateDriverProfileUseCase - Success Path', () => {
|
||||
it('should update driver bio', async () => {
|
||||
// Scenario: Update driver bio
|
||||
// Given: A driver exists with bio
|
||||
const driverId = 'd2';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '2', name: 'Update Driver', country: 'US', bio: 'Original bio' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: UpdateDriverProfileUseCase.execute() is called with new bio
|
||||
const result = await updateDriverProfileUseCase.execute({
|
||||
driverId,
|
||||
bio: 'Updated bio',
|
||||
});
|
||||
|
||||
// Then: The operation should succeed
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
// And: The driver's bio should be updated
|
||||
const updatedDriver = await driverRepository.findById(driverId);
|
||||
expect(updatedDriver).not.toBeNull();
|
||||
expect(updatedDriver!.bio?.toString()).toBe('Updated bio');
|
||||
});
|
||||
|
||||
it('should update driver country', async () => {
|
||||
// Scenario: Update driver country
|
||||
// Given: A driver exists with country
|
||||
const driverId = 'd3';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '3', name: 'Country Driver', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: UpdateDriverProfileUseCase.execute() is called with new country
|
||||
const result = await updateDriverProfileUseCase.execute({
|
||||
driverId,
|
||||
country: 'DE',
|
||||
});
|
||||
|
||||
// Then: The operation should succeed
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
// And: The driver's country should be updated
|
||||
const updatedDriver = await driverRepository.findById(driverId);
|
||||
expect(updatedDriver).not.toBeNull();
|
||||
expect(updatedDriver!.country.toString()).toBe('DE');
|
||||
});
|
||||
|
||||
it('should update multiple profile fields at once', async () => {
|
||||
// Scenario: Update multiple fields
|
||||
// Given: A driver exists
|
||||
const driverId = 'd4';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '4', name: 'Multi Update Driver', country: 'US', bio: 'Original bio' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: UpdateDriverProfileUseCase.execute() is called with multiple updates
|
||||
const result = await updateDriverProfileUseCase.execute({
|
||||
driverId,
|
||||
bio: 'Updated bio',
|
||||
country: 'FR',
|
||||
});
|
||||
|
||||
// Then: The operation should succeed
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
// And: Both fields should be updated
|
||||
const updatedDriver = await driverRepository.findById(driverId);
|
||||
expect(updatedDriver).not.toBeNull();
|
||||
expect(updatedDriver!.bio?.toString()).toBe('Updated bio');
|
||||
expect(updatedDriver!.country.toString()).toBe('FR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateDriverProfileUseCase - Validation', () => {
|
||||
it('should reject update with empty bio', async () => {
|
||||
// Scenario: Empty bio
|
||||
// Given: A driver exists
|
||||
const driverId = 'd5';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '5', name: 'Empty Bio Driver', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: UpdateDriverProfileUseCase.execute() is called with empty bio
|
||||
const result = await updateDriverProfileUseCase.execute({
|
||||
driverId,
|
||||
bio: '',
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('INVALID_PROFILE_DATA');
|
||||
expect(error.details.message).toBe('Profile data is invalid');
|
||||
});
|
||||
|
||||
it('should reject update with empty country', async () => {
|
||||
// Scenario: Empty country
|
||||
// Given: A driver exists
|
||||
const driverId = 'd6';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '6', name: 'Empty Country Driver', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: UpdateDriverProfileUseCase.execute() is called with empty country
|
||||
const result = await updateDriverProfileUseCase.execute({
|
||||
driverId,
|
||||
country: '',
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('INVALID_PROFILE_DATA');
|
||||
expect(error.details.message).toBe('Profile data is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateDriverProfileUseCase - Error Handling', () => {
|
||||
it('should return error when driver does not exist', async () => {
|
||||
// Scenario: Non-existent driver
|
||||
// Given: No driver exists with the given ID
|
||||
const nonExistentDriverId = 'non-existent-driver';
|
||||
|
||||
// When: UpdateDriverProfileUseCase.execute() is called with non-existent driver ID
|
||||
const result = await updateDriverProfileUseCase.execute({
|
||||
driverId: nonExistentDriverId,
|
||||
bio: 'New bio',
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('DRIVER_NOT_FOUND');
|
||||
expect(error.details.message).toContain('Driver with id');
|
||||
});
|
||||
|
||||
it('should return error when driver ID is invalid', async () => {
|
||||
// Scenario: Invalid driver ID
|
||||
// Given: An invalid driver ID (empty string)
|
||||
const invalidDriverId = '';
|
||||
|
||||
// When: UpdateDriverProfileUseCase.execute() is called with invalid driver ID
|
||||
const result = await updateDriverProfileUseCase.execute({
|
||||
driverId: invalidDriverId,
|
||||
bio: 'New bio',
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('DRIVER_NOT_FOUND');
|
||||
expect(error.details.message).toContain('Driver with id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DriverStatsUseCase - Success Path', () => {
|
||||
it('should compute driver statistics from race results', async () => {
|
||||
// Scenario: Driver with race results
|
||||
// Given: A driver exists
|
||||
const driverId = 'd7';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '7', name: 'Stats Driver', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: The driver has race results
|
||||
await driverStatsRepository.saveDriverStats(driverId, {
|
||||
rating: 1800,
|
||||
totalRaces: 15,
|
||||
wins: 3,
|
||||
podiums: 8,
|
||||
overallRank: 5,
|
||||
safetyRating: 4.2,
|
||||
sportsmanshipRating: 90,
|
||||
dnfs: 1,
|
||||
avgFinish: 4.2,
|
||||
bestFinish: 1,
|
||||
worstFinish: 12,
|
||||
consistency: 80,
|
||||
experienceLevel: 'intermediate'
|
||||
});
|
||||
|
||||
// When: DriverStatsUseCase.getDriverStats() is called
|
||||
const stats = await driverStatsUseCase.getDriverStats(driverId);
|
||||
|
||||
// Then: Should return computed statistics
|
||||
expect(stats).not.toBeNull();
|
||||
expect(stats!.rating).toBe(1800);
|
||||
expect(stats!.totalRaces).toBe(15);
|
||||
expect(stats!.wins).toBe(3);
|
||||
expect(stats!.podiums).toBe(8);
|
||||
expect(stats!.overallRank).toBe(5);
|
||||
expect(stats!.safetyRating).toBe(4.2);
|
||||
expect(stats!.sportsmanshipRating).toBe(90);
|
||||
expect(stats!.dnfs).toBe(1);
|
||||
expect(stats!.avgFinish).toBe(4.2);
|
||||
expect(stats!.bestFinish).toBe(1);
|
||||
expect(stats!.worstFinish).toBe(12);
|
||||
expect(stats!.consistency).toBe(80);
|
||||
expect(stats!.experienceLevel).toBe('intermediate');
|
||||
});
|
||||
|
||||
it('should handle driver with no race results', async () => {
|
||||
// Scenario: New driver with no history
|
||||
// Given: A driver exists
|
||||
const driverId = 'd8';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '8', name: 'New Stats Driver', country: 'DE' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: DriverStatsUseCase.getDriverStats() is called
|
||||
const stats = await driverStatsUseCase.getDriverStats(driverId);
|
||||
|
||||
// Then: Should return null stats
|
||||
expect(stats).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DriverStatsUseCase - Error Handling', () => {
|
||||
it('should return error when driver does not exist', async () => {
|
||||
// Scenario: Non-existent driver
|
||||
// Given: No driver exists with the given ID
|
||||
const nonExistentDriverId = 'non-existent-driver';
|
||||
|
||||
// When: DriverStatsUseCase.getDriverStats() is called
|
||||
const stats = await driverStatsUseCase.getDriverStats(nonExistentDriverId);
|
||||
|
||||
// Then: Should return null (no error for non-existent driver)
|
||||
expect(stats).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetProfileOverviewUseCase - Success Path', () => {
|
||||
it('should retrieve complete driver profile overview', async () => {
|
||||
// Scenario: Driver with complete data
|
||||
@@ -110,7 +339,7 @@ describe('GetProfileOverviewUseCase Orchestration', () => {
|
||||
});
|
||||
|
||||
// And: The driver is in a team
|
||||
const team = Team.create({ id: 't1', name: 'Team 1', tag: 'T1', description: 'Desc', ownerId: 'other' });
|
||||
const team = Team.create({ id: 't1', name: 'Team 1', tag: 'T1', description: 'Desc', ownerId: 'other', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
await teamMembershipRepository.saveMembership({
|
||||
teamId: 't1',
|
||||
|
||||
303
tests/integration/profile/profile-use-cases.integration.test.ts
Normal file
303
tests/integration/profile/profile-use-cases.integration.test.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Integration Test: Profile Use Cases Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of profile-related Use Cases:
|
||||
* - GetProfileOverviewUseCase: Retrieves driver profile overview
|
||||
* - UpdateDriverProfileUseCase: Updates driver profile information
|
||||
* - GetDriverLiveriesUseCase: Retrieves driver liveries
|
||||
* - GetLeagueMembershipsUseCase: Retrieves driver league memberships (via league)
|
||||
* - GetPendingSponsorshipRequestsUseCase: Retrieves pending sponsorship requests
|
||||
*
|
||||
* Adheres to Clean Architecture:
|
||||
* - Tests Core Use Cases directly
|
||||
* - Uses In-Memory adapters for repositories
|
||||
* - Follows Given/When/Then pattern
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { InMemorySocialGraphRepository } from '../../../adapters/social/persistence/inmemory/InMemorySocialAndFeed';
|
||||
import { InMemoryDriverExtendedProfileProvider } from '../../../adapters/racing/ports/InMemoryDriverExtendedProfileProvider';
|
||||
import { InMemoryDriverStatsRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository';
|
||||
import { InMemoryLiveryRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLiveryRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemorySponsorshipRequestRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository';
|
||||
import { InMemorySponsorRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||
|
||||
import { GetProfileOverviewUseCase } from '../../../core/racing/application/use-cases/GetProfileOverviewUseCase';
|
||||
import { UpdateDriverProfileUseCase } from '../../../core/racing/application/use-cases/UpdateDriverProfileUseCase';
|
||||
import { DriverStatsUseCase } from '../../../core/racing/application/use-cases/DriverStatsUseCase';
|
||||
import { RankingUseCase } from '../../../core/racing/application/use-cases/RankingUseCase';
|
||||
import { GetDriverLiveriesUseCase } from '../../../core/racing/application/use-cases/GetDriverLiveriesUseCase';
|
||||
import { GetLeagueMembershipsUseCase } from '../../../core/racing/application/use-cases/GetLeagueMembershipsUseCase';
|
||||
import { GetPendingSponsorshipRequestsUseCase } from '../../../core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
|
||||
|
||||
import { Driver } from '../../../core/racing/domain/entities/Driver';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { LeagueMembership } from '../../../core/racing/domain/entities/LeagueMembership';
|
||||
import { DriverLivery } from '../../../core/racing/domain/entities/DriverLivery';
|
||||
import { SponsorshipRequest } from '../../../core/racing/domain/entities/SponsorshipRequest';
|
||||
import { Sponsor } from '../../../core/racing/domain/entities/sponsor/Sponsor';
|
||||
import { Money } from '../../../core/racing/domain/value-objects/Money';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Profile Use Cases Orchestration', () => {
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let teamMembershipRepository: InMemoryTeamMembershipRepository;
|
||||
let socialRepository: InMemorySocialGraphRepository;
|
||||
let driverExtendedProfileProvider: InMemoryDriverExtendedProfileProvider;
|
||||
let driverStatsRepository: InMemoryDriverStatsRepository;
|
||||
let liveryRepository: InMemoryLiveryRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let leagueMembershipRepository: InMemoryLeagueMembershipRepository;
|
||||
let sponsorshipRequestRepository: InMemorySponsorshipRequestRepository;
|
||||
let sponsorRepository: InMemorySponsorRepository;
|
||||
|
||||
let driverStatsUseCase: DriverStatsUseCase;
|
||||
let rankingUseCase: RankingUseCase;
|
||||
let getProfileOverviewUseCase: GetProfileOverviewUseCase;
|
||||
let updateDriverProfileUseCase: UpdateDriverProfileUseCase;
|
||||
let getDriverLiveriesUseCase: GetDriverLiveriesUseCase;
|
||||
let getLeagueMembershipsUseCase: GetLeagueMembershipsUseCase;
|
||||
let getPendingSponsorshipRequestsUseCase: GetPendingSponsorshipRequestsUseCase;
|
||||
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
driverRepository = new InMemoryDriverRepository(mockLogger);
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
teamMembershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
socialRepository = new InMemorySocialGraphRepository(mockLogger);
|
||||
driverExtendedProfileProvider = new InMemoryDriverExtendedProfileProvider(mockLogger);
|
||||
driverStatsRepository = new InMemoryDriverStatsRepository(mockLogger);
|
||||
liveryRepository = new InMemoryLiveryRepository(mockLogger);
|
||||
leagueRepository = new InMemoryLeagueRepository(mockLogger);
|
||||
leagueMembershipRepository = new InMemoryLeagueMembershipRepository(mockLogger);
|
||||
sponsorshipRequestRepository = new InMemorySponsorshipRequestRepository(mockLogger);
|
||||
sponsorRepository = new InMemorySponsorRepository(mockLogger);
|
||||
|
||||
driverStatsUseCase = new DriverStatsUseCase(
|
||||
{} as any,
|
||||
{} as any,
|
||||
driverStatsRepository,
|
||||
mockLogger
|
||||
);
|
||||
|
||||
rankingUseCase = new RankingUseCase(
|
||||
{} as any,
|
||||
{} as any,
|
||||
driverStatsRepository,
|
||||
mockLogger
|
||||
);
|
||||
|
||||
getProfileOverviewUseCase = new GetProfileOverviewUseCase(
|
||||
driverRepository,
|
||||
teamRepository,
|
||||
teamMembershipRepository,
|
||||
socialRepository,
|
||||
driverExtendedProfileProvider,
|
||||
driverStatsUseCase,
|
||||
rankingUseCase
|
||||
);
|
||||
|
||||
updateDriverProfileUseCase = new UpdateDriverProfileUseCase(driverRepository, mockLogger);
|
||||
getDriverLiveriesUseCase = new GetDriverLiveriesUseCase(liveryRepository, mockLogger);
|
||||
getLeagueMembershipsUseCase = new GetLeagueMembershipsUseCase(leagueMembershipRepository, driverRepository, leagueRepository);
|
||||
getPendingSponsorshipRequestsUseCase = new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepository, sponsorRepository);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
driverRepository.clear();
|
||||
teamRepository.clear();
|
||||
teamMembershipRepository.clear();
|
||||
socialRepository.clear();
|
||||
driverExtendedProfileProvider.clear();
|
||||
driverStatsRepository.clear();
|
||||
liveryRepository.clear();
|
||||
leagueRepository.clear();
|
||||
leagueMembershipRepository.clear();
|
||||
sponsorshipRequestRepository.clear();
|
||||
sponsorRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetProfileOverviewUseCase', () => {
|
||||
it('should retrieve complete driver profile overview', async () => {
|
||||
// Given: A driver exists with stats, team, and friends
|
||||
const driverId = 'd1';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '1', name: 'John Doe', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
await driverStatsRepository.saveDriverStats(driverId, {
|
||||
rating: 2000,
|
||||
totalRaces: 10,
|
||||
wins: 2,
|
||||
podiums: 5,
|
||||
overallRank: 1,
|
||||
safetyRating: 4.5,
|
||||
sportsmanshipRating: 95,
|
||||
dnfs: 0,
|
||||
avgFinish: 3.5,
|
||||
bestFinish: 1,
|
||||
worstFinish: 10,
|
||||
consistency: 85,
|
||||
experienceLevel: 'pro'
|
||||
});
|
||||
|
||||
const team = Team.create({ id: 't1', name: 'Team 1', tag: 'T1', description: 'Desc', ownerId: 'other', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
await teamMembershipRepository.saveMembership({
|
||||
teamId: 't1',
|
||||
driverId: driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
socialRepository.seed({
|
||||
drivers: [driver, Driver.create({ id: 'f1', iracingId: '2', name: 'Friend 1', country: 'UK' })],
|
||||
friendships: [{ driverId: driverId, friendId: 'f1' }],
|
||||
feedEvents: []
|
||||
});
|
||||
|
||||
// When: GetProfileOverviewUseCase.execute() is called
|
||||
const result = await getProfileOverviewUseCase.execute({ driverId });
|
||||
|
||||
// Then: The result should contain all profile sections
|
||||
expect(result.isOk()).toBe(true);
|
||||
const overview = result.unwrap();
|
||||
expect(overview.driverInfo.driver.id).toBe(driverId);
|
||||
expect(overview.stats?.rating).toBe(2000);
|
||||
expect(overview.teamMemberships).toHaveLength(1);
|
||||
expect(overview.socialSummary.friendsCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateDriverProfileUseCase', () => {
|
||||
it('should update driver bio and country', async () => {
|
||||
// Given: A driver exists
|
||||
const driverId = 'd2';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '2', name: 'Update Driver', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: UpdateDriverProfileUseCase.execute() is called
|
||||
const result = await updateDriverProfileUseCase.execute({
|
||||
driverId,
|
||||
bio: 'New bio',
|
||||
country: 'DE',
|
||||
});
|
||||
|
||||
// Then: The driver should be updated
|
||||
expect(result.isOk()).toBe(true);
|
||||
const updatedDriver = await driverRepository.findById(driverId);
|
||||
expect(updatedDriver?.bio?.toString()).toBe('New bio');
|
||||
expect(updatedDriver?.country.toString()).toBe('DE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetDriverLiveriesUseCase', () => {
|
||||
it('should retrieve driver liveries', async () => {
|
||||
// Given: A driver has liveries
|
||||
const driverId = 'd3';
|
||||
const livery = DriverLivery.create({
|
||||
id: 'l1',
|
||||
driverId,
|
||||
gameId: 'iracing',
|
||||
carId: 'porsche_911_gt3_r',
|
||||
uploadedImageUrl: 'https://example.com/livery.png'
|
||||
});
|
||||
await liveryRepository.createDriverLivery(livery);
|
||||
|
||||
// When: GetDriverLiveriesUseCase.execute() is called
|
||||
const result = await getDriverLiveriesUseCase.execute({ driverId });
|
||||
|
||||
// Then: It should return the liveries
|
||||
expect(result.isOk()).toBe(true);
|
||||
const liveries = result.unwrap();
|
||||
expect(liveries).toHaveLength(1);
|
||||
expect(liveries[0].id).toBe('l1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetLeagueMembershipsUseCase', () => {
|
||||
it('should retrieve league memberships for a league', async () => {
|
||||
// Given: A league with members
|
||||
const leagueId = 'lg1';
|
||||
const driverId = 'd4';
|
||||
const league = League.create({ id: leagueId, name: 'League 1', description: 'Desc', ownerId: 'owner' });
|
||||
await leagueRepository.create(league);
|
||||
|
||||
const membership = LeagueMembership.create({
|
||||
id: 'm1',
|
||||
leagueId,
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active'
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
|
||||
const driver = Driver.create({ id: driverId, iracingId: '4', name: 'Member Driver', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: GetLeagueMembershipsUseCase.execute() is called
|
||||
const result = await getLeagueMembershipsUseCase.execute({ leagueId });
|
||||
|
||||
// Then: It should return the memberships with driver info
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.memberships).toHaveLength(1);
|
||||
expect(data.memberships[0].driver?.id).toBe(driverId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetPendingSponsorshipRequestsUseCase', () => {
|
||||
it('should retrieve pending sponsorship requests for a driver', async () => {
|
||||
// Given: A driver has pending sponsorship requests
|
||||
const driverId = 'd5';
|
||||
const sponsorId = 's1';
|
||||
|
||||
const sponsor = Sponsor.create({
|
||||
id: sponsorId,
|
||||
name: 'Sponsor 1',
|
||||
contactEmail: 'sponsor@example.com'
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
const request = SponsorshipRequest.create({
|
||||
id: 'sr1',
|
||||
sponsorId,
|
||||
entityType: 'driver',
|
||||
entityId: driverId,
|
||||
tier: 'main',
|
||||
offeredAmount: Money.create(1000, 'USD')
|
||||
});
|
||||
await sponsorshipRequestRepository.create(request);
|
||||
|
||||
// When: GetPendingSponsorshipRequestsUseCase.execute() is called
|
||||
const result = await getPendingSponsorshipRequestsUseCase.execute({
|
||||
entityType: 'driver',
|
||||
entityId: driverId
|
||||
});
|
||||
|
||||
// Then: It should return the pending requests
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.requests).toHaveLength(1);
|
||||
expect(data.requests[0].request.id).toBe('sr1');
|
||||
expect(data.requests[0].sponsor?.id.toString()).toBe(sponsorId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,767 +3,143 @@
|
||||
*
|
||||
* Tests the orchestration logic of race detail page-related Use Cases:
|
||||
* - GetRaceDetailUseCase: Retrieves comprehensive race details
|
||||
* - GetRaceParticipantsUseCase: Retrieves race participants count
|
||||
* - GetRaceWinnerUseCase: Retrieves race winner and podium
|
||||
* - GetRaceStatisticsUseCase: Retrieves race statistics
|
||||
* - GetRaceLapTimesUseCase: Retrieves race lap times
|
||||
* - GetRaceQualifyingUseCase: Retrieves race qualifying results
|
||||
* - GetRacePointsUseCase: Retrieves race points distribution
|
||||
* - GetRaceHighlightsUseCase: Retrieves race highlights
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Adheres to Clean Architecture:
|
||||
* - Tests Core Use Cases directly
|
||||
* - Uses In-Memory adapters for repositories
|
||||
* - Follows Given/When/Then pattern
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetRaceDetailUseCase } from '../../../core/races/use-cases/GetRaceDetailUseCase';
|
||||
import { GetRaceParticipantsUseCase } from '../../../core/races/use-cases/GetRaceParticipantsUseCase';
|
||||
import { GetRaceWinnerUseCase } from '../../../core/races/use-cases/GetRaceWinnerUseCase';
|
||||
import { GetRaceStatisticsUseCase } from '../../../core/races/use-cases/GetRaceStatisticsUseCase';
|
||||
import { GetRaceLapTimesUseCase } from '../../../core/races/use-cases/GetRaceLapTimesUseCase';
|
||||
import { GetRaceQualifyingUseCase } from '../../../core/races/use-cases/GetRaceQualifyingUseCase';
|
||||
import { GetRacePointsUseCase } from '../../../core/races/use-cases/GetRacePointsUseCase';
|
||||
import { GetRaceHighlightsUseCase } from '../../../core/races/use-cases/GetRaceHighlightsUseCase';
|
||||
import { RaceDetailQuery } from '../../../core/races/ports/RaceDetailQuery';
|
||||
import { RaceParticipantsQuery } from '../../../core/races/ports/RaceParticipantsQuery';
|
||||
import { RaceWinnerQuery } from '../../../core/races/ports/RaceWinnerQuery';
|
||||
import { RaceStatisticsQuery } from '../../../core/races/ports/RaceStatisticsQuery';
|
||||
import { RaceLapTimesQuery } from '../../../core/races/ports/RaceLapTimesQuery';
|
||||
import { RaceQualifyingQuery } from '../../../core/races/ports/RaceQualifyingQuery';
|
||||
import { RacePointsQuery } from '../../../core/races/ports/RacePointsQuery';
|
||||
import { RaceHighlightsQuery } from '../../../core/races/ports/RaceHighlightsQuery';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRaceRegistrationRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||
import { InMemoryResultRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryResultRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { GetRaceDetailUseCase } from '../../../core/racing/application/use-cases/GetRaceDetailUseCase';
|
||||
import { Race } from '../../../core/racing/domain/entities/Race';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { Driver } from '../../../core/racing/domain/entities/Driver';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Race Detail Use Case Orchestration', () => {
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let raceRegistrationRepository: InMemoryRaceRegistrationRepository;
|
||||
let resultRepository: InMemoryResultRepository;
|
||||
let leagueMembershipRepository: InMemoryLeagueMembershipRepository;
|
||||
let getRaceDetailUseCase: GetRaceDetailUseCase;
|
||||
let getRaceParticipantsUseCase: GetRaceParticipantsUseCase;
|
||||
let getRaceWinnerUseCase: GetRaceWinnerUseCase;
|
||||
let getRaceStatisticsUseCase: GetRaceStatisticsUseCase;
|
||||
let getRaceLapTimesUseCase: GetRaceLapTimesUseCase;
|
||||
let getRaceQualifyingUseCase: GetRaceQualifyingUseCase;
|
||||
let getRacePointsUseCase: GetRacePointsUseCase;
|
||||
let getRaceHighlightsUseCase: GetRaceHighlightsUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// raceRepository = new InMemoryRaceRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getRaceDetailUseCase = new GetRaceDetailUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceParticipantsUseCase = new GetRaceParticipantsUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceWinnerUseCase = new GetRaceWinnerUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceStatisticsUseCase = new GetRaceStatisticsUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceLapTimesUseCase = new GetRaceLapTimesUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceQualifyingUseCase = new GetRaceQualifyingUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRacePointsUseCase = new GetRacePointsUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceHighlightsUseCase = new GetRaceHighlightsUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
raceRepository = new InMemoryRaceRepository(mockLogger);
|
||||
leagueRepository = new InMemoryLeagueRepository(mockLogger);
|
||||
driverRepository = new InMemoryDriverRepository(mockLogger);
|
||||
raceRegistrationRepository = new InMemoryRaceRegistrationRepository(mockLogger);
|
||||
resultRepository = new InMemoryResultRepository(mockLogger, raceRepository);
|
||||
leagueMembershipRepository = new InMemoryLeagueMembershipRepository(mockLogger);
|
||||
|
||||
getRaceDetailUseCase = new GetRaceDetailUseCase(
|
||||
raceRepository,
|
||||
leagueRepository,
|
||||
driverRepository,
|
||||
raceRegistrationRepository,
|
||||
resultRepository,
|
||||
leagueMembershipRepository
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// raceRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
beforeEach(async () => {
|
||||
// Clear repositories
|
||||
(raceRepository as any).races.clear();
|
||||
leagueRepository.clear();
|
||||
await driverRepository.clear();
|
||||
(raceRegistrationRepository as any).registrations.clear();
|
||||
(resultRepository as any).results.clear();
|
||||
leagueMembershipRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetRaceDetailUseCase - Success Path', () => {
|
||||
describe('GetRaceDetailUseCase', () => {
|
||||
it('should retrieve race detail with complete information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver views race detail
|
||||
// Given: A race exists with complete information
|
||||
// And: The race has track, car, league, date, time, duration, status
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should contain complete race information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
// Given: A race and league exist
|
||||
const leagueId = 'l1';
|
||||
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
|
||||
await leagueRepository.create(league);
|
||||
|
||||
const raceId = 'r1';
|
||||
const race = Race.create({
|
||||
id: raceId,
|
||||
leagueId,
|
||||
scheduledAt: new Date(Date.now() + 86400000),
|
||||
track: 'Spa',
|
||||
car: 'GT3',
|
||||
status: 'scheduled'
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
|
||||
// When: GetRaceDetailUseCase.execute() is called
|
||||
const result = await getRaceDetailUseCase.execute({ raceId });
|
||||
|
||||
// Then: The result should contain race and league information
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.race.id).toBe(raceId);
|
||||
expect(data.league?.id).toBe(leagueId);
|
||||
expect(data.isUserRegistered).toBe(false);
|
||||
});
|
||||
|
||||
it('should retrieve race detail with track layout', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with track layout
|
||||
// Given: A race exists with track layout
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show track layout
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with weather information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with weather information
|
||||
// Given: A race exists with weather information
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show weather information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with race conditions', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with conditions
|
||||
// Given: A race exists with conditions
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show race conditions
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with description', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with description
|
||||
// Given: A race exists with description
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show description
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with rules', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with rules
|
||||
// Given: A race exists with rules
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show rules
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with requirements', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with requirements
|
||||
// Given: A race exists with requirements
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show requirements
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with page title', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with page title
|
||||
// Given: A race exists
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should include page title
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with page description', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with page description
|
||||
// Given: A race exists
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should include page description
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceDetailUseCase - Edge Cases', () => {
|
||||
it('should handle race with missing track information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with missing track data
|
||||
// Given: A race exists with missing track information
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should contain race with available information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing car information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with missing car data
|
||||
// Given: A race exists with missing car information
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should contain race with available information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing league information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with missing league data
|
||||
// Given: A race exists with missing league information
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should contain race with available information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no description', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no description
|
||||
// Given: A race exists with no description
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default description
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no rules', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no rules
|
||||
// Given: A race exists with no rules
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default rules
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no requirements', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no requirements
|
||||
// Given: A race exists with no requirements
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default requirements
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceDetailUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceDetailUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
const result = await getRaceDetailUseCase.execute({ raceId: 'non-existent' });
|
||||
|
||||
// Then: Should return RACE_NOT_FOUND error
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('RACE_NOT_FOUND');
|
||||
});
|
||||
|
||||
it('should throw error when race ID is invalid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid race ID
|
||||
// Given: An invalid race ID (e.g., empty string, null, undefined)
|
||||
// When: GetRaceDetailUseCase.execute() is called with invalid race ID
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
it('should identify if a driver is registered', async () => {
|
||||
// Given: A race and a registered driver
|
||||
const leagueId = 'l1';
|
||||
const raceId = 'r1';
|
||||
const driverId = 'd1';
|
||||
|
||||
const race = Race.create({
|
||||
id: raceId,
|
||||
leagueId,
|
||||
scheduledAt: new Date(Date.now() + 86400000),
|
||||
track: 'Spa',
|
||||
car: 'GT3',
|
||||
status: 'scheduled'
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: A race exists
|
||||
// And: RaceRepository throws an error during query
|
||||
// When: GetRaceDetailUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
describe('GetRaceParticipantsUseCase - Success Path', () => {
|
||||
it('should retrieve race participants count', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with participants
|
||||
// Given: A race exists with participants
|
||||
// When: GetRaceParticipantsUseCase.execute() is called with race ID
|
||||
// Then: The result should show participants count
|
||||
// And: EventPublisher should emit RaceParticipantsAccessedEvent
|
||||
});
|
||||
// Mock registration (using any to bypass private access if needed, but InMemoryRaceRegistrationRepository has register method)
|
||||
await raceRegistrationRepository.register({
|
||||
raceId: raceId as any,
|
||||
driverId: driverId as any,
|
||||
registeredAt: new Date()
|
||||
} as any);
|
||||
|
||||
it('should retrieve race participants count for race with no participants', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no participants
|
||||
// Given: A race exists with no participants
|
||||
// When: GetRaceParticipantsUseCase.execute() is called with race ID
|
||||
// Then: The result should show 0 participants
|
||||
// And: EventPublisher should emit RaceParticipantsAccessedEvent
|
||||
});
|
||||
// When: GetRaceDetailUseCase.execute() is called with driverId
|
||||
const result = await getRaceDetailUseCase.execute({ raceId, driverId });
|
||||
|
||||
it('should retrieve race participants count for upcoming race', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Upcoming race with participants
|
||||
// Given: An upcoming race exists with participants
|
||||
// When: GetRaceParticipantsUseCase.execute() is called with race ID
|
||||
// Then: The result should show participants count
|
||||
// And: EventPublisher should emit RaceParticipantsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race participants count for completed race', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Completed race with participants
|
||||
// Given: A completed race exists with participants
|
||||
// When: GetRaceParticipantsUseCase.execute() is called with race ID
|
||||
// Then: The result should show participants count
|
||||
// And: EventPublisher should emit RaceParticipantsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceParticipantsUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceParticipantsUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRaceParticipantsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceWinnerUseCase - Success Path', () => {
|
||||
it('should retrieve race winner for completed race', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Completed race with winner
|
||||
// Given: A completed race exists with winner
|
||||
// When: GetRaceWinnerUseCase.execute() is called with race ID
|
||||
// Then: The result should show race winner
|
||||
// And: EventPublisher should emit RaceWinnerAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race podium for completed race', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Completed race with podium
|
||||
// Given: A completed race exists with podium
|
||||
// When: GetRaceWinnerUseCase.execute() is called with race ID
|
||||
// Then: The result should show top 3 finishers
|
||||
// And: EventPublisher should emit RaceWinnerAccessedEvent
|
||||
});
|
||||
|
||||
it('should not retrieve winner for upcoming race', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Upcoming race without winner
|
||||
// Given: An upcoming race exists
|
||||
// When: GetRaceWinnerUseCase.execute() is called with race ID
|
||||
// Then: The result should not show winner or podium
|
||||
// And: EventPublisher should emit RaceWinnerAccessedEvent
|
||||
});
|
||||
|
||||
it('should not retrieve winner for in-progress race', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: In-progress race without winner
|
||||
// Given: An in-progress race exists
|
||||
// When: GetRaceWinnerUseCase.execute() is called with race ID
|
||||
// Then: The result should not show winner or podium
|
||||
// And: EventPublisher should emit RaceWinnerAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceWinnerUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceWinnerUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRaceWinnerUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceStatisticsUseCase - Success Path', () => {
|
||||
it('should retrieve race statistics with lap count', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with lap count
|
||||
// Given: A race exists with lap count
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show lap count
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with incidents count', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with incidents count
|
||||
// Given: A race exists with incidents count
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show incidents count
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with penalties count', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with penalties count
|
||||
// Given: A race exists with penalties count
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show penalties count
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with protests count', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with protests count
|
||||
// Given: A race exists with protests count
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show protests count
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with stewarding actions count', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with stewarding actions count
|
||||
// Given: A race exists with stewarding actions count
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show stewarding actions count
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with all metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with all statistics
|
||||
// Given: A race exists with all statistics
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show all statistics
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with empty metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no statistics
|
||||
// Given: A race exists with no statistics
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default statistics
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceStatisticsUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRaceStatisticsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceLapTimesUseCase - Success Path', () => {
|
||||
it('should retrieve race lap times with average lap time', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with average lap time
|
||||
// Given: A race exists with average lap time
|
||||
// When: GetRaceLapTimesUseCase.execute() is called with race ID
|
||||
// Then: The result should show average lap time
|
||||
// And: EventPublisher should emit RaceLapTimesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race lap times with fastest lap', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with fastest lap
|
||||
// Given: A race exists with fastest lap
|
||||
// When: GetRaceLapTimesUseCase.execute() is called with race ID
|
||||
// Then: The result should show fastest lap
|
||||
// And: EventPublisher should emit RaceLapTimesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race lap times with best sector times', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with best sector times
|
||||
// Given: A race exists with best sector times
|
||||
// When: GetRaceLapTimesUseCase.execute() is called with race ID
|
||||
// Then: The result should show best sector times
|
||||
// And: EventPublisher should emit RaceLapTimesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race lap times with all metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with all lap time metrics
|
||||
// Given: A race exists with all lap time metrics
|
||||
// When: GetRaceLapTimesUseCase.execute() is called with race ID
|
||||
// Then: The result should show all lap time metrics
|
||||
// And: EventPublisher should emit RaceLapTimesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race lap times with empty metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no lap times
|
||||
// Given: A race exists with no lap times
|
||||
// When: GetRaceLapTimesUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default lap times
|
||||
// And: EventPublisher should emit RaceLapTimesAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceLapTimesUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceLapTimesUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRaceLapTimesUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceQualifyingUseCase - Success Path', () => {
|
||||
it('should retrieve race qualifying results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with qualifying results
|
||||
// Given: A race exists with qualifying results
|
||||
// When: GetRaceQualifyingUseCase.execute() is called with race ID
|
||||
// Then: The result should show qualifying results
|
||||
// And: EventPublisher should emit RaceQualifyingAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race starting grid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with starting grid
|
||||
// Given: A race exists with starting grid
|
||||
// When: GetRaceQualifyingUseCase.execute() is called with race ID
|
||||
// Then: The result should show starting grid
|
||||
// And: EventPublisher should emit RaceQualifyingAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race qualifying results with pole position', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with pole position
|
||||
// Given: A race exists with pole position
|
||||
// When: GetRaceQualifyingUseCase.execute() is called with race ID
|
||||
// Then: The result should show pole position
|
||||
// And: EventPublisher should emit RaceQualifyingAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race qualifying results with empty results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no qualifying results
|
||||
// Given: A race exists with no qualifying results
|
||||
// When: GetRaceQualifyingUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default qualifying results
|
||||
// And: EventPublisher should emit RaceQualifyingAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceQualifyingUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceQualifyingUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRaceQualifyingUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRacePointsUseCase - Success Path', () => {
|
||||
it('should retrieve race points distribution', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with points distribution
|
||||
// Given: A race exists with points distribution
|
||||
// When: GetRacePointsUseCase.execute() is called with race ID
|
||||
// Then: The result should show points distribution
|
||||
// And: EventPublisher should emit RacePointsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race championship implications', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with championship implications
|
||||
// Given: A race exists with championship implications
|
||||
// When: GetRacePointsUseCase.execute() is called with race ID
|
||||
// Then: The result should show championship implications
|
||||
// And: EventPublisher should emit RacePointsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race points with empty distribution', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no points distribution
|
||||
// Given: A race exists with no points distribution
|
||||
// When: GetRacePointsUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default points distribution
|
||||
// And: EventPublisher should emit RacePointsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRacePointsUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRacePointsUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRacePointsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceHighlightsUseCase - Success Path', () => {
|
||||
it('should retrieve race highlights', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with highlights
|
||||
// Given: A race exists with highlights
|
||||
// When: GetRaceHighlightsUseCase.execute() is called with race ID
|
||||
// Then: The result should show highlights
|
||||
// And: EventPublisher should emit RaceHighlightsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race video link', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with video link
|
||||
// Given: A race exists with video link
|
||||
// When: GetRaceHighlightsUseCase.execute() is called with race ID
|
||||
// Then: The result should show video link
|
||||
// And: EventPublisher should emit RaceHighlightsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race gallery', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with gallery
|
||||
// Given: A race exists with gallery
|
||||
// When: GetRaceHighlightsUseCase.execute() is called with race ID
|
||||
// Then: The result should show gallery
|
||||
// And: EventPublisher should emit RaceHighlightsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race highlights with empty results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no highlights
|
||||
// Given: A race exists with no highlights
|
||||
// When: GetRaceHighlightsUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default highlights
|
||||
// And: EventPublisher should emit RaceHighlightsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceHighlightsUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceHighlightsUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRaceHighlightsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Race Detail Page Data Orchestration', () => {
|
||||
it('should correctly orchestrate data for race detail page', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race detail page data orchestration
|
||||
// Given: A race exists with all information
|
||||
// When: Multiple use cases are executed for the same race
|
||||
// Then: Each use case should return its respective data
|
||||
// And: EventPublisher should emit appropriate events for each use case
|
||||
});
|
||||
|
||||
it('should correctly format race information for display', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race information formatting
|
||||
// Given: A race exists with all information
|
||||
// When: GetRaceDetailUseCase.execute() is called
|
||||
// Then: The result should format:
|
||||
// - Track name: Clearly displayed
|
||||
// - Car: Clearly displayed
|
||||
// - League: Clearly displayed
|
||||
// - Date: Formatted correctly
|
||||
// - Time: Formatted correctly
|
||||
// - Duration: Formatted correctly
|
||||
// - Status: Clearly indicated (Upcoming, In Progress, Completed)
|
||||
});
|
||||
|
||||
it('should correctly handle race status transitions', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race status transitions
|
||||
// Given: A race exists with status "Upcoming"
|
||||
// When: Race status changes to "In Progress"
|
||||
// And: GetRaceDetailUseCase.execute() is called
|
||||
// Then: The result should show the updated status
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle race with no statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no statistics
|
||||
// Given: A race exists with no statistics
|
||||
// When: GetRaceStatisticsUseCase.execute() is called
|
||||
// Then: The result should show empty or default statistics
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle race with no lap times', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no lap times
|
||||
// Given: A race exists with no lap times
|
||||
// When: GetRaceLapTimesUseCase.execute() is called
|
||||
// Then: The result should show empty or default lap times
|
||||
// And: EventPublisher should emit RaceLapTimesAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle race with no qualifying results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no qualifying results
|
||||
// Given: A race exists with no qualifying results
|
||||
// When: GetRaceQualifyingUseCase.execute() is called
|
||||
// Then: The result should show empty or default qualifying results
|
||||
// And: EventPublisher should emit RaceQualifyingAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle race with no highlights', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no highlights
|
||||
// Given: A race exists with no highlights
|
||||
// When: GetRaceHighlightsUseCase.execute() is called
|
||||
// Then: The result should show empty or default highlights
|
||||
// And: EventPublisher should emit RaceHighlightsAccessedEvent
|
||||
// Then: isUserRegistered should be true
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap().isUserRegistered).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,722 +2,158 @@
|
||||
* Integration Test: Race Results Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of race results page-related Use Cases:
|
||||
* - GetRaceResultsUseCase: Retrieves complete race results (all finishers)
|
||||
* - GetRaceStatisticsUseCase: Retrieves race statistics (fastest lap, average lap time, etc.)
|
||||
* - GetRaceResultsDetailUseCase: Retrieves complete race results (all finishers)
|
||||
* - GetRacePenaltiesUseCase: Retrieves race penalties and incidents
|
||||
* - GetRaceStewardingActionsUseCase: Retrieves race stewarding actions
|
||||
* - GetRacePointsDistributionUseCase: Retrieves race points distribution
|
||||
* - GetRaceChampionshipImplicationsUseCase: Retrieves race championship implications
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Adheres to Clean Architecture:
|
||||
* - Tests Core Use Cases directly
|
||||
* - Uses In-Memory adapters for repositories
|
||||
* - Follows Given/When/Then pattern
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetRaceResultsUseCase } from '../../../core/races/use-cases/GetRaceResultsUseCase';
|
||||
import { GetRaceStatisticsUseCase } from '../../../core/races/use-cases/GetRaceStatisticsUseCase';
|
||||
import { GetRacePenaltiesUseCase } from '../../../core/races/use-cases/GetRacePenaltiesUseCase';
|
||||
import { GetRaceStewardingActionsUseCase } from '../../../core/races/use-cases/GetRaceStewardingActionsUseCase';
|
||||
import { GetRacePointsDistributionUseCase } from '../../../core/races/use-cases/GetRacePointsDistributionUseCase';
|
||||
import { GetRaceChampionshipImplicationsUseCase } from '../../../core/races/use-cases/GetRaceChampionshipImplicationsUseCase';
|
||||
import { RaceResultsQuery } from '../../../core/races/ports/RaceResultsQuery';
|
||||
import { RaceStatisticsQuery } from '../../../core/races/ports/RaceStatisticsQuery';
|
||||
import { RacePenaltiesQuery } from '../../../core/races/ports/RacePenaltiesQuery';
|
||||
import { RaceStewardingActionsQuery } from '../../../core/races/ports/RaceStewardingActionsQuery';
|
||||
import { RacePointsDistributionQuery } from '../../../core/races/ports/RacePointsDistributionQuery';
|
||||
import { RaceChampionshipImplicationsQuery } from '../../../core/races/ports/RaceChampionshipImplicationsQuery';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryResultRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryResultRepository';
|
||||
import { InMemoryPenaltyRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryPenaltyRepository';
|
||||
import { GetRaceResultsDetailUseCase } from '../../../core/racing/application/use-cases/GetRaceResultsDetailUseCase';
|
||||
import { GetRacePenaltiesUseCase } from '../../../core/racing/application/use-cases/GetRacePenaltiesUseCase';
|
||||
import { Race } from '../../../core/racing/domain/entities/Race';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { Driver } from '../../../core/racing/domain/entities/Driver';
|
||||
import { Result as RaceResult } from '../../../core/racing/domain/entities/result/Result';
|
||||
import { Penalty } from '../../../core/racing/domain/entities/penalty/Penalty';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Race Results Use Case Orchestration', () => {
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getRaceResultsUseCase: GetRaceResultsUseCase;
|
||||
let getRaceStatisticsUseCase: GetRaceStatisticsUseCase;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let resultRepository: InMemoryResultRepository;
|
||||
let penaltyRepository: InMemoryPenaltyRepository;
|
||||
let getRaceResultsDetailUseCase: GetRaceResultsDetailUseCase;
|
||||
let getRacePenaltiesUseCase: GetRacePenaltiesUseCase;
|
||||
let getRaceStewardingActionsUseCase: GetRaceStewardingActionsUseCase;
|
||||
let getRacePointsDistributionUseCase: GetRacePointsDistributionUseCase;
|
||||
let getRaceChampionshipImplicationsUseCase: GetRaceChampionshipImplicationsUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// raceRepository = new InMemoryRaceRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getRaceResultsUseCase = new GetRaceResultsUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceStatisticsUseCase = new GetRaceStatisticsUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRacePenaltiesUseCase = new GetRacePenaltiesUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceStewardingActionsUseCase = new GetRaceStewardingActionsUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRacePointsDistributionUseCase = new GetRacePointsDistributionUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceChampionshipImplicationsUseCase = new GetRaceChampionshipImplicationsUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
raceRepository = new InMemoryRaceRepository(mockLogger);
|
||||
leagueRepository = new InMemoryLeagueRepository(mockLogger);
|
||||
driverRepository = new InMemoryDriverRepository(mockLogger);
|
||||
resultRepository = new InMemoryResultRepository(mockLogger, raceRepository);
|
||||
penaltyRepository = new InMemoryPenaltyRepository(mockLogger);
|
||||
|
||||
getRaceResultsDetailUseCase = new GetRaceResultsDetailUseCase(
|
||||
raceRepository,
|
||||
leagueRepository,
|
||||
resultRepository,
|
||||
driverRepository,
|
||||
penaltyRepository
|
||||
);
|
||||
|
||||
getRacePenaltiesUseCase = new GetRacePenaltiesUseCase(
|
||||
penaltyRepository,
|
||||
driverRepository
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// raceRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
beforeEach(async () => {
|
||||
(raceRepository as any).races.clear();
|
||||
leagueRepository.clear();
|
||||
await driverRepository.clear();
|
||||
(resultRepository as any).results.clear();
|
||||
(penaltyRepository as any).penalties.clear();
|
||||
});
|
||||
|
||||
describe('GetRaceResultsUseCase - Success Path', () => {
|
||||
describe('GetRaceResultsDetailUseCase', () => {
|
||||
it('should retrieve complete race results with all finishers', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver views complete race results
|
||||
// Given: A completed race exists with multiple finishers
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain all finishers
|
||||
// And: The list should be ordered by position
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
// Given: A completed race with results
|
||||
const leagueId = 'l1';
|
||||
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
|
||||
await leagueRepository.create(league);
|
||||
|
||||
it('should retrieve race results with race winner', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with winner
|
||||
// Given: A completed race exists with winner
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show race winner
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
const raceId = 'r1';
|
||||
const race = Race.create({
|
||||
id: raceId,
|
||||
leagueId,
|
||||
scheduledAt: new Date(Date.now() - 86400000),
|
||||
track: 'Spa',
|
||||
car: 'GT3',
|
||||
status: 'completed'
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
|
||||
it('should retrieve race results with podium', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with podium
|
||||
// Given: A completed race exists with podium
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show top 3 finishers
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
const driverId = 'd1';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
it('should retrieve race results with driver information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with driver information
|
||||
// Given: A completed race exists with driver information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show driver name, team, car
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
const raceResult = RaceResult.create({
|
||||
id: 'res1',
|
||||
raceId,
|
||||
driverId,
|
||||
position: 1,
|
||||
lapsCompleted: 20,
|
||||
totalTime: 3600,
|
||||
fastestLap: 105,
|
||||
points: 25
|
||||
});
|
||||
await resultRepository.create(raceResult);
|
||||
|
||||
it('should retrieve race results with position information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with position information
|
||||
// Given: A completed race exists with position information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show position, race time, gaps
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
// When: GetRaceResultsDetailUseCase.execute() is called
|
||||
const result = await getRaceResultsDetailUseCase.execute({ raceId });
|
||||
|
||||
it('should retrieve race results with lap information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with lap information
|
||||
// Given: A completed race exists with lap information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show laps completed, fastest lap, average lap time
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race results with points information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with points information
|
||||
// Given: A completed race exists with points information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show points earned
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race results with penalties information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with penalties information
|
||||
// Given: A completed race exists with penalties information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show penalties
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race results with incidents information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with incidents information
|
||||
// Given: A completed race exists with incidents information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show incidents
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race results with stewarding actions information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with stewarding actions information
|
||||
// Given: A completed race exists with stewarding actions information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show stewarding actions
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race results with protests information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with protests information
|
||||
// Given: A completed race exists with protests information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should show protests
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race results with empty results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no results
|
||||
// Given: A race exists with no results
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
// Then: The result should contain race and results
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.race.id).toBe(raceId);
|
||||
expect(data.results).toHaveLength(1);
|
||||
expect(data.results[0].driverId.toString()).toBe(driverId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceResultsUseCase - Edge Cases', () => {
|
||||
it('should handle race with missing driver information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing driver data
|
||||
// Given: A completed race exists with missing driver information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
describe('GetRacePenaltiesUseCase', () => {
|
||||
it('should retrieve race penalties with driver information', async () => {
|
||||
// Given: A race with penalties
|
||||
const raceId = 'r1';
|
||||
const driverId = 'd1';
|
||||
const stewardId = 's1';
|
||||
|
||||
it('should handle race with missing team information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing team data
|
||||
// Given: A completed race exists with missing team information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
const steward = Driver.create({ id: stewardId, iracingId: '200', name: 'Steward', country: 'UK' });
|
||||
await driverRepository.create(steward);
|
||||
|
||||
it('should handle race with missing car information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing car data
|
||||
// Given: A completed race exists with missing car information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
const penalty = Penalty.create({
|
||||
id: 'p1',
|
||||
raceId,
|
||||
driverId,
|
||||
type: 'time',
|
||||
value: 5,
|
||||
reason: 'Track limits',
|
||||
issuedBy: stewardId,
|
||||
status: 'applied'
|
||||
});
|
||||
await penaltyRepository.create(penalty);
|
||||
|
||||
it('should handle race with missing position information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing position data
|
||||
// Given: A completed race exists with missing position information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing lap information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing lap data
|
||||
// Given: A completed race exists with missing lap information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing points information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing points data
|
||||
// Given: A completed race exists with missing points information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing penalties information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing penalties data
|
||||
// Given: A completed race exists with missing penalties information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing incidents information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing incidents data
|
||||
// Given: A completed race exists with missing incidents information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing stewarding actions information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing stewarding actions data
|
||||
// Given: A completed race exists with missing stewarding actions information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing protests information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results with missing protests data
|
||||
// Given: A completed race exists with missing protests information
|
||||
// When: GetRaceResultsUseCase.execute() is called with race ID
|
||||
// Then: The result should contain results with available information
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceResultsUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceResultsUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when race ID is invalid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid race ID
|
||||
// Given: An invalid race ID (e.g., empty string, null, undefined)
|
||||
// When: GetRaceResultsUseCase.execute() is called with invalid race 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 race exists
|
||||
// And: RaceRepository throws an error during query
|
||||
// When: GetRaceResultsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceStatisticsUseCase - Success Path', () => {
|
||||
it('should retrieve race statistics with fastest lap', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with fastest lap
|
||||
// Given: A completed race exists with fastest lap
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show fastest lap
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with average lap time', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with average lap time
|
||||
// Given: A completed race exists with average lap time
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show average lap time
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with total incidents', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with total incidents
|
||||
// Given: A completed race exists with total incidents
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show total incidents
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with total penalties', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with total penalties
|
||||
// Given: A completed race exists with total penalties
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show total penalties
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with total protests', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with total protests
|
||||
// Given: A completed race exists with total protests
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show total protests
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with total stewarding actions', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with total stewarding actions
|
||||
// Given: A completed race exists with total stewarding actions
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show total stewarding actions
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with all metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with all statistics
|
||||
// Given: A completed race exists with all statistics
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show all statistics
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race statistics with empty metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no statistics
|
||||
// Given: A completed race exists with no statistics
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default statistics
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceStatisticsUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceStatisticsUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRaceStatisticsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRacePenaltiesUseCase - Success Path', () => {
|
||||
it('should retrieve race penalties with penalty information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with penalties
|
||||
// Given: A completed race exists with penalties
|
||||
// When: GetRacePenaltiesUseCase.execute() is called with race ID
|
||||
// Then: The result should show penalty information
|
||||
// And: EventPublisher should emit RacePenaltiesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race penalties with incident information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with incidents
|
||||
// Given: A completed race exists with incidents
|
||||
// When: GetRacePenaltiesUseCase.execute() is called with race ID
|
||||
// Then: The result should show incident information
|
||||
// And: EventPublisher should emit RacePenaltiesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race penalties with empty results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no penalties
|
||||
// Given: A completed race exists with no penalties
|
||||
// When: GetRacePenaltiesUseCase.execute() is called with race ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RacePenaltiesAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRacePenaltiesUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRacePenaltiesUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRacePenaltiesUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
const result = await getRacePenaltiesUseCase.execute({ raceId });
|
||||
|
||||
describe('GetRaceStewardingActionsUseCase - Success Path', () => {
|
||||
it('should retrieve race stewarding actions with action information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with stewarding actions
|
||||
// Given: A completed race exists with stewarding actions
|
||||
// When: GetRaceStewardingActionsUseCase.execute() is called with race ID
|
||||
// Then: The result should show stewarding action information
|
||||
// And: EventPublisher should emit RaceStewardingActionsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race stewarding actions with empty results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no stewarding actions
|
||||
// Given: A completed race exists with no stewarding actions
|
||||
// When: GetRaceStewardingActionsUseCase.execute() is called with race ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RaceStewardingActionsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceStewardingActionsUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceStewardingActionsUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRaceStewardingActionsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRacePointsDistributionUseCase - Success Path', () => {
|
||||
it('should retrieve race points distribution', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with points distribution
|
||||
// Given: A completed race exists with points distribution
|
||||
// When: GetRacePointsDistributionUseCase.execute() is called with race ID
|
||||
// Then: The result should show points distribution
|
||||
// And: EventPublisher should emit RacePointsDistributionAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race points distribution with empty results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no points distribution
|
||||
// Given: A completed race exists with no points distribution
|
||||
// When: GetRacePointsDistributionUseCase.execute() is called with race ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RacePointsDistributionAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRacePointsDistributionUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRacePointsDistributionUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRacePointsDistributionUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceChampionshipImplicationsUseCase - Success Path', () => {
|
||||
it('should retrieve race championship implications', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with championship implications
|
||||
// Given: A completed race exists with championship implications
|
||||
// When: GetRaceChampionshipImplicationsUseCase.execute() is called with race ID
|
||||
// Then: The result should show championship implications
|
||||
// And: EventPublisher should emit RaceChampionshipImplicationsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race championship implications with empty results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no championship implications
|
||||
// Given: A completed race exists with no championship implications
|
||||
// When: GetRaceChampionshipImplicationsUseCase.execute() is called with race ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RaceChampionshipImplicationsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceChampionshipImplicationsUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceChampionshipImplicationsUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRaceChampionshipImplicationsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Race Results Page Data Orchestration', () => {
|
||||
it('should correctly orchestrate data for race results page', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results page data orchestration
|
||||
// Given: A completed race exists with all information
|
||||
// When: Multiple use cases are executed for the same race
|
||||
// Then: Each use case should return its respective data
|
||||
// And: EventPublisher should emit appropriate events for each use case
|
||||
});
|
||||
|
||||
it('should correctly format race results for display', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race results formatting
|
||||
// Given: A completed race exists with all information
|
||||
// When: GetRaceResultsUseCase.execute() is called
|
||||
// Then: The result should format:
|
||||
// - Driver name: Clearly displayed
|
||||
// - Team: Clearly displayed
|
||||
// - Car: Clearly displayed
|
||||
// - Position: Clearly displayed
|
||||
// - Race time: Formatted correctly
|
||||
// - Gaps: Formatted correctly
|
||||
// - Laps completed: Clearly displayed
|
||||
// - Points earned: Clearly displayed
|
||||
// - Fastest lap: Formatted correctly
|
||||
// - Average lap time: Formatted correctly
|
||||
// - Penalties: Clearly displayed
|
||||
// - Incidents: Clearly displayed
|
||||
// - Stewarding actions: Clearly displayed
|
||||
// - Protests: Clearly displayed
|
||||
});
|
||||
|
||||
it('should correctly format race statistics for display', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race statistics formatting
|
||||
// Given: A completed race exists with all statistics
|
||||
// When: GetRaceStatisticsUseCase.execute() is called
|
||||
// Then: The result should format:
|
||||
// - Fastest lap: Formatted correctly
|
||||
// - Average lap time: Formatted correctly
|
||||
// - Total incidents: Clearly displayed
|
||||
// - Total penalties: Clearly displayed
|
||||
// - Total protests: Clearly displayed
|
||||
// - Total stewarding actions: Clearly displayed
|
||||
});
|
||||
|
||||
it('should correctly format race penalties for display', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race penalties formatting
|
||||
// Given: A completed race exists with penalties
|
||||
// When: GetRacePenaltiesUseCase.execute() is called
|
||||
// Then: The result should format:
|
||||
// - Penalty ID: Clearly displayed
|
||||
// - Penalty type: Clearly displayed
|
||||
// - Penalty severity: Clearly displayed
|
||||
// - Penalty recipient: Clearly displayed
|
||||
// - Penalty reason: Clearly displayed
|
||||
// - Penalty timestamp: Formatted correctly
|
||||
});
|
||||
|
||||
it('should correctly format race stewarding actions for display', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race stewarding actions formatting
|
||||
// Given: A completed race exists with stewarding actions
|
||||
// When: GetRaceStewardingActionsUseCase.execute() is called
|
||||
// Then: The result should format:
|
||||
// - Stewarding action ID: Clearly displayed
|
||||
// - Stewarding action type: Clearly displayed
|
||||
// - Stewarding action recipient: Clearly displayed
|
||||
// - Stewarding action reason: Clearly displayed
|
||||
// - Stewarding action timestamp: Formatted correctly
|
||||
});
|
||||
|
||||
it('should correctly format race points distribution for display', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race points distribution formatting
|
||||
// Given: A completed race exists with points distribution
|
||||
// When: GetRacePointsDistributionUseCase.execute() is called
|
||||
// Then: The result should format:
|
||||
// - Points distribution: Clearly displayed
|
||||
// - Championship implications: Clearly displayed
|
||||
});
|
||||
|
||||
it('should correctly format race championship implications for display', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race championship implications formatting
|
||||
// Given: A completed race exists with championship implications
|
||||
// When: GetRaceChampionshipImplicationsUseCase.execute() is called
|
||||
// Then: The result should format:
|
||||
// - Championship implications: Clearly displayed
|
||||
// - Points changes: Clearly displayed
|
||||
// - Position changes: Clearly displayed
|
||||
});
|
||||
|
||||
it('should correctly handle race with no results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no results
|
||||
// Given: A race exists with no results
|
||||
// When: GetRaceResultsUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle race with no statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no statistics
|
||||
// Given: A race exists with no statistics
|
||||
// When: GetRaceStatisticsUseCase.execute() is called
|
||||
// Then: The result should show empty or default statistics
|
||||
// And: EventPublisher should emit RaceStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle race with no penalties', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no penalties
|
||||
// Given: A race exists with no penalties
|
||||
// When: GetRacePenaltiesUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RacePenaltiesAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle race with no stewarding actions', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no stewarding actions
|
||||
// Given: A race exists with no stewarding actions
|
||||
// When: GetRaceStewardingActionsUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RaceStewardingActionsAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle race with no points distribution', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no points distribution
|
||||
// Given: A race exists with no points distribution
|
||||
// When: GetRacePointsDistributionUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RacePointsDistributionAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle race with no championship implications', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no championship implications
|
||||
// Given: A race exists with no championship implications
|
||||
// When: GetRaceChampionshipImplicationsUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RaceChampionshipImplicationsAccessedEvent
|
||||
// Then: It should return penalties and drivers
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.penalties).toHaveLength(1);
|
||||
expect(data.drivers.some(d => d.id === driverId)).toBe(true);
|
||||
expect(data.drivers.some(d => d.id === stewardId)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,682 +3,97 @@
|
||||
*
|
||||
* Tests the orchestration logic of all races page-related Use Cases:
|
||||
* - GetAllRacesUseCase: Retrieves comprehensive list of all races
|
||||
* - FilterRacesUseCase: Filters races by league, car, track, date range
|
||||
* - SearchRacesUseCase: Searches races by track name and league name
|
||||
* - SortRacesUseCase: Sorts races by date, league, car
|
||||
* - PaginateRacesUseCase: Paginates race results
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Adheres to Clean Architecture:
|
||||
* - Tests Core Use Cases directly
|
||||
* - Uses In-Memory adapters for repositories
|
||||
* - Follows Given/When/Then pattern
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetAllRacesUseCase } from '../../../core/races/use-cases/GetAllRacesUseCase';
|
||||
import { FilterRacesUseCase } from '../../../core/races/use-cases/FilterRacesUseCase';
|
||||
import { SearchRacesUseCase } from '../../../core/races/use-cases/SearchRacesUseCase';
|
||||
import { SortRacesUseCase } from '../../../core/races/use-cases/SortRacesUseCase';
|
||||
import { PaginateRacesUseCase } from '../../../core/races/use-cases/PaginateRacesUseCase';
|
||||
import { AllRacesQuery } from '../../../core/races/ports/AllRacesQuery';
|
||||
import { RaceFilterCommand } from '../../../core/races/ports/RaceFilterCommand';
|
||||
import { RaceSearchCommand } from '../../../core/races/ports/RaceSearchCommand';
|
||||
import { RaceSortCommand } from '../../../core/races/ports/RaceSortCommand';
|
||||
import { RacePaginationCommand } from '../../../core/races/ports/RacePaginationCommand';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { GetAllRacesUseCase } from '../../../core/racing/application/use-cases/GetAllRacesUseCase';
|
||||
import { Race } from '../../../core/racing/domain/entities/Race';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('All Races Use Case Orchestration', () => {
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let getAllRacesUseCase: GetAllRacesUseCase;
|
||||
let filterRacesUseCase: FilterRacesUseCase;
|
||||
let searchRacesUseCase: SearchRacesUseCase;
|
||||
let sortRacesUseCase: SortRacesUseCase;
|
||||
let paginateRacesUseCase: PaginateRacesUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// raceRepository = new InMemoryRaceRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getAllRacesUseCase = new GetAllRacesUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// filterRacesUseCase = new FilterRacesUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// searchRacesUseCase = new SearchRacesUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// sortRacesUseCase = new SortRacesUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// paginateRacesUseCase = new PaginateRacesUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
raceRepository = new InMemoryRaceRepository(mockLogger);
|
||||
leagueRepository = new InMemoryLeagueRepository(mockLogger);
|
||||
|
||||
getAllRacesUseCase = new GetAllRacesUseCase(
|
||||
raceRepository,
|
||||
leagueRepository,
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// raceRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
beforeEach(async () => {
|
||||
(raceRepository as any).races.clear();
|
||||
leagueRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetAllRacesUseCase - Success Path', () => {
|
||||
describe('GetAllRacesUseCase', () => {
|
||||
it('should retrieve comprehensive list of all races', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver views all races
|
||||
// Given: Multiple races exist with different tracks, cars, leagues, and dates
|
||||
// And: Races include upcoming, in-progress, and completed races
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
// Then: The result should contain all races
|
||||
// And: Each race should display track name, date, car, league, and winner (if completed)
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve all races with complete information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: All races with complete information
|
||||
// Given: Multiple races exist with complete information
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with all available information
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve all races with minimal information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: All races with minimal data
|
||||
// Given: Races exist with basic information only
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve all races when no races exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No races exist
|
||||
// Given: No races exist in the system
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetAllRacesUseCase - Edge Cases', () => {
|
||||
it('should handle races with missing track information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Races with missing track data
|
||||
// Given: Races exist with missing track information
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle races with missing car information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Races with missing car data
|
||||
// Given: Races exist with missing car information
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle races with missing league information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Races with missing league data
|
||||
// Given: Races exist with missing league information
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle races with missing winner information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Races with missing winner data
|
||||
// Given: Races exist with missing winner information
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetAllRacesUseCase - Error Handling', () => {
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('FilterRacesUseCase - Success Path', () => {
|
||||
it('should filter races by league', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter races by league
|
||||
// Given: Multiple races exist across different leagues
|
||||
// When: FilterRacesUseCase.execute() is called with league filter
|
||||
// Then: The result should contain only races from the specified league
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
|
||||
it('should filter races by car', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter races by car
|
||||
// Given: Multiple races exist with different cars
|
||||
// When: FilterRacesUseCase.execute() is called with car filter
|
||||
// Then: The result should contain only races with the specified car
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
|
||||
it('should filter races by track', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter races by track
|
||||
// Given: Multiple races exist at different tracks
|
||||
// When: FilterRacesUseCase.execute() is called with track filter
|
||||
// Then: The result should contain only races at the specified track
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
|
||||
it('should filter races by date range', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter races by date range
|
||||
// Given: Multiple races exist across different dates
|
||||
// When: FilterRacesUseCase.execute() is called with date range
|
||||
// Then: The result should contain only races within the date range
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
|
||||
it('should filter races by multiple criteria', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter races by multiple criteria
|
||||
// Given: Multiple races exist with different attributes
|
||||
// When: FilterRacesUseCase.execute() is called with multiple filters
|
||||
// Then: The result should contain only races matching all criteria
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
|
||||
it('should filter races with empty result when no matches', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter with no matches
|
||||
// Given: Races exist but none match the filter criteria
|
||||
// When: FilterRacesUseCase.execute() is called with filter
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
|
||||
it('should filter races with pagination', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter races with pagination
|
||||
// Given: Many races exist matching filter criteria
|
||||
// When: FilterRacesUseCase.execute() is called with filter and pagination
|
||||
// Then: The result should contain only the specified page of filtered races
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
|
||||
it('should filter races with limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter races with limit
|
||||
// Given: Many races exist matching filter criteria
|
||||
// When: FilterRacesUseCase.execute() is called with filter and limit
|
||||
// Then: The result should contain only the specified number of filtered races
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('FilterRacesUseCase - Edge Cases', () => {
|
||||
it('should handle empty filter criteria', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Empty filter criteria
|
||||
// Given: Races exist
|
||||
// When: FilterRacesUseCase.execute() is called with empty filter
|
||||
// Then: The result should contain all races (no filtering applied)
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
|
||||
it('should handle case-insensitive filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Case-insensitive filtering
|
||||
// Given: Races exist with mixed case names
|
||||
// When: FilterRacesUseCase.execute() is called with different case filter
|
||||
// Then: The result should match regardless of case
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
|
||||
it('should handle partial matches in text filters', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Partial matches in text filters
|
||||
// Given: Races exist with various names
|
||||
// When: FilterRacesUseCase.execute() is called with partial text
|
||||
// Then: The result should include races with partial matches
|
||||
// And: EventPublisher should emit RacesFilteredEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('FilterRacesUseCase - Error Handling', () => {
|
||||
it('should handle invalid filter parameters', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid filter parameters
|
||||
// Given: Invalid filter values (e.g., empty strings, null)
|
||||
// When: FilterRacesUseCase.execute() is called with invalid parameters
|
||||
// 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: RaceRepository throws an error during filter
|
||||
// When: FilterRacesUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchRacesUseCase - Success Path', () => {
|
||||
it('should search races by track name', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search races by track name
|
||||
// Given: Multiple races exist at different tracks
|
||||
// When: SearchRacesUseCase.execute() is called with track name
|
||||
// Then: The result should contain races matching the track name
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
|
||||
it('should search races by league name', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search races by league name
|
||||
// Given: Multiple races exist in different leagues
|
||||
// When: SearchRacesUseCase.execute() is called with league name
|
||||
// Then: The result should contain races matching the league name
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
|
||||
it('should search races with partial matches', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search with partial matches
|
||||
// Given: Races exist with various names
|
||||
// When: SearchRacesUseCase.execute() is called with partial search term
|
||||
// Then: The result should include races with partial matches
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
|
||||
it('should search races case-insensitively', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Case-insensitive search
|
||||
// Given: Races exist with mixed case names
|
||||
// When: SearchRacesUseCase.execute() is called with different case search term
|
||||
// Then: The result should match regardless of case
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
|
||||
it('should search races with empty result when no matches', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search with no matches
|
||||
// Given: Races exist but none match the search term
|
||||
// When: SearchRacesUseCase.execute() is called with search term
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
|
||||
it('should search races with pagination', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search races with pagination
|
||||
// Given: Many races exist matching search term
|
||||
// When: SearchRacesUseCase.execute() is called with search term and pagination
|
||||
// Then: The result should contain only the specified page of search results
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
|
||||
it('should search races with limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search races with limit
|
||||
// Given: Many races exist matching search term
|
||||
// When: SearchRacesUseCase.execute() is called with search term and limit
|
||||
// Then: The result should contain only the specified number of search results
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchRacesUseCase - Edge Cases', () => {
|
||||
it('should handle empty search term', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Empty search term
|
||||
// Given: Races exist
|
||||
// When: SearchRacesUseCase.execute() is called with empty search term
|
||||
// Then: The result should contain all races (no search applied)
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
|
||||
it('should handle special characters in search term', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Special characters in search term
|
||||
// Given: Races exist with special characters in names
|
||||
// When: SearchRacesUseCase.execute() is called with special characters
|
||||
// Then: The result should handle special characters appropriately
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
|
||||
it('should handle very long search terms', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Very long search term
|
||||
// Given: Races exist
|
||||
// When: SearchRacesUseCase.execute() is called with very long search term
|
||||
// Then: The result should handle the long term appropriately
|
||||
// And: EventPublisher should emit RacesSearchedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchRacesUseCase - Error Handling', () => {
|
||||
it('should handle invalid search parameters', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid search parameters
|
||||
// Given: Invalid search values (e.g., null, undefined)
|
||||
// When: SearchRacesUseCase.execute() is called with invalid parameters
|
||||
// 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: RaceRepository throws an error during search
|
||||
// When: SearchRacesUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('SortRacesUseCase - Success Path', () => {
|
||||
it('should sort races by date', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sort races by date
|
||||
// Given: Multiple races exist with different dates
|
||||
// When: SortRacesUseCase.execute() is called with date sort
|
||||
// Then: The result should be sorted by date
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
});
|
||||
|
||||
it('should sort races by league', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sort races by league
|
||||
// Given: Multiple races exist with different leagues
|
||||
// When: SortRacesUseCase.execute() is called with league sort
|
||||
// Then: The result should be sorted by league name alphabetically
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
});
|
||||
|
||||
it('should sort races by car', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sort races by car
|
||||
// Given: Multiple races exist with different cars
|
||||
// When: SortRacesUseCase.execute() is called with car sort
|
||||
// Then: The result should be sorted by car name alphabetically
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
});
|
||||
|
||||
it('should sort races in ascending order', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sort races in ascending order
|
||||
// Given: Multiple races exist
|
||||
// When: SortRacesUseCase.execute() is called with ascending sort
|
||||
// Then: The result should be sorted in ascending order
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
const leagueId = 'l1';
|
||||
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
|
||||
await leagueRepository.create(league);
|
||||
|
||||
const race1 = Race.create({
|
||||
id: 'r1',
|
||||
leagueId,
|
||||
scheduledAt: new Date(Date.now() + 86400000),
|
||||
track: 'Spa',
|
||||
car: 'GT3',
|
||||
status: 'scheduled'
|
||||
});
|
||||
const race2 = Race.create({
|
||||
id: 'r2',
|
||||
leagueId,
|
||||
scheduledAt: new Date(Date.now() - 86400000),
|
||||
track: 'Monza',
|
||||
car: 'GT3',
|
||||
status: 'completed'
|
||||
});
|
||||
await raceRepository.create(race1);
|
||||
await raceRepository.create(race2);
|
||||
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
const result = await getAllRacesUseCase.execute({});
|
||||
|
||||
// Then: The result should contain all races and leagues
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.races).toHaveLength(2);
|
||||
expect(data.leagues).toHaveLength(1);
|
||||
expect(data.totalCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should sort races in descending order', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sort races in descending order
|
||||
// Given: Multiple races exist
|
||||
// When: SortRacesUseCase.execute() is called with descending sort
|
||||
// Then: The result should be sorted in descending order
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
});
|
||||
it('should return empty list when no races exist', async () => {
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
const result = await getAllRacesUseCase.execute({});
|
||||
|
||||
it('should sort races with pagination', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sort races with pagination
|
||||
// Given: Many races exist
|
||||
// When: SortRacesUseCase.execute() is called with sort and pagination
|
||||
// Then: The result should contain only the specified page of sorted races
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
});
|
||||
|
||||
it('should sort races with limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sort races with limit
|
||||
// Given: Many races exist
|
||||
// When: SortRacesUseCase.execute() is called with sort and limit
|
||||
// Then: The result should contain only the specified number of sorted races
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('SortRacesUseCase - Edge Cases', () => {
|
||||
it('should handle races with missing sort field', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Races with missing sort field
|
||||
// Given: Races exist with missing sort field values
|
||||
// When: SortRacesUseCase.execute() is called
|
||||
// Then: The result should handle missing values appropriately
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
});
|
||||
|
||||
it('should handle empty race list', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Empty race list
|
||||
// Given: No races exist
|
||||
// When: SortRacesUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
});
|
||||
|
||||
it('should handle single race', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Single race
|
||||
// Given: Only one race exists
|
||||
// When: SortRacesUseCase.execute() is called
|
||||
// Then: The result should contain the single race
|
||||
// And: EventPublisher should emit RacesSortedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('SortRacesUseCase - Error Handling', () => {
|
||||
it('should handle invalid sort parameters', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid sort parameters
|
||||
// Given: Invalid sort field or direction
|
||||
// When: SortRacesUseCase.execute() is called with invalid parameters
|
||||
// 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: RaceRepository throws an error during sort
|
||||
// When: SortRacesUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginateRacesUseCase - Success Path', () => {
|
||||
it('should paginate races with page and pageSize', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Paginate races
|
||||
// Given: Many races exist
|
||||
// When: PaginateRacesUseCase.execute() is called with page and pageSize
|
||||
// Then: The result should contain only the specified page of races
|
||||
// And: EventPublisher should emit RacesPaginatedEvent
|
||||
});
|
||||
|
||||
it('should paginate races with first page', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: First page of races
|
||||
// Given: Many races exist
|
||||
// When: PaginateRacesUseCase.execute() is called with page 1
|
||||
// Then: The result should contain the first page of races
|
||||
// And: EventPublisher should emit RacesPaginatedEvent
|
||||
});
|
||||
|
||||
it('should paginate races with middle page', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Middle page of races
|
||||
// Given: Many races exist
|
||||
// When: PaginateRacesUseCase.execute() is called with middle page number
|
||||
// Then: The result should contain the middle page of races
|
||||
// And: EventPublisher should emit RacesPaginatedEvent
|
||||
});
|
||||
|
||||
it('should paginate races with last page', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Last page of races
|
||||
// Given: Many races exist
|
||||
// When: PaginateRacesUseCase.execute() is called with last page number
|
||||
// Then: The result should contain the last page of races
|
||||
// And: EventPublisher should emit RacesPaginatedEvent
|
||||
});
|
||||
|
||||
it('should paginate races with different page sizes', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Different page sizes
|
||||
// Given: Many races exist
|
||||
// When: PaginateRacesUseCase.execute() is called with different pageSize values
|
||||
// Then: The result should contain the correct number of races per page
|
||||
// And: EventPublisher should emit RacesPaginatedEvent
|
||||
});
|
||||
|
||||
it('should paginate races with empty result when page exceeds total', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Page exceeds total
|
||||
// Given: Races exist
|
||||
// When: PaginateRacesUseCase.execute() is called with page beyond total
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RacesPaginatedEvent
|
||||
});
|
||||
|
||||
it('should paginate races with empty result when no races exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No races exist
|
||||
// Given: No races exist
|
||||
// When: PaginateRacesUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RacesPaginatedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginateRacesUseCase - Edge Cases', () => {
|
||||
it('should handle page 0', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Page 0
|
||||
// Given: Races exist
|
||||
// When: PaginateRacesUseCase.execute() is called with page 0
|
||||
// Then: Should handle appropriately (either throw error or return first page)
|
||||
// And: EventPublisher should emit RacesPaginatedEvent or NOT emit
|
||||
});
|
||||
|
||||
it('should handle very large page size', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Very large page size
|
||||
// Given: Races exist
|
||||
// When: PaginateRacesUseCase.execute() is called with very large pageSize
|
||||
// Then: The result should contain all races or handle appropriately
|
||||
// And: EventPublisher should emit RacesPaginatedEvent
|
||||
});
|
||||
|
||||
it('should handle page size larger than total races', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Page size larger than total
|
||||
// Given: Few races exist
|
||||
// When: PaginateRacesUseCase.execute() is called with pageSize > total
|
||||
// Then: The result should contain all races
|
||||
// And: EventPublisher should emit RacesPaginatedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginateRacesUseCase - Error Handling', () => {
|
||||
it('should handle invalid pagination parameters', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid pagination parameters
|
||||
// Given: Invalid page or pageSize values (negative, null, undefined)
|
||||
// When: PaginateRacesUseCase.execute() is called with invalid parameters
|
||||
// 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: RaceRepository throws an error during pagination
|
||||
// When: PaginateRacesUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('All Races Page Data Orchestration', () => {
|
||||
it('should correctly orchestrate filtering, searching, sorting, and pagination', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Combined operations
|
||||
// Given: Many races exist with various attributes
|
||||
// When: Multiple use cases are executed in sequence
|
||||
// Then: Each use case should work correctly
|
||||
// And: EventPublisher should emit appropriate events for each operation
|
||||
});
|
||||
|
||||
it('should correctly format race information for all races list', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race information formatting
|
||||
// Given: Races exist with all information
|
||||
// When: AllRacesUseCase.execute() is called
|
||||
// Then: The result should format:
|
||||
// - Track name: Clearly displayed
|
||||
// - Date: Formatted correctly
|
||||
// - Car: Clearly displayed
|
||||
// - League: Clearly displayed
|
||||
// - Winner: Clearly displayed (if completed)
|
||||
});
|
||||
|
||||
it('should correctly handle race status in all races list', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race status in all races
|
||||
// Given: Races exist with different statuses (Upcoming, In Progress, Completed)
|
||||
// When: AllRacesUseCase.execute() is called
|
||||
// Then: The result should show appropriate status for each race
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle empty states', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Empty states
|
||||
// Given: No races exist
|
||||
// When: AllRacesUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit AllRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly handle loading states', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Loading states
|
||||
// Given: Races are being loaded
|
||||
// When: AllRacesUseCase.execute() is called
|
||||
// Then: The use case should handle loading state appropriately
|
||||
// And: EventPublisher should emit appropriate events
|
||||
});
|
||||
|
||||
it('should correctly handle error states', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Error states
|
||||
// Given: Repository throws error
|
||||
// When: AllRacesUseCase.execute() is called
|
||||
// Then: The use case should handle error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap().races).toHaveLength(0);
|
||||
expect(result.unwrap().totalCount).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,699 +2,88 @@
|
||||
* Integration Test: Races Main Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of races main page-related Use Cases:
|
||||
* - GetUpcomingRacesUseCase: Retrieves upcoming races for the main page
|
||||
* - GetRecentRaceResultsUseCase: Retrieves recent race results for the main page
|
||||
* - GetRaceDetailUseCase: Retrieves race details for navigation
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
* - GetAllRacesUseCase: Used to retrieve upcoming and recent races
|
||||
*
|
||||
* Adheres to Clean Architecture:
|
||||
* - Tests Core Use Cases directly
|
||||
* - Uses In-Memory adapters for repositories
|
||||
* - Follows Given/When/Then pattern
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/races/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetUpcomingRacesUseCase } from '../../../core/races/use-cases/GetUpcomingRacesUseCase';
|
||||
import { GetRecentRaceResultsUseCase } from '../../../core/races/use-cases/GetRecentRaceResultsUseCase';
|
||||
import { GetRaceDetailUseCase } from '../../../core/races/use-cases/GetRaceDetailUseCase';
|
||||
import { UpcomingRacesQuery } from '../../../core/races/ports/UpcomingRacesQuery';
|
||||
import { RecentRaceResultsQuery } from '../../../core/races/ports/RecentRaceResultsQuery';
|
||||
import { RaceDetailQuery } from '../../../core/races/ports/RaceDetailQuery';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { GetAllRacesUseCase } from '../../../core/racing/application/use-cases/GetAllRacesUseCase';
|
||||
import { Race } from '../../../core/racing/domain/entities/Race';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Races Main Use Case Orchestration', () => {
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getUpcomingRacesUseCase: GetUpcomingRacesUseCase;
|
||||
let getRecentRaceResultsUseCase: GetRecentRaceResultsUseCase;
|
||||
let getRaceDetailUseCase: GetRaceDetailUseCase;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let getAllRacesUseCase: GetAllRacesUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// raceRepository = new InMemoryRaceRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getUpcomingRacesUseCase = new GetUpcomingRacesUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRecentRaceResultsUseCase = new GetRecentRaceResultsUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRaceDetailUseCase = new GetRaceDetailUseCase({
|
||||
// raceRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
raceRepository = new InMemoryRaceRepository(mockLogger);
|
||||
leagueRepository = new InMemoryLeagueRepository(mockLogger);
|
||||
|
||||
getAllRacesUseCase = new GetAllRacesUseCase(
|
||||
raceRepository,
|
||||
leagueRepository,
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// raceRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
beforeEach(async () => {
|
||||
(raceRepository as any).races.clear();
|
||||
leagueRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetUpcomingRacesUseCase - Success Path', () => {
|
||||
it('should retrieve upcoming races with complete information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver views upcoming races
|
||||
// Given: Multiple upcoming races exist with different tracks, cars, and leagues
|
||||
// And: Each race has track name, date, time, car, and league
|
||||
// When: GetUpcomingRacesUseCase.execute() is called
|
||||
// Then: The result should contain all upcoming races
|
||||
// And: Each race should display track name, date, time, car, and league
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve upcoming races sorted by date', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Upcoming races are sorted by date
|
||||
// Given: Multiple upcoming races exist with different dates
|
||||
// When: GetUpcomingRacesUseCase.execute() is called
|
||||
// Then: The result should be sorted by date (earliest first)
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve upcoming races with minimal information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Upcoming races with minimal data
|
||||
// Given: Upcoming races exist with basic information only
|
||||
// When: GetUpcomingRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve upcoming races with league filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter upcoming races by league
|
||||
// Given: Multiple upcoming races exist across different leagues
|
||||
// When: GetUpcomingRacesUseCase.execute() is called with league filter
|
||||
// Then: The result should contain only races from the specified league
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve upcoming races with car filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter upcoming races by car
|
||||
// Given: Multiple upcoming races exist with different cars
|
||||
// When: GetUpcomingRacesUseCase.execute() is called with car filter
|
||||
// Then: The result should contain only races with the specified car
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve upcoming races with track filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter upcoming races by track
|
||||
// Given: Multiple upcoming races exist at different tracks
|
||||
// When: GetUpcomingRacesUseCase.execute() is called with track filter
|
||||
// Then: The result should contain only races at the specified track
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve upcoming races with date range filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter upcoming races by date range
|
||||
// Given: Multiple upcoming races exist across different dates
|
||||
// When: GetUpcomingRacesUseCase.execute() is called with date range
|
||||
// Then: The result should contain only races within the date range
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve upcoming races with pagination', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Paginate upcoming races
|
||||
// Given: Many upcoming races exist (more than page size)
|
||||
// When: GetUpcomingRacesUseCase.execute() is called with pagination
|
||||
// Then: The result should contain only the specified page of races
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve upcoming races with limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Limit upcoming races
|
||||
// Given: Many upcoming races exist
|
||||
// When: GetUpcomingRacesUseCase.execute() is called with limit
|
||||
// Then: The result should contain only the specified number of races
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve upcoming races with empty result when no races exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No upcoming races exist
|
||||
// Given: No upcoming races exist in the system
|
||||
// When: GetUpcomingRacesUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetUpcomingRacesUseCase - Edge Cases', () => {
|
||||
it('should handle races with missing track information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Upcoming races with missing track data
|
||||
// Given: Upcoming races exist with missing track information
|
||||
// When: GetUpcomingRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle races with missing car information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Upcoming races with missing car data
|
||||
// Given: Upcoming races exist with missing car information
|
||||
// When: GetUpcomingRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle races with missing league information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Upcoming races with missing league data
|
||||
// Given: Upcoming races exist with missing league information
|
||||
// When: GetUpcomingRacesUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit UpcomingRacesAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetUpcomingRacesUseCase - Error Handling', () => {
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetUpcomingRacesUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle invalid pagination parameters', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid pagination parameters
|
||||
// Given: Invalid page or pageSize values
|
||||
// When: GetUpcomingRacesUseCase.execute() is called with invalid parameters
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRecentRaceResultsUseCase - Success Path', () => {
|
||||
it('should retrieve recent race results with complete information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver views recent race results
|
||||
// Given: Multiple recent race results exist with different tracks, cars, and leagues
|
||||
// And: Each race has track name, date, winner, car, and league
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: The result should contain all recent race results
|
||||
// And: Each race should display track name, date, winner, car, and league
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve recent race results sorted by date (newest first)', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Recent race results are sorted by date
|
||||
// Given: Multiple recent race results exist with different dates
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: The result should be sorted by date (newest first)
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve recent race results with minimal information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Recent race results with minimal data
|
||||
// Given: Recent race results exist with basic information only
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve recent race results with league filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter recent race results by league
|
||||
// Given: Multiple recent race results exist across different leagues
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called with league filter
|
||||
// Then: The result should contain only races from the specified league
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve recent race results with car filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter recent race results by car
|
||||
// Given: Multiple recent race results exist with different cars
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called with car filter
|
||||
// Then: The result should contain only races with the specified car
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve recent race results with track filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter recent race results by track
|
||||
// Given: Multiple recent race results exist at different tracks
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called with track filter
|
||||
// Then: The result should contain only races at the specified track
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve recent race results with date range filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter recent race results by date range
|
||||
// Given: Multiple recent race results exist across different dates
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called with date range
|
||||
// Then: The result should contain only races within the date range
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve recent race results with pagination', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Paginate recent race results
|
||||
// Given: Many recent race results exist (more than page size)
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called with pagination
|
||||
// Then: The result should contain only the specified page of races
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve recent race results with limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Limit recent race results
|
||||
// Given: Many recent race results exist
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called with limit
|
||||
// Then: The result should contain only the specified number of races
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve recent race results with empty result when no races exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No recent race results exist
|
||||
// Given: No recent race results exist in the system
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRecentRaceResultsUseCase - Edge Cases', () => {
|
||||
it('should handle races with missing winner information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Recent race results with missing winner data
|
||||
// Given: Recent race results exist with missing winner information
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle races with missing track information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Recent race results with missing track data
|
||||
// Given: Recent race results exist with missing track information
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle races with missing car information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Recent race results with missing car data
|
||||
// Given: Recent race results exist with missing car information
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle races with missing league information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Recent race results with missing league data
|
||||
// Given: Recent race results exist with missing league information
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: The result should contain races with available information
|
||||
// And: EventPublisher should emit RecentRaceResultsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRecentRaceResultsUseCase - Error Handling', () => {
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: RaceRepository throws an error during query
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle invalid pagination parameters', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid pagination parameters
|
||||
// Given: Invalid page or pageSize values
|
||||
// When: GetRecentRaceResultsUseCase.execute() is called with invalid parameters
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceDetailUseCase - Success Path', () => {
|
||||
it('should retrieve race detail with complete information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver views race detail
|
||||
// Given: A race exists with complete information
|
||||
// And: The race has track, car, league, date, time, duration, status
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should contain complete race information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with participants count', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with participants count
|
||||
// Given: A race exists with participants
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show participants count
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with winner and podium for completed races', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Completed race with winner and podium
|
||||
// Given: A completed race exists with winner and podium
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show winner and podium
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with track layout', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with track layout
|
||||
// Given: A race exists with track layout
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show track layout
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with weather information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with weather information
|
||||
// Given: A race exists with weather information
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show weather information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with race conditions', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with conditions
|
||||
// Given: A race exists with conditions
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show race conditions
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with statistics
|
||||
// Given: A race exists with statistics (lap count, incidents, penalties, protests, stewarding actions)
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show race statistics
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with lap times', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with lap times
|
||||
// Given: A race exists with lap times (average, fastest, best sectors)
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show lap times
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with qualifying results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with qualifying results
|
||||
// Given: A race exists with qualifying results
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show qualifying results
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with starting grid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with starting grid
|
||||
// Given: A race exists with starting grid
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show starting grid
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with points distribution', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with points distribution
|
||||
// Given: A race exists with points distribution
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show points distribution
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with championship implications', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with championship implications
|
||||
// Given: A race exists with championship implications
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show championship implications
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with highlights', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with highlights
|
||||
// Given: A race exists with highlights
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show highlights
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with video link', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with video link
|
||||
// Given: A race exists with video link
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show video link
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with gallery', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with gallery
|
||||
// Given: A race exists with gallery
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show gallery
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with description', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with description
|
||||
// Given: A race exists with description
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show description
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with rules', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with rules
|
||||
// Given: A race exists with rules
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show rules
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve race detail with requirements', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with requirements
|
||||
// Given: A race exists with requirements
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show requirements
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceDetailUseCase - Edge Cases', () => {
|
||||
it('should handle race with missing track information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with missing track data
|
||||
// Given: A race exists with missing track information
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should contain race with available information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing car information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with missing car data
|
||||
// Given: A race exists with missing car information
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should contain race with available information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with missing league information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with missing league data
|
||||
// Given: A race exists with missing league information
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should contain race with available information
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle upcoming race without winner or podium', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Upcoming race without winner or podium
|
||||
// Given: An upcoming race exists (not completed)
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should not show winner or podium
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no statistics
|
||||
// Given: A race exists with no statistics
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default statistics
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no lap times', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no lap times
|
||||
// Given: A race exists with no lap times
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default lap times
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no qualifying results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no qualifying results
|
||||
// Given: A race exists with no qualifying results
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default qualifying results
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no highlights', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no highlights
|
||||
// Given: A race exists with no highlights
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default highlights
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no video link', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no video link
|
||||
// Given: A race exists with no video link
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default video link
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no gallery', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no gallery
|
||||
// Given: A race exists with no gallery
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default gallery
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no description', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no description
|
||||
// Given: A race exists with no description
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default description
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no rules', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no rules
|
||||
// Given: A race exists with no rules
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default rules
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle race with no requirements', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race with no requirements
|
||||
// Given: A race exists with no requirements
|
||||
// When: GetRaceDetailUseCase.execute() is called with race ID
|
||||
// Then: The result should show empty or default requirements
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRaceDetailUseCase - Error Handling', () => {
|
||||
it('should throw error when race does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent race
|
||||
// Given: No race exists with the given ID
|
||||
// When: GetRaceDetailUseCase.execute() is called with non-existent race ID
|
||||
// Then: Should throw RaceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when race ID is invalid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid race ID
|
||||
// Given: An invalid race ID (e.g., empty string, null, undefined)
|
||||
// When: GetRaceDetailUseCase.execute() is called with invalid race 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 race exists
|
||||
// And: RaceRepository throws an error during query
|
||||
// When: GetRaceDetailUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Races Main Page Data Orchestration', () => {
|
||||
it('should correctly orchestrate data for main races page', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Main races page data orchestration
|
||||
// Given: Multiple upcoming races exist
|
||||
// And: Multiple recent race results exist
|
||||
// When: GetUpcomingRacesUseCase.execute() is called
|
||||
// And: GetRecentRaceResultsUseCase.execute() is called
|
||||
// Then: Both use cases should return their respective data
|
||||
// And: EventPublisher should emit appropriate events for each use case
|
||||
});
|
||||
|
||||
it('should correctly format race information for display', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race information formatting
|
||||
// Given: A race exists with all information
|
||||
// When: GetRaceDetailUseCase.execute() is called
|
||||
// Then: The result should format:
|
||||
// - Track name: Clearly displayed
|
||||
// - Date: Formatted correctly
|
||||
// - Time: Formatted correctly
|
||||
// - Car: Clearly displayed
|
||||
// - League: Clearly displayed
|
||||
// - Status: Clearly indicated (Upcoming, In Progress, Completed)
|
||||
});
|
||||
|
||||
it('should correctly handle race status transitions', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Race status transitions
|
||||
// Given: A race exists with status "Upcoming"
|
||||
// When: Race status changes to "In Progress"
|
||||
// And: GetRaceDetailUseCase.execute() is called
|
||||
// Then: The result should show the updated status
|
||||
// And: EventPublisher should emit RaceDetailAccessedEvent
|
||||
describe('Races Main Page Data', () => {
|
||||
it('should retrieve upcoming and recent races', async () => {
|
||||
// Given: Upcoming and completed races exist
|
||||
const leagueId = 'l1';
|
||||
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
|
||||
await leagueRepository.create(league);
|
||||
|
||||
const upcomingRace = Race.create({
|
||||
id: 'r1',
|
||||
leagueId,
|
||||
scheduledAt: new Date(Date.now() + 86400000),
|
||||
track: 'Spa',
|
||||
car: 'GT3',
|
||||
status: 'scheduled'
|
||||
});
|
||||
const completedRace = Race.create({
|
||||
id: 'r2',
|
||||
leagueId,
|
||||
scheduledAt: new Date(Date.now() - 86400000),
|
||||
track: 'Monza',
|
||||
car: 'GT3',
|
||||
status: 'completed'
|
||||
});
|
||||
await raceRepository.create(upcomingRace);
|
||||
await raceRepository.create(completedRace);
|
||||
|
||||
// When: GetAllRacesUseCase.execute() is called
|
||||
const result = await getAllRacesUseCase.execute({});
|
||||
|
||||
// Then: The result should contain both races
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.races).toHaveLength(2);
|
||||
expect(data.races.some(r => r.status.isScheduled())).toBe(true);
|
||||
expect(data.races.some(r => r.status.isCompleted())).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,359 +1,568 @@
|
||||
/**
|
||||
* Integration Test: Sponsor Billing Use Case Orchestration
|
||||
*
|
||||
*
|
||||
* Tests the orchestration logic of sponsor billing-related Use Cases:
|
||||
* - GetBillingStatisticsUseCase: Retrieves billing statistics
|
||||
* - GetPaymentMethodsUseCase: Retrieves payment methods
|
||||
* - SetDefaultPaymentMethodUseCase: Sets default payment method
|
||||
* - GetInvoicesUseCase: Retrieves invoices
|
||||
* - DownloadInvoiceUseCase: Downloads invoice
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - GetSponsorBillingUseCase: Retrieves sponsor billing information
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemorySponsorRepository } from '../../../adapters/sponsors/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemoryBillingRepository } from '../../../adapters/billing/persistence/inmemory/InMemoryBillingRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetBillingStatisticsUseCase } from '../../../core/sponsors/use-cases/GetBillingStatisticsUseCase';
|
||||
import { GetPaymentMethodsUseCase } from '../../../core/sponsors/use-cases/GetPaymentMethodsUseCase';
|
||||
import { SetDefaultPaymentMethodUseCase } from '../../../core/sponsors/use-cases/SetDefaultPaymentMethodUseCase';
|
||||
import { GetInvoicesUseCase } from '../../../core/sponsors/use-cases/GetInvoicesUseCase';
|
||||
import { DownloadInvoiceUseCase } from '../../../core/sponsors/use-cases/DownloadInvoiceUseCase';
|
||||
import { GetBillingStatisticsQuery } from '../../../core/sponsors/ports/GetBillingStatisticsQuery';
|
||||
import { GetPaymentMethodsQuery } from '../../../core/sponsors/ports/GetPaymentMethodsQuery';
|
||||
import { SetDefaultPaymentMethodCommand } from '../../../core/sponsors/ports/SetDefaultPaymentMethodCommand';
|
||||
import { GetInvoicesQuery } from '../../../core/sponsors/ports/GetInvoicesQuery';
|
||||
import { DownloadInvoiceCommand } from '../../../core/sponsors/ports/DownloadInvoiceCommand';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemorySponsorRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemorySeasonSponsorshipRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
||||
import { InMemoryPaymentRepository } from '../../../adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
|
||||
import { GetSponsorBillingUseCase } from '../../../core/payments/application/use-cases/GetSponsorBillingUseCase';
|
||||
import { Sponsor } from '../../../core/racing/domain/entities/sponsor/Sponsor';
|
||||
import { SeasonSponsorship } from '../../../core/racing/domain/entities/season/SeasonSponsorship';
|
||||
import { Payment, PaymentType, PaymentStatus } from '../../../core/payments/domain/entities/Payment';
|
||||
import { Money } from '../../../core/racing/domain/value-objects/Money';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Sponsor Billing Use Case Orchestration', () => {
|
||||
let sponsorRepository: InMemorySponsorRepository;
|
||||
let billingRepository: InMemoryBillingRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getBillingStatisticsUseCase: GetBillingStatisticsUseCase;
|
||||
let getPaymentMethodsUseCase: GetPaymentMethodsUseCase;
|
||||
let setDefaultPaymentMethodUseCase: SetDefaultPaymentMethodUseCase;
|
||||
let getInvoicesUseCase: GetInvoicesUseCase;
|
||||
let downloadInvoiceUseCase: DownloadInvoiceUseCase;
|
||||
let seasonSponsorshipRepository: InMemorySeasonSponsorshipRepository;
|
||||
let paymentRepository: InMemoryPaymentRepository;
|
||||
let getSponsorBillingUseCase: GetSponsorBillingUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// sponsorRepository = new InMemorySponsorRepository();
|
||||
// billingRepository = new InMemoryBillingRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getBillingStatisticsUseCase = new GetBillingStatisticsUseCase({
|
||||
// sponsorRepository,
|
||||
// billingRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getPaymentMethodsUseCase = new GetPaymentMethodsUseCase({
|
||||
// sponsorRepository,
|
||||
// billingRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// setDefaultPaymentMethodUseCase = new SetDefaultPaymentMethodUseCase({
|
||||
// sponsorRepository,
|
||||
// billingRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getInvoicesUseCase = new GetInvoicesUseCase({
|
||||
// sponsorRepository,
|
||||
// billingRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// downloadInvoiceUseCase = new DownloadInvoiceUseCase({
|
||||
// sponsorRepository,
|
||||
// billingRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
sponsorRepository = new InMemorySponsorRepository(mockLogger);
|
||||
seasonSponsorshipRepository = new InMemorySeasonSponsorshipRepository(mockLogger);
|
||||
paymentRepository = new InMemoryPaymentRepository(mockLogger);
|
||||
|
||||
getSponsorBillingUseCase = new GetSponsorBillingUseCase(
|
||||
paymentRepository,
|
||||
seasonSponsorshipRepository,
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// sponsorRepository.clear();
|
||||
// billingRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
sponsorRepository.clear();
|
||||
seasonSponsorshipRepository.clear();
|
||||
paymentRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetBillingStatisticsUseCase - Success Path', () => {
|
||||
it('should retrieve billing statistics for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with billing data
|
||||
describe('GetSponsorBillingUseCase - Success Path', () => {
|
||||
it('should retrieve billing statistics for a sponsor with paid invoices', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has total spent: $5000
|
||||
// And: The sponsor has pending payments: $1000
|
||||
// And: The sponsor has next payment date: "2024-02-01"
|
||||
// And: The sponsor has monthly average spend: $1250
|
||||
// When: GetBillingStatisticsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show total spent: $5000
|
||||
// And: The result should show pending payments: $1000
|
||||
// And: The result should show next payment date: "2024-02-01"
|
||||
// And: The result should show monthly average spend: $1250
|
||||
// And: EventPublisher should emit BillingStatisticsAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 2 active sponsorships
|
||||
const sponsorship1 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship1);
|
||||
|
||||
const sponsorship2 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-2',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-2',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(500, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship2);
|
||||
|
||||
// And: The sponsor has 3 paid invoices
|
||||
const payment1: Payment = {
|
||||
id: 'payment-1',
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: 1000,
|
||||
platformFee: 100,
|
||||
netAmount: 900,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
status: PaymentStatus.COMPLETED,
|
||||
createdAt: new Date('2025-01-15'),
|
||||
completedAt: new Date('2025-01-15'),
|
||||
};
|
||||
await paymentRepository.create(payment1);
|
||||
|
||||
const payment2: Payment = {
|
||||
id: 'payment-2',
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: 2000,
|
||||
platformFee: 200,
|
||||
netAmount: 1800,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-2',
|
||||
seasonId: 'season-2',
|
||||
status: PaymentStatus.COMPLETED,
|
||||
createdAt: new Date('2025-02-15'),
|
||||
completedAt: new Date('2025-02-15'),
|
||||
};
|
||||
await paymentRepository.create(payment2);
|
||||
|
||||
const payment3: Payment = {
|
||||
id: 'payment-3',
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: 3000,
|
||||
platformFee: 300,
|
||||
netAmount: 2700,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-3',
|
||||
seasonId: 'season-3',
|
||||
status: PaymentStatus.COMPLETED,
|
||||
createdAt: new Date('2025-03-15'),
|
||||
completedAt: new Date('2025-03-15'),
|
||||
};
|
||||
await paymentRepository.create(payment3);
|
||||
|
||||
// When: GetSponsorBillingUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain billing data
|
||||
expect(result.isOk()).toBe(true);
|
||||
const billing = result.unwrap();
|
||||
|
||||
// And: The invoices should contain all 3 paid invoices
|
||||
expect(billing.invoices).toHaveLength(3);
|
||||
expect(billing.invoices[0].status).toBe('paid');
|
||||
expect(billing.invoices[1].status).toBe('paid');
|
||||
expect(billing.invoices[2].status).toBe('paid');
|
||||
|
||||
// And: The stats should show correct total spent
|
||||
// Total spent = 1000 + 2000 + 3000 = 6000
|
||||
expect(billing.stats.totalSpent).toBe(6000);
|
||||
|
||||
// And: The stats should show no pending payments
|
||||
expect(billing.stats.pendingAmount).toBe(0);
|
||||
|
||||
// And: The stats should show no next payment date
|
||||
expect(billing.stats.nextPaymentDate).toBeNull();
|
||||
expect(billing.stats.nextPaymentAmount).toBeNull();
|
||||
|
||||
// And: The stats should show correct active sponsorships
|
||||
expect(billing.stats.activeSponsorships).toBe(2);
|
||||
|
||||
// And: The stats should show correct average monthly spend
|
||||
// Average monthly spend = total / months = 6000 / 3 = 2000
|
||||
expect(billing.stats.averageMonthlySpend).toBe(2000);
|
||||
});
|
||||
|
||||
it('should retrieve statistics with zero values', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no billing data
|
||||
it('should retrieve billing statistics with pending invoices', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has no billing history
|
||||
// When: GetBillingStatisticsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show total spent: $0
|
||||
// And: The result should show pending payments: $0
|
||||
// And: The result should show next payment date: null
|
||||
// And: The result should show monthly average spend: $0
|
||||
// And: EventPublisher should emit BillingStatisticsAccessedEvent
|
||||
});
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
describe('GetBillingStatisticsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetBillingStatisticsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// And: The sponsor has 1 active sponsorship
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
// And: The sponsor has 1 paid invoice and 1 pending invoice
|
||||
const payment1: Payment = {
|
||||
id: 'payment-1',
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: 1000,
|
||||
platformFee: 100,
|
||||
netAmount: 900,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
status: PaymentStatus.COMPLETED,
|
||||
createdAt: new Date('2025-01-15'),
|
||||
completedAt: new Date('2025-01-15'),
|
||||
};
|
||||
await paymentRepository.create(payment1);
|
||||
|
||||
const payment2: Payment = {
|
||||
id: 'payment-2',
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: 500,
|
||||
platformFee: 50,
|
||||
netAmount: 450,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-2',
|
||||
seasonId: 'season-2',
|
||||
status: PaymentStatus.PENDING,
|
||||
createdAt: new Date('2025-02-15'),
|
||||
};
|
||||
await paymentRepository.create(payment2);
|
||||
|
||||
// When: GetSponsorBillingUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain billing data
|
||||
expect(result.isOk()).toBe(true);
|
||||
const billing = result.unwrap();
|
||||
|
||||
// And: The invoices should contain both invoices
|
||||
expect(billing.invoices).toHaveLength(2);
|
||||
|
||||
// And: The stats should show correct total spent (only paid invoices)
|
||||
expect(billing.stats.totalSpent).toBe(1000);
|
||||
|
||||
// And: The stats should show correct pending amount
|
||||
expect(billing.stats.pendingAmount).toBe(550); // 500 + 50
|
||||
|
||||
// And: The stats should show next payment date
|
||||
expect(billing.stats.nextPaymentDate).toBeDefined();
|
||||
expect(billing.stats.nextPaymentAmount).toBe(550);
|
||||
|
||||
// And: The stats should show correct active sponsorships
|
||||
expect(billing.stats.activeSponsorships).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw error when sponsor ID is invalid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid sponsor ID
|
||||
// Given: An invalid sponsor ID (e.g., empty string, null, undefined)
|
||||
// When: GetBillingStatisticsUseCase.execute() is called with invalid sponsor ID
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetPaymentMethodsUseCase - Success Path', () => {
|
||||
it('should retrieve payment methods for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with multiple payment methods
|
||||
it('should retrieve billing statistics with zero values when no invoices exist', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 3 payment methods (1 default, 2 non-default)
|
||||
// When: GetPaymentMethodsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should contain all 3 payment methods
|
||||
// And: Each payment method should display its details
|
||||
// And: The default payment method should be marked
|
||||
// And: EventPublisher should emit PaymentMethodsAccessedEvent
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
it('should retrieve payment methods with minimal data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with single payment method
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 1 payment method (default)
|
||||
// When: GetPaymentMethodsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should contain the single payment method
|
||||
// And: EventPublisher should emit PaymentMethodsAccessedEvent
|
||||
});
|
||||
// And: The sponsor has 1 active sponsorship
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
it('should retrieve payment methods with empty result', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no payment methods
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has no payment methods
|
||||
// When: GetPaymentMethodsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit PaymentMethodsAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetPaymentMethodsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetPaymentMethodsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('SetDefaultPaymentMethodUseCase - Success Path', () => {
|
||||
it('should set default payment method for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Set default payment method
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 3 payment methods (1 default, 2 non-default)
|
||||
// When: SetDefaultPaymentMethodUseCase.execute() is called with payment method ID
|
||||
// Then: The payment method should become default
|
||||
// And: The previous default should no longer be default
|
||||
// And: EventPublisher should emit PaymentMethodUpdatedEvent
|
||||
});
|
||||
|
||||
it('should set default payment method when no default exists', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Set default when none exists
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 2 payment methods (no default)
|
||||
// When: SetDefaultPaymentMethodUseCase.execute() is called with payment method ID
|
||||
// Then: The payment method should become default
|
||||
// And: EventPublisher should emit PaymentMethodUpdatedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('SetDefaultPaymentMethodUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: SetDefaultPaymentMethodUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when payment method does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent payment method
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 2 payment methods
|
||||
// When: SetDefaultPaymentMethodUseCase.execute() is called with non-existent payment method ID
|
||||
// Then: Should throw PaymentMethodNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when payment method does not belong to sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Payment method belongs to different sponsor
|
||||
// Given: Sponsor A exists with ID "sponsor-123"
|
||||
// And: Sponsor B exists with ID "sponsor-456"
|
||||
// And: Sponsor B has a payment method with ID "pm-789"
|
||||
// When: SetDefaultPaymentMethodUseCase.execute() is called with sponsor ID "sponsor-123" and payment method ID "pm-789"
|
||||
// Then: Should throw PaymentMethodNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetInvoicesUseCase - Success Path', () => {
|
||||
it('should retrieve invoices for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with multiple invoices
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 5 invoices (2 pending, 2 paid, 1 overdue)
|
||||
// When: GetInvoicesUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should contain all 5 invoices
|
||||
// And: Each invoice should display its details
|
||||
// And: EventPublisher should emit InvoicesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve invoices with minimal data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with single invoice
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 1 invoice
|
||||
// When: GetInvoicesUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should contain the single invoice
|
||||
// And: EventPublisher should emit InvoicesAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve invoices with empty result', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no invoices
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has no invoices
|
||||
// When: GetInvoicesUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit InvoicesAccessedEvent
|
||||
});
|
||||
});
|
||||
// When: GetSponsorBillingUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
describe('GetInvoicesUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetInvoicesUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
// Then: The result should contain billing data
|
||||
expect(result.isOk()).toBe(true);
|
||||
const billing = result.unwrap();
|
||||
|
||||
describe('DownloadInvoiceUseCase - Success Path', () => {
|
||||
it('should download invoice for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Download invoice
|
||||
// And: The invoices should be empty
|
||||
expect(billing.invoices).toHaveLength(0);
|
||||
|
||||
// And: The stats should show zero values
|
||||
expect(billing.stats.totalSpent).toBe(0);
|
||||
expect(billing.stats.pendingAmount).toBe(0);
|
||||
expect(billing.stats.nextPaymentDate).toBeNull();
|
||||
expect(billing.stats.nextPaymentAmount).toBeNull();
|
||||
expect(billing.stats.activeSponsorships).toBe(1);
|
||||
expect(billing.stats.averageMonthlySpend).toBe(0);
|
||||
});
|
||||
|
||||
it('should retrieve billing statistics with mixed invoice statuses', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has an invoice with ID "inv-456"
|
||||
// When: DownloadInvoiceUseCase.execute() is called with invoice ID
|
||||
// Then: The invoice should be downloaded
|
||||
// And: The invoice should be in PDF format
|
||||
// And: EventPublisher should emit InvoiceDownloadedEvent
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
it('should download invoice with correct content', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Download invoice with correct content
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has an invoice with ID "inv-456"
|
||||
// When: DownloadInvoiceUseCase.execute() is called with invoice ID
|
||||
// Then: The downloaded invoice should contain correct invoice number
|
||||
// And: The downloaded invoice should contain correct date
|
||||
// And: The downloaded invoice should contain correct amount
|
||||
// And: EventPublisher should emit InvoiceDownloadedEvent
|
||||
});
|
||||
});
|
||||
// And: The sponsor has 1 active sponsorship
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
describe('DownloadInvoiceUseCase - Error Handling', () => {
|
||||
it('should throw error when invoice does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent invoice
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has no invoice with ID "inv-999"
|
||||
// When: DownloadInvoiceUseCase.execute() is called with non-existent invoice ID
|
||||
// Then: Should throw InvoiceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when invoice does not belong to sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invoice belongs to different sponsor
|
||||
// Given: Sponsor A exists with ID "sponsor-123"
|
||||
// And: Sponsor B exists with ID "sponsor-456"
|
||||
// And: Sponsor B has an invoice with ID "inv-789"
|
||||
// When: DownloadInvoiceUseCase.execute() is called with invoice ID "inv-789"
|
||||
// Then: Should throw InvoiceNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Billing Data Orchestration', () => {
|
||||
it('should correctly aggregate billing statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Billing statistics aggregation
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 3 invoices with amounts: $1000, $2000, $3000
|
||||
// And: The sponsor has 1 pending invoice with amount: $500
|
||||
// When: GetBillingStatisticsUseCase.execute() is called
|
||||
// Then: Total spent should be $6000
|
||||
// And: Pending payments should be $500
|
||||
// And: EventPublisher should emit BillingStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly set default payment method', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Set default payment method
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 3 payment methods
|
||||
// When: SetDefaultPaymentMethodUseCase.execute() is called
|
||||
// Then: Only one payment method should be default
|
||||
// And: The default payment method should be marked correctly
|
||||
// And: EventPublisher should emit PaymentMethodUpdatedEvent
|
||||
});
|
||||
|
||||
it('should correctly retrieve invoices with status', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invoice status retrieval
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has invoices with different statuses
|
||||
// When: GetInvoicesUseCase.execute() is called
|
||||
// Then: Each invoice should have correct status
|
||||
// And: Pending invoices should be highlighted
|
||||
// And: Overdue invoices should show warning
|
||||
// And: EventPublisher should emit InvoicesAccessedEvent
|
||||
const payment1: Payment = {
|
||||
id: 'payment-1',
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: 1000,
|
||||
platformFee: 100,
|
||||
netAmount: 900,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
status: PaymentStatus.COMPLETED,
|
||||
createdAt: new Date('2025-01-15'),
|
||||
completedAt: new Date('2025-01-15'),
|
||||
};
|
||||
await paymentRepository.create(payment1);
|
||||
|
||||
const payment2: Payment = {
|
||||
id: 'payment-2',
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: 500,
|
||||
platformFee: 50,
|
||||
netAmount: 450,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-2',
|
||||
seasonId: 'season-2',
|
||||
status: PaymentStatus.PENDING,
|
||||
createdAt: new Date('2025-02-15'),
|
||||
};
|
||||
await paymentRepository.create(payment2);
|
||||
|
||||
const payment3: Payment = {
|
||||
id: 'payment-3',
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: 300,
|
||||
platformFee: 30,
|
||||
netAmount: 270,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-3',
|
||||
seasonId: 'season-3',
|
||||
status: PaymentStatus.FAILED,
|
||||
createdAt: new Date('2025-03-15'),
|
||||
};
|
||||
await paymentRepository.create(payment3);
|
||||
|
||||
// When: GetSponsorBillingUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain billing data
|
||||
expect(result.isOk()).toBe(true);
|
||||
const billing = result.unwrap();
|
||||
|
||||
// And: The invoices should contain all 3 invoices
|
||||
expect(billing.invoices).toHaveLength(3);
|
||||
|
||||
// And: The stats should show correct total spent (only paid invoices)
|
||||
expect(billing.stats.totalSpent).toBe(1000);
|
||||
|
||||
// And: The stats should show correct pending amount (pending + failed)
|
||||
expect(billing.stats.pendingAmount).toBe(550); // 500 + 50
|
||||
|
||||
// And: The stats should show correct active sponsorships
|
||||
expect(billing.stats.activeSponsorships).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetSponsorBillingUseCase - Error Handling', () => {
|
||||
it('should return error when sponsor does not exist', async () => {
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetSponsorBillingUseCase.execute() is called with non-existent sponsor ID
|
||||
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'non-existent-sponsor' });
|
||||
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('SPONSOR_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sponsor Billing Data Orchestration', () => {
|
||||
it('should correctly aggregate billing statistics across multiple invoices', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 1 active sponsorship
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
// And: The sponsor has 5 invoices with different amounts and statuses
|
||||
const invoices = [
|
||||
{ id: 'payment-1', amount: 1000, status: PaymentStatus.COMPLETED, date: new Date('2025-01-15') },
|
||||
{ id: 'payment-2', amount: 2000, status: PaymentStatus.COMPLETED, date: new Date('2025-02-15') },
|
||||
{ id: 'payment-3', amount: 1500, status: PaymentStatus.PENDING, date: new Date('2025-03-15') },
|
||||
{ id: 'payment-4', amount: 3000, status: PaymentStatus.COMPLETED, date: new Date('2025-04-15') },
|
||||
{ id: 'payment-5', amount: 500, status: PaymentStatus.FAILED, date: new Date('2025-05-15') },
|
||||
];
|
||||
|
||||
for (const invoice of invoices) {
|
||||
const payment: Payment = {
|
||||
id: invoice.id,
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: invoice.amount,
|
||||
platformFee: invoice.amount * 0.1,
|
||||
netAmount: invoice.amount * 0.9,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
status: invoice.status,
|
||||
createdAt: invoice.date,
|
||||
completedAt: invoice.status === PaymentStatus.COMPLETED ? invoice.date : undefined,
|
||||
};
|
||||
await paymentRepository.create(payment);
|
||||
}
|
||||
|
||||
// When: GetSponsorBillingUseCase.execute() is called
|
||||
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The billing statistics should be correctly aggregated
|
||||
expect(result.isOk()).toBe(true);
|
||||
const billing = result.unwrap();
|
||||
|
||||
// Total spent = 1000 + 2000 + 3000 = 6000
|
||||
expect(billing.stats.totalSpent).toBe(6000);
|
||||
|
||||
// Pending amount = 1500 + 500 = 2000
|
||||
expect(billing.stats.pendingAmount).toBe(2000);
|
||||
|
||||
// Average monthly spend = 6000 / 5 = 1200
|
||||
expect(billing.stats.averageMonthlySpend).toBe(1200);
|
||||
|
||||
// Active sponsorships = 1
|
||||
expect(billing.stats.activeSponsorships).toBe(1);
|
||||
});
|
||||
|
||||
it('should correctly calculate average monthly spend over time', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 1 active sponsorship
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
// And: The sponsor has invoices spanning 6 months
|
||||
const invoices = [
|
||||
{ id: 'payment-1', amount: 1000, date: new Date('2025-01-15') },
|
||||
{ id: 'payment-2', amount: 1500, date: new Date('2025-02-15') },
|
||||
{ id: 'payment-3', amount: 2000, date: new Date('2025-03-15') },
|
||||
{ id: 'payment-4', amount: 2500, date: new Date('2025-04-15') },
|
||||
{ id: 'payment-5', amount: 3000, date: new Date('2025-05-15') },
|
||||
{ id: 'payment-6', amount: 3500, date: new Date('2025-06-15') },
|
||||
];
|
||||
|
||||
for (const invoice of invoices) {
|
||||
const payment: Payment = {
|
||||
id: invoice.id,
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: invoice.amount,
|
||||
platformFee: invoice.amount * 0.1,
|
||||
netAmount: invoice.amount * 0.9,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
status: PaymentStatus.COMPLETED,
|
||||
createdAt: invoice.date,
|
||||
completedAt: invoice.date,
|
||||
};
|
||||
await paymentRepository.create(payment);
|
||||
}
|
||||
|
||||
// When: GetSponsorBillingUseCase.execute() is called
|
||||
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The average monthly spend should be calculated correctly
|
||||
expect(result.isOk()).toBe(true);
|
||||
const billing = result.unwrap();
|
||||
|
||||
// Total = 1000 + 1500 + 2000 + 2500 + 3000 + 3500 = 13500
|
||||
// Months = 6 (Jan to Jun)
|
||||
// Average = 13500 / 6 = 2250
|
||||
expect(billing.stats.averageMonthlySpend).toBe(2250);
|
||||
});
|
||||
|
||||
it('should correctly identify next payment date from pending invoices', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 1 active sponsorship
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
// And: The sponsor has multiple pending invoices with different due dates
|
||||
const invoices = [
|
||||
{ id: 'payment-1', amount: 500, date: new Date('2025-03-15') },
|
||||
{ id: 'payment-2', amount: 1000, date: new Date('2025-02-15') },
|
||||
{ id: 'payment-3', amount: 750, date: new Date('2025-01-15') },
|
||||
];
|
||||
|
||||
for (const invoice of invoices) {
|
||||
const payment: Payment = {
|
||||
id: invoice.id,
|
||||
type: PaymentType.SPONSORSHIP,
|
||||
amount: invoice.amount,
|
||||
platformFee: invoice.amount * 0.1,
|
||||
netAmount: invoice.amount * 0.9,
|
||||
payerId: 'sponsor-123',
|
||||
payerType: 'sponsor',
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
status: PaymentStatus.PENDING,
|
||||
createdAt: invoice.date,
|
||||
};
|
||||
await paymentRepository.create(payment);
|
||||
}
|
||||
|
||||
// When: GetSponsorBillingUseCase.execute() is called
|
||||
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The next payment should be the earliest pending invoice
|
||||
expect(result.isOk()).toBe(true);
|
||||
const billing = result.unwrap();
|
||||
|
||||
// Next payment should be from payment-3 (earliest date)
|
||||
expect(billing.stats.nextPaymentDate).toBe('2025-01-15T00:00:00.000Z');
|
||||
expect(billing.stats.nextPaymentAmount).toBe(825); // 750 + 75
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,346 +1,658 @@
|
||||
/**
|
||||
* Integration Test: Sponsor Campaigns Use Case Orchestration
|
||||
*
|
||||
*
|
||||
* Tests the orchestration logic of sponsor campaigns-related Use Cases:
|
||||
* - GetSponsorCampaignsUseCase: Retrieves sponsor's campaigns
|
||||
* - GetCampaignStatisticsUseCase: Retrieves campaign statistics
|
||||
* - FilterCampaignsUseCase: Filters campaigns by status
|
||||
* - SearchCampaignsUseCase: Searches campaigns by query
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - GetSponsorSponsorshipsUseCase: Retrieves sponsor's sponsorships/campaigns
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemorySponsorRepository } from '../../../adapters/sponsors/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemoryCampaignRepository } from '../../../adapters/sponsors/persistence/inmemory/InMemoryCampaignRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetSponsorCampaignsUseCase } from '../../../core/sponsors/use-cases/GetSponsorCampaignsUseCase';
|
||||
import { GetCampaignStatisticsUseCase } from '../../../core/sponsors/use-cases/GetCampaignStatisticsUseCase';
|
||||
import { FilterCampaignsUseCase } from '../../../core/sponsors/use-cases/FilterCampaignsUseCase';
|
||||
import { SearchCampaignsUseCase } from '../../../core/sponsors/use-cases/SearchCampaignsUseCase';
|
||||
import { GetSponsorCampaignsQuery } from '../../../core/sponsors/ports/GetSponsorCampaignsQuery';
|
||||
import { GetCampaignStatisticsQuery } from '../../../core/sponsors/ports/GetCampaignStatisticsQuery';
|
||||
import { FilterCampaignsCommand } from '../../../core/sponsors/ports/FilterCampaignsCommand';
|
||||
import { SearchCampaignsCommand } from '../../../core/sponsors/ports/SearchCampaignsCommand';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemorySponsorRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemorySeasonSponsorshipRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
||||
import { InMemorySeasonRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { GetSponsorSponsorshipsUseCase } from '../../../core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
|
||||
import { Sponsor } from '../../../core/racing/domain/entities/sponsor/Sponsor';
|
||||
import { SeasonSponsorship } from '../../../core/racing/domain/entities/season/SeasonSponsorship';
|
||||
import { Season } from '../../../core/racing/domain/entities/season/Season';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { LeagueMembership } from '../../../core/racing/domain/entities/LeagueMembership';
|
||||
import { Race } from '../../../core/racing/domain/entities/Race';
|
||||
import { Money } from '../../../core/racing/domain/value-objects/Money';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Sponsor Campaigns Use Case Orchestration', () => {
|
||||
let sponsorRepository: InMemorySponsorRepository;
|
||||
let campaignRepository: InMemoryCampaignRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getSponsorCampaignsUseCase: GetSponsorCampaignsUseCase;
|
||||
let getCampaignStatisticsUseCase: GetCampaignStatisticsUseCase;
|
||||
let filterCampaignsUseCase: FilterCampaignsUseCase;
|
||||
let searchCampaignsUseCase: SearchCampaignsUseCase;
|
||||
let seasonSponsorshipRepository: InMemorySeasonSponsorshipRepository;
|
||||
let seasonRepository: InMemorySeasonRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let leagueMembershipRepository: InMemoryLeagueMembershipRepository;
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let getSponsorSponsorshipsUseCase: GetSponsorSponsorshipsUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// sponsorRepository = new InMemorySponsorRepository();
|
||||
// campaignRepository = new InMemoryCampaignRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getSponsorCampaignsUseCase = new GetSponsorCampaignsUseCase({
|
||||
// sponsorRepository,
|
||||
// campaignRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getCampaignStatisticsUseCase = new GetCampaignStatisticsUseCase({
|
||||
// sponsorRepository,
|
||||
// campaignRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// filterCampaignsUseCase = new FilterCampaignsUseCase({
|
||||
// sponsorRepository,
|
||||
// campaignRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// searchCampaignsUseCase = new SearchCampaignsUseCase({
|
||||
// sponsorRepository,
|
||||
// campaignRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
sponsorRepository = new InMemorySponsorRepository(mockLogger);
|
||||
seasonSponsorshipRepository = new InMemorySeasonSponsorshipRepository(mockLogger);
|
||||
seasonRepository = new InMemorySeasonRepository(mockLogger);
|
||||
leagueRepository = new InMemoryLeagueRepository(mockLogger);
|
||||
leagueMembershipRepository = new InMemoryLeagueMembershipRepository(mockLogger);
|
||||
raceRepository = new InMemoryRaceRepository(mockLogger);
|
||||
|
||||
getSponsorSponsorshipsUseCase = new GetSponsorSponsorshipsUseCase(
|
||||
sponsorRepository,
|
||||
seasonSponsorshipRepository,
|
||||
seasonRepository,
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRepository,
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// sponsorRepository.clear();
|
||||
// campaignRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
sponsorRepository.clear();
|
||||
seasonSponsorshipRepository.clear();
|
||||
seasonRepository.clear();
|
||||
leagueRepository.clear();
|
||||
leagueMembershipRepository.clear();
|
||||
raceRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetSponsorCampaignsUseCase - Success Path', () => {
|
||||
it('should retrieve all campaigns for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with multiple campaigns
|
||||
describe('GetSponsorSponsorshipsUseCase - Success Path', () => {
|
||||
it('should retrieve all sponsorships for a sponsor', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 5 campaigns (2 active, 2 pending, 1 rejected)
|
||||
// When: GetSponsorCampaignsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should contain all 5 campaigns
|
||||
// And: Each campaign should display its details
|
||||
// And: EventPublisher should emit SponsorCampaignsAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 3 sponsorships with different statuses
|
||||
const league1 = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league1);
|
||||
|
||||
const league2 = League.create({
|
||||
id: 'league-2',
|
||||
name: 'League 2',
|
||||
description: 'Description 2',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
await leagueRepository.create(league2);
|
||||
|
||||
const league3 = League.create({
|
||||
id: 'league-3',
|
||||
name: 'League 3',
|
||||
description: 'Description 3',
|
||||
ownerId: 'owner-3',
|
||||
});
|
||||
await leagueRepository.create(league3);
|
||||
|
||||
const season1 = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season1);
|
||||
|
||||
const season2 = Season.create({
|
||||
id: 'season-2',
|
||||
leagueId: 'league-2',
|
||||
name: 'Season 2',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season2);
|
||||
|
||||
const season3 = Season.create({
|
||||
id: 'season-3',
|
||||
leagueId: 'league-3',
|
||||
name: 'Season 3',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season3);
|
||||
|
||||
const sponsorship1 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship1);
|
||||
|
||||
const sponsorship2 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-2',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-2',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(500, 'USD'),
|
||||
status: 'pending',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship2);
|
||||
|
||||
const sponsorship3 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-3',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-3',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(300, 'USD'),
|
||||
status: 'completed',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship3);
|
||||
|
||||
// And: The sponsor has different numbers of drivers and races in each league
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
driverId: `driver-1-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
driverId: `driver-2-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
driverId: `driver-3-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
track: 'Track 1',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
track: 'Track 2',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
track: 'Track 3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain sponsor sponsorships
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
// And: The sponsor name should be correct
|
||||
expect(sponsorships.sponsor.name.toString()).toBe('Test Company');
|
||||
|
||||
// And: The sponsorships should contain all 3 sponsorships
|
||||
expect(sponsorships.sponsorships).toHaveLength(3);
|
||||
|
||||
// And: The summary should show correct values
|
||||
expect(sponsorships.summary.totalSponsorships).toBe(3);
|
||||
expect(sponsorships.summary.activeSponsorships).toBe(1);
|
||||
expect(sponsorships.summary.totalInvestment.amount).toBe(1800); // 1000 + 500 + 300
|
||||
expect(sponsorships.summary.totalPlatformFees.amount).toBe(180); // 100 + 50 + 30
|
||||
|
||||
// And: Each sponsorship should have correct metrics
|
||||
const sponsorship1Summary = sponsorships.sponsorships.find(s => s.sponsorship.id === 'sponsorship-1');
|
||||
expect(sponsorship1Summary).toBeDefined();
|
||||
expect(sponsorship1Summary?.metrics.drivers).toBe(10);
|
||||
expect(sponsorship1Summary?.metrics.races).toBe(5);
|
||||
expect(sponsorship1Summary?.metrics.completedRaces).toBe(5);
|
||||
expect(sponsorship1Summary?.metrics.impressions).toBe(5000); // 5 * 10 * 100
|
||||
});
|
||||
|
||||
it('should retrieve campaigns with minimal data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with minimal campaigns
|
||||
it('should retrieve sponsorships with minimal data', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 1 campaign
|
||||
// When: GetSponsorCampaignsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should contain the single campaign
|
||||
// And: EventPublisher should emit SponsorCampaignsAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 1 sponsorship
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league);
|
||||
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season);
|
||||
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain sponsor sponsorships
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
// And: The sponsorships should contain 1 sponsorship
|
||||
expect(sponsorships.sponsorships).toHaveLength(1);
|
||||
|
||||
// And: The summary should show correct values
|
||||
expect(sponsorships.summary.totalSponsorships).toBe(1);
|
||||
expect(sponsorships.summary.activeSponsorships).toBe(1);
|
||||
expect(sponsorships.summary.totalInvestment.amount).toBe(1000);
|
||||
expect(sponsorships.summary.totalPlatformFees.amount).toBe(100);
|
||||
});
|
||||
|
||||
it('should retrieve campaigns with empty result', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no campaigns
|
||||
it('should retrieve sponsorships with empty result when no sponsorships exist', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has no campaigns
|
||||
// When: GetSponsorCampaignsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit SponsorCampaignsAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has no sponsorships
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain sponsor sponsorships
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
// And: The sponsorships should be empty
|
||||
expect(sponsorships.sponsorships).toHaveLength(0);
|
||||
|
||||
// And: The summary should show zero values
|
||||
expect(sponsorships.summary.totalSponsorships).toBe(0);
|
||||
expect(sponsorships.summary.activeSponsorships).toBe(0);
|
||||
expect(sponsorships.summary.totalInvestment.amount).toBe(0);
|
||||
expect(sponsorships.summary.totalPlatformFees.amount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetSponsorCampaignsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
describe('GetSponsorSponsorshipsUseCase - Error Handling', () => {
|
||||
it('should return error when sponsor does not exist', async () => {
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetSponsorCampaignsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called with non-existent sponsor ID
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'non-existent-sponsor' });
|
||||
|
||||
it('should throw error when sponsor ID is invalid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid sponsor ID
|
||||
// Given: An invalid sponsor ID (e.g., empty string, null, undefined)
|
||||
// When: GetSponsorCampaignsUseCase.execute() is called with invalid sponsor ID
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('SPONSOR_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetCampaignStatisticsUseCase - Success Path', () => {
|
||||
it('should retrieve campaign statistics for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with multiple campaigns
|
||||
describe('Sponsor Campaigns Data Orchestration', () => {
|
||||
it('should correctly aggregate sponsorship metrics across multiple sponsorships', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 5 campaigns (2 active, 2 pending, 1 rejected)
|
||||
// And: The sponsor has total investment of $5000
|
||||
// And: The sponsor has total impressions of 100000
|
||||
// When: GetCampaignStatisticsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show total sponsorships count: 5
|
||||
// And: The result should show active sponsorships count: 2
|
||||
// And: The result should show pending sponsorships count: 2
|
||||
// And: The result should show approved sponsorships count: 2
|
||||
// And: The result should show rejected sponsorships count: 1
|
||||
// And: The result should show total investment: $5000
|
||||
// And: The result should show total impressions: 100000
|
||||
// And: EventPublisher should emit CampaignStatisticsAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 3 sponsorships with different investments
|
||||
const league1 = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league1);
|
||||
|
||||
const league2 = League.create({
|
||||
id: 'league-2',
|
||||
name: 'League 2',
|
||||
description: 'Description 2',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
await leagueRepository.create(league2);
|
||||
|
||||
const league3 = League.create({
|
||||
id: 'league-3',
|
||||
name: 'League 3',
|
||||
description: 'Description 3',
|
||||
ownerId: 'owner-3',
|
||||
});
|
||||
await leagueRepository.create(league3);
|
||||
|
||||
const season1 = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season1);
|
||||
|
||||
const season2 = Season.create({
|
||||
id: 'season-2',
|
||||
leagueId: 'league-2',
|
||||
name: 'Season 2',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season2);
|
||||
|
||||
const season3 = Season.create({
|
||||
id: 'season-3',
|
||||
leagueId: 'league-3',
|
||||
name: 'Season 3',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season3);
|
||||
|
||||
const sponsorship1 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship1);
|
||||
|
||||
const sponsorship2 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-2',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-2',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(2000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship2);
|
||||
|
||||
const sponsorship3 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-3',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-3',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(3000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship3);
|
||||
|
||||
// And: The sponsor has different numbers of drivers and races in each league
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
driverId: `driver-1-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
driverId: `driver-2-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
driverId: `driver-3-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
track: 'Track 1',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
track: 'Track 2',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
track: 'Track 3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The metrics should be correctly aggregated
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
// Total drivers: 10 + 5 + 8 = 23
|
||||
expect(sponsorships.sponsorships[0].metrics.drivers).toBe(10);
|
||||
expect(sponsorships.sponsorships[1].metrics.drivers).toBe(5);
|
||||
expect(sponsorships.sponsorships[2].metrics.drivers).toBe(8);
|
||||
|
||||
// Total races: 5 + 3 + 4 = 12
|
||||
expect(sponsorships.sponsorships[0].metrics.races).toBe(5);
|
||||
expect(sponsorships.sponsorships[1].metrics.races).toBe(3);
|
||||
expect(sponsorships.sponsorships[2].metrics.races).toBe(4);
|
||||
|
||||
// Total investment: 1000 + 2000 + 3000 = 6000
|
||||
expect(sponsorships.summary.totalInvestment.amount).toBe(6000);
|
||||
|
||||
// Total platform fees: 100 + 200 + 300 = 600
|
||||
expect(sponsorships.summary.totalPlatformFees.amount).toBe(600);
|
||||
});
|
||||
|
||||
it('should retrieve statistics with zero values', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no campaigns
|
||||
it('should correctly calculate impressions based on completed races and drivers', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has no campaigns
|
||||
// When: GetCampaignStatisticsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show all counts as 0
|
||||
// And: The result should show total investment as 0
|
||||
// And: The result should show total impressions as 0
|
||||
// And: EventPublisher should emit CampaignStatisticsAccessedEvent
|
||||
});
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
describe('GetCampaignStatisticsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetCampaignStatisticsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
// And: The sponsor has 1 league with 10 drivers and 5 completed races
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league);
|
||||
|
||||
describe('FilterCampaignsUseCase - Success Path', () => {
|
||||
it('should filter campaigns by "All" status', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter by All
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season);
|
||||
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-${i}`,
|
||||
leagueId: 'league-1',
|
||||
driverId: `driver-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-${i}`,
|
||||
leagueId: 'league-1',
|
||||
track: 'Track 1',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: Impressions should be calculated correctly
|
||||
// Impressions = completed races * drivers * 100 = 5 * 10 * 100 = 5000
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
expect(sponsorships.sponsorships[0].metrics.impressions).toBe(5000);
|
||||
});
|
||||
|
||||
it('should correctly calculate platform fees and net amounts', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 5 campaigns (2 active, 2 pending, 1 rejected)
|
||||
// When: FilterCampaignsUseCase.execute() is called with status "All"
|
||||
// Then: The result should contain all 5 campaigns
|
||||
// And: EventPublisher should emit CampaignsFilteredEvent
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
it('should filter campaigns by "Active" status', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter by Active
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 5 campaigns (2 active, 2 pending, 1 rejected)
|
||||
// When: FilterCampaignsUseCase.execute() is called with status "Active"
|
||||
// Then: The result should contain only 2 active campaigns
|
||||
// And: EventPublisher should emit CampaignsFilteredEvent
|
||||
});
|
||||
// And: The sponsor has 1 sponsorship
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league);
|
||||
|
||||
it('should filter campaigns by "Pending" status', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter by Pending
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 5 campaigns (2 active, 2 pending, 1 rejected)
|
||||
// When: FilterCampaignsUseCase.execute() is called with status "Pending"
|
||||
// Then: The result should contain only 2 pending campaigns
|
||||
// And: EventPublisher should emit CampaignsFilteredEvent
|
||||
});
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season);
|
||||
|
||||
it('should filter campaigns by "Approved" status', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter by Approved
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 5 campaigns (2 active, 2 pending, 1 rejected)
|
||||
// When: FilterCampaignsUseCase.execute() is called with status "Approved"
|
||||
// Then: The result should contain only 2 approved campaigns
|
||||
// And: EventPublisher should emit CampaignsFilteredEvent
|
||||
});
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
it('should filter campaigns by "Rejected" status', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter by Rejected
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 5 campaigns (2 active, 2 pending, 1 rejected)
|
||||
// When: FilterCampaignsUseCase.execute() is called with status "Rejected"
|
||||
// Then: The result should contain only 1 rejected campaign
|
||||
// And: EventPublisher should emit CampaignsFilteredEvent
|
||||
});
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
it('should return empty result when no campaigns match filter', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter with no matches
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 2 active campaigns
|
||||
// When: FilterCampaignsUseCase.execute() is called with status "Pending"
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit CampaignsFilteredEvent
|
||||
});
|
||||
});
|
||||
// Then: Platform fees and net amounts should be calculated correctly
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
describe('FilterCampaignsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: FilterCampaignsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
// Platform fee = 10% of pricing = 100
|
||||
expect(sponsorships.sponsorships[0].financials.platformFee.amount).toBe(100);
|
||||
|
||||
it('should throw error with invalid status', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid status
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// When: FilterCampaignsUseCase.execute() is called with invalid status
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchCampaignsUseCase - Success Path', () => {
|
||||
it('should search campaigns by league name', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search by league name
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has campaigns for leagues: "League A", "League B", "League C"
|
||||
// When: SearchCampaignsUseCase.execute() is called with query "League A"
|
||||
// Then: The result should contain only campaigns for "League A"
|
||||
// And: EventPublisher should emit CampaignsSearchedEvent
|
||||
});
|
||||
|
||||
it('should search campaigns by partial match', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search by partial match
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has campaigns for leagues: "Premier League", "League A", "League B"
|
||||
// When: SearchCampaignsUseCase.execute() is called with query "League"
|
||||
// Then: The result should contain campaigns for "Premier League", "League A", "League B"
|
||||
// And: EventPublisher should emit CampaignsSearchedEvent
|
||||
});
|
||||
|
||||
it('should return empty result when no campaigns match search', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search with no matches
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has campaigns for leagues: "League A", "League B"
|
||||
// When: SearchCampaignsUseCase.execute() is called with query "NonExistent"
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit CampaignsSearchedEvent
|
||||
});
|
||||
|
||||
it('should return all campaigns when search query is empty', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search with empty query
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 3 campaigns
|
||||
// When: SearchCampaignsUseCase.execute() is called with empty query
|
||||
// Then: The result should contain all 3 campaigns
|
||||
// And: EventPublisher should emit CampaignsSearchedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchCampaignsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: SearchCampaignsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error with invalid query', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid query
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// When: SearchCampaignsUseCase.execute() is called with invalid query (e.g., null, undefined)
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Campaign Data Orchestration', () => {
|
||||
it('should correctly aggregate campaign statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Campaign statistics aggregation
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 3 campaigns with investments: $1000, $2000, $3000
|
||||
// And: The sponsor has 3 campaigns with impressions: 50000, 30000, 20000
|
||||
// When: GetCampaignStatisticsUseCase.execute() is called
|
||||
// Then: Total investment should be $6000
|
||||
// And: Total impressions should be 100000
|
||||
// And: EventPublisher should emit CampaignStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly filter campaigns by status', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Campaign status filtering
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has campaigns with different statuses
|
||||
// When: FilterCampaignsUseCase.execute() is called with "Active"
|
||||
// Then: Only active campaigns should be returned
|
||||
// And: Each campaign should have correct status
|
||||
// And: EventPublisher should emit CampaignsFilteredEvent
|
||||
});
|
||||
|
||||
it('should correctly search campaigns by league name', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Campaign league name search
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has campaigns for different leagues
|
||||
// When: SearchCampaignsUseCase.execute() is called with league name
|
||||
// Then: Only campaigns for matching leagues should be returned
|
||||
// And: Each campaign should have correct league name
|
||||
// And: EventPublisher should emit CampaignsSearchedEvent
|
||||
// Net amount = pricing - platform fee = 1000 - 100 = 900
|
||||
expect(sponsorships.sponsorships[0].financials.netAmount.amount).toBe(900);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,273 +1,709 @@
|
||||
/**
|
||||
* Integration Test: Sponsor Dashboard Use Case Orchestration
|
||||
*
|
||||
*
|
||||
* Tests the orchestration logic of sponsor dashboard-related Use Cases:
|
||||
* - GetDashboardOverviewUseCase: Retrieves dashboard overview
|
||||
* - GetDashboardMetricsUseCase: Retrieves dashboard metrics
|
||||
* - GetRecentActivityUseCase: Retrieves recent activity
|
||||
* - GetPendingActionsUseCase: Retrieves pending actions
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - GetSponsorDashboardUseCase: Retrieves sponsor dashboard metrics
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemorySponsorRepository } from '../../../adapters/sponsors/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemoryCampaignRepository } from '../../../adapters/sponsors/persistence/inmemory/InMemoryCampaignRepository';
|
||||
import { InMemoryBillingRepository } from '../../../adapters/billing/persistence/inmemory/InMemoryBillingRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetDashboardOverviewUseCase } from '../../../core/sponsors/use-cases/GetDashboardOverviewUseCase';
|
||||
import { GetDashboardMetricsUseCase } from '../../../core/sponsors/use-cases/GetDashboardMetricsUseCase';
|
||||
import { GetRecentActivityUseCase } from '../../../core/sponsors/use-cases/GetRecentActivityUseCase';
|
||||
import { GetPendingActionsUseCase } from '../../../core/sponsors/use-cases/GetPendingActionsUseCase';
|
||||
import { GetDashboardOverviewQuery } from '../../../core/sponsors/ports/GetDashboardOverviewQuery';
|
||||
import { GetDashboardMetricsQuery } from '../../../core/sponsors/ports/GetDashboardMetricsQuery';
|
||||
import { GetRecentActivityQuery } from '../../../core/sponsors/ports/GetRecentActivityQuery';
|
||||
import { GetPendingActionsQuery } from '../../../core/sponsors/ports/GetPendingActionsQuery';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemorySponsorRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemorySeasonSponsorshipRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
||||
import { InMemorySeasonRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { GetSponsorDashboardUseCase } from '../../../core/racing/application/use-cases/GetSponsorDashboardUseCase';
|
||||
import { Sponsor } from '../../../core/racing/domain/entities/sponsor/Sponsor';
|
||||
import { SeasonSponsorship } from '../../../core/racing/domain/entities/season/SeasonSponsorship';
|
||||
import { Season } from '../../../core/racing/domain/entities/season/Season';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { LeagueMembership } from '../../../core/racing/domain/entities/LeagueMembership';
|
||||
import { Race } from '../../../core/racing/domain/entities/Race';
|
||||
import { Money } from '../../../core/racing/domain/value-objects/Money';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Sponsor Dashboard Use Case Orchestration', () => {
|
||||
let sponsorRepository: InMemorySponsorRepository;
|
||||
let campaignRepository: InMemoryCampaignRepository;
|
||||
let billingRepository: InMemoryBillingRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getDashboardOverviewUseCase: GetDashboardOverviewUseCase;
|
||||
let getDashboardMetricsUseCase: GetDashboardMetricsUseCase;
|
||||
let getRecentActivityUseCase: GetRecentActivityUseCase;
|
||||
let getPendingActionsUseCase: GetPendingActionsUseCase;
|
||||
let seasonSponsorshipRepository: InMemorySeasonSponsorshipRepository;
|
||||
let seasonRepository: InMemorySeasonRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let leagueMembershipRepository: InMemoryLeagueMembershipRepository;
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let getSponsorDashboardUseCase: GetSponsorDashboardUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// sponsorRepository = new InMemorySponsorRepository();
|
||||
// campaignRepository = new InMemoryCampaignRepository();
|
||||
// billingRepository = new InMemoryBillingRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getDashboardOverviewUseCase = new GetDashboardOverviewUseCase({
|
||||
// sponsorRepository,
|
||||
// campaignRepository,
|
||||
// billingRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getDashboardMetricsUseCase = new GetDashboardMetricsUseCase({
|
||||
// sponsorRepository,
|
||||
// campaignRepository,
|
||||
// billingRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getRecentActivityUseCase = new GetRecentActivityUseCase({
|
||||
// sponsorRepository,
|
||||
// campaignRepository,
|
||||
// billingRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getPendingActionsUseCase = new GetPendingActionsUseCase({
|
||||
// sponsorRepository,
|
||||
// campaignRepository,
|
||||
// billingRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
sponsorRepository = new InMemorySponsorRepository(mockLogger);
|
||||
seasonSponsorshipRepository = new InMemorySeasonSponsorshipRepository(mockLogger);
|
||||
seasonRepository = new InMemorySeasonRepository(mockLogger);
|
||||
leagueRepository = new InMemoryLeagueRepository(mockLogger);
|
||||
leagueMembershipRepository = new InMemoryLeagueMembershipRepository(mockLogger);
|
||||
raceRepository = new InMemoryRaceRepository(mockLogger);
|
||||
|
||||
getSponsorDashboardUseCase = new GetSponsorDashboardUseCase(
|
||||
sponsorRepository,
|
||||
seasonSponsorshipRepository,
|
||||
seasonRepository,
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRepository,
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// sponsorRepository.clear();
|
||||
// campaignRepository.clear();
|
||||
// billingRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
sponsorRepository.clear();
|
||||
seasonSponsorshipRepository.clear();
|
||||
seasonRepository.clear();
|
||||
leagueRepository.clear();
|
||||
leagueMembershipRepository.clear();
|
||||
raceRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetDashboardOverviewUseCase - Success Path', () => {
|
||||
it('should retrieve dashboard overview for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with complete dashboard data
|
||||
describe('GetSponsorDashboardUseCase - Success Path', () => {
|
||||
it('should retrieve dashboard metrics for a sponsor with active sponsorships', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has company name "Test Company"
|
||||
// And: The sponsor has 5 campaigns
|
||||
// And: The sponsor has billing data
|
||||
// When: GetDashboardOverviewUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show company name
|
||||
// And: The result should show welcome message
|
||||
// And: The result should show quick action buttons
|
||||
// And: EventPublisher should emit DashboardOverviewAccessedEvent
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
it('should retrieve overview with minimal data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with minimal data
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has company name "Test Company"
|
||||
// And: The sponsor has no campaigns
|
||||
// And: The sponsor has no billing data
|
||||
// When: GetDashboardOverviewUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show company name
|
||||
// And: The result should show welcome message
|
||||
// And: EventPublisher should emit DashboardOverviewAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetDashboardOverviewUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetDashboardOverviewUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetDashboardMetricsUseCase - Success Path', () => {
|
||||
it('should retrieve dashboard metrics for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with complete metrics
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 5 total sponsorships
|
||||
// And: The sponsor has 2 active sponsorships
|
||||
// And: The sponsor has total investment of $5000
|
||||
// And: The sponsor has total impressions of 100000
|
||||
// When: GetDashboardMetricsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show total sponsorships: 5
|
||||
// And: The result should show active sponsorships: 2
|
||||
// And: The result should show total investment: $5000
|
||||
// And: The result should show total impressions: 100000
|
||||
// And: EventPublisher should emit DashboardMetricsAccessedEvent
|
||||
const league1 = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league1);
|
||||
|
||||
const league2 = League.create({
|
||||
id: 'league-2',
|
||||
name: 'League 2',
|
||||
description: 'Description 2',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
await leagueRepository.create(league2);
|
||||
|
||||
const season1 = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season1);
|
||||
|
||||
const season2 = Season.create({
|
||||
id: 'season-2',
|
||||
leagueId: 'league-2',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 2',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season2);
|
||||
|
||||
const sponsorship1 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship1);
|
||||
|
||||
const sponsorship2 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-2',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-2',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(500, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship2);
|
||||
|
||||
// And: The sponsor has 5 drivers in league 1 and 3 drivers in league 2
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
driverId: `driver-1-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
driverId: `driver-2-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
// And: The sponsor has 3 completed races in league 1 and 2 completed races in league 2
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
track: 'Track 1',
|
||||
car: 'GT3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
track: 'Track 2',
|
||||
car: 'GT3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
// When: GetSponsorDashboardUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain dashboard metrics
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dashboard = result.unwrap();
|
||||
|
||||
// And: The sponsor name should be correct
|
||||
expect(dashboard.sponsorName).toBe('Test Company');
|
||||
|
||||
// And: The metrics should show correct values
|
||||
expect(dashboard.metrics.impressions).toBeGreaterThan(0);
|
||||
expect(dashboard.metrics.races).toBe(5); // 3 + 2
|
||||
expect(dashboard.metrics.drivers).toBe(8); // 5 + 3
|
||||
expect(dashboard.metrics.exposure).toBeGreaterThan(0);
|
||||
|
||||
// And: The sponsored leagues should contain both leagues
|
||||
expect(dashboard.sponsoredLeagues).toHaveLength(2);
|
||||
expect(dashboard.sponsoredLeagues[0].leagueName).toBe('League 1');
|
||||
expect(dashboard.sponsoredLeagues[1].leagueName).toBe('League 2');
|
||||
|
||||
// And: The investment summary should show correct values
|
||||
expect(dashboard.investment.activeSponsorships).toBe(2);
|
||||
expect(dashboard.investment.totalInvestment.amount).toBe(1500); // 1000 + 500
|
||||
expect(dashboard.investment.costPerThousandViews).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should retrieve metrics with zero values', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no metrics
|
||||
it('should retrieve dashboard with zero values when sponsor has no sponsorships', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has no campaigns
|
||||
// When: GetDashboardMetricsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show total sponsorships: 0
|
||||
// And: The result should show active sponsorships: 0
|
||||
// And: The result should show total investment: $0
|
||||
// And: The result should show total impressions: 0
|
||||
// And: EventPublisher should emit DashboardMetricsAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has no sponsorships
|
||||
// When: GetSponsorDashboardUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain dashboard metrics with zero values
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dashboard = result.unwrap();
|
||||
|
||||
// And: The sponsor name should be correct
|
||||
expect(dashboard.sponsorName).toBe('Test Company');
|
||||
|
||||
// And: The metrics should show zero values
|
||||
expect(dashboard.metrics.impressions).toBe(0);
|
||||
expect(dashboard.metrics.races).toBe(0);
|
||||
expect(dashboard.metrics.drivers).toBe(0);
|
||||
expect(dashboard.metrics.exposure).toBe(0);
|
||||
|
||||
// And: The sponsored leagues should be empty
|
||||
expect(dashboard.sponsoredLeagues).toHaveLength(0);
|
||||
|
||||
// And: The investment summary should show zero values
|
||||
expect(dashboard.investment.activeSponsorships).toBe(0);
|
||||
expect(dashboard.investment.totalInvestment.amount).toBe(0);
|
||||
expect(dashboard.investment.costPerThousandViews).toBe(0);
|
||||
});
|
||||
|
||||
it('should retrieve dashboard with mixed sponsorship statuses', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 1 active, 1 pending, and 1 completed sponsorship
|
||||
const league1 = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league1);
|
||||
|
||||
const season1 = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season1);
|
||||
|
||||
const sponsorship1 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship1);
|
||||
|
||||
const sponsorship2 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-2',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(500, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship2);
|
||||
|
||||
const sponsorship3 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-3',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(300, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship3);
|
||||
|
||||
// When: GetSponsorDashboardUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain dashboard metrics
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dashboard = result.unwrap();
|
||||
|
||||
// And: The investment summary should show only active sponsorships
|
||||
expect(dashboard.investment.activeSponsorships).toBe(3);
|
||||
expect(dashboard.investment.totalInvestment.amount).toBe(1800); // 1000 + 500 + 300
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetDashboardMetricsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
describe('GetSponsorDashboardUseCase - Error Handling', () => {
|
||||
it('should return error when sponsor does not exist', async () => {
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetDashboardMetricsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// When: GetSponsorDashboardUseCase.execute() is called with non-existent sponsor ID
|
||||
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'non-existent-sponsor' });
|
||||
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('SPONSOR_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRecentActivityUseCase - Success Path', () => {
|
||||
it('should retrieve recent activity for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with recent activity
|
||||
describe('Sponsor Dashboard Data Orchestration', () => {
|
||||
it('should correctly aggregate dashboard metrics across multiple sponsorships', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has recent sponsorship updates
|
||||
// And: The sponsor has recent billing activity
|
||||
// And: The sponsor has recent campaign changes
|
||||
// When: GetRecentActivityUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should contain recent sponsorship updates
|
||||
// And: The result should contain recent billing activity
|
||||
// And: The result should contain recent campaign changes
|
||||
// And: EventPublisher should emit RecentActivityAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 3 sponsorships with different investments
|
||||
const league1 = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league1);
|
||||
|
||||
const league2 = League.create({
|
||||
id: 'league-2',
|
||||
name: 'League 2',
|
||||
description: 'Description 2',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
await leagueRepository.create(league2);
|
||||
|
||||
const league3 = League.create({
|
||||
id: 'league-3',
|
||||
name: 'League 3',
|
||||
description: 'Description 3',
|
||||
ownerId: 'owner-3',
|
||||
});
|
||||
await leagueRepository.create(league3);
|
||||
|
||||
const season1 = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season1);
|
||||
|
||||
const season2 = Season.create({
|
||||
id: 'season-2',
|
||||
leagueId: 'league-2',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 2',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season2);
|
||||
|
||||
const season3 = Season.create({
|
||||
id: 'season-3',
|
||||
leagueId: 'league-3',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 3',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season3);
|
||||
|
||||
const sponsorship1 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship1);
|
||||
|
||||
const sponsorship2 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-2',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-2',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(2000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship2);
|
||||
|
||||
const sponsorship3 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-3',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-3',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(3000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship3);
|
||||
|
||||
// And: The sponsor has different numbers of drivers and races in each league
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
driverId: `driver-1-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
driverId: `driver-2-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
driverId: `driver-3-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
track: 'Track 1',
|
||||
car: 'GT3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
track: 'Track 2',
|
||||
car: 'GT3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
track: 'Track 3',
|
||||
car: 'GT3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
// When: GetSponsorDashboardUseCase.execute() is called
|
||||
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The metrics should be correctly aggregated
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dashboard = result.unwrap();
|
||||
|
||||
// Total drivers: 10 + 5 + 8 = 23
|
||||
expect(dashboard.metrics.drivers).toBe(23);
|
||||
|
||||
// Total races: 5 + 3 + 4 = 12
|
||||
expect(dashboard.metrics.races).toBe(12);
|
||||
|
||||
// Total investment: 1000 + 2000 + 3000 = 6000
|
||||
expect(dashboard.investment.totalInvestment.amount).toBe(6000);
|
||||
|
||||
// Total sponsorships: 3
|
||||
expect(dashboard.investment.activeSponsorships).toBe(3);
|
||||
|
||||
// Cost per thousand views should be calculated correctly
|
||||
expect(dashboard.investment.costPerThousandViews).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should retrieve activity with empty result', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no recent activity
|
||||
it('should correctly calculate impressions based on completed races and drivers', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has no recent activity
|
||||
// When: GetRecentActivityUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit RecentActivityAccessedEvent
|
||||
});
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
describe('GetRecentActivityUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetRecentActivityUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
// And: The sponsor has 1 league with 10 drivers and 5 completed races
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league);
|
||||
|
||||
describe('GetPendingActionsUseCase - Success Path', () => {
|
||||
it('should retrieve pending actions for a sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with pending actions
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season);
|
||||
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-${i}`,
|
||||
leagueId: 'league-1',
|
||||
driverId: `driver-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-${i}`,
|
||||
leagueId: 'league-1',
|
||||
track: 'Track 1',
|
||||
car: 'GT3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
// When: GetSponsorDashboardUseCase.execute() is called
|
||||
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: Impressions should be calculated correctly
|
||||
// Impressions = completed races * drivers * 100 = 5 * 10 * 100 = 5000
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dashboard = result.unwrap();
|
||||
expect(dashboard.metrics.impressions).toBe(5000);
|
||||
});
|
||||
|
||||
it('should correctly determine sponsorship status based on season dates', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has sponsorships awaiting approval
|
||||
// And: The sponsor has pending payments
|
||||
// And: The sponsor has action items
|
||||
// When: GetPendingActionsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show sponsorships awaiting approval
|
||||
// And: The result should show pending payments
|
||||
// And: The result should show action items
|
||||
// And: EventPublisher should emit PendingActionsAccessedEvent
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
it('should retrieve pending actions with empty result', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no pending actions
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has no pending actions
|
||||
// When: GetPendingActionsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit PendingActionsAccessedEvent
|
||||
});
|
||||
});
|
||||
// And: The sponsor has sponsorships with different season dates
|
||||
const league1 = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league1);
|
||||
|
||||
describe('GetPendingActionsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetPendingActionsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
const league2 = League.create({
|
||||
id: 'league-2',
|
||||
name: 'League 2',
|
||||
description: 'Description 2',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
await leagueRepository.create(league2);
|
||||
|
||||
describe('Dashboard Data Orchestration', () => {
|
||||
it('should correctly aggregate dashboard metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Dashboard metrics aggregation
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has 3 campaigns with investments: $1000, $2000, $3000
|
||||
// And: The sponsor has 3 campaigns with impressions: 50000, 30000, 20000
|
||||
// When: GetDashboardMetricsUseCase.execute() is called
|
||||
// Then: Total sponsorships should be 3
|
||||
// And: Active sponsorships should be calculated correctly
|
||||
// And: Total investment should be $6000
|
||||
// And: Total impressions should be 100000
|
||||
// And: EventPublisher should emit DashboardMetricsAccessedEvent
|
||||
});
|
||||
const league3 = League.create({
|
||||
id: 'league-3',
|
||||
name: 'League 3',
|
||||
description: 'Description 3',
|
||||
ownerId: 'owner-3',
|
||||
});
|
||||
await leagueRepository.create(league3);
|
||||
|
||||
it('should correctly format recent activity', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Recent activity formatting
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has recent activity from different sources
|
||||
// When: GetRecentActivityUseCase.execute() is called
|
||||
// Then: Activity should be sorted by date (newest first)
|
||||
// And: Each activity should have correct type and details
|
||||
// And: EventPublisher should emit RecentActivityAccessedEvent
|
||||
});
|
||||
// Active season (current date is between start and end)
|
||||
const season1 = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date(Date.now() - 86400000),
|
||||
endDate: new Date(Date.now() + 86400000),
|
||||
});
|
||||
await seasonRepository.create(season1);
|
||||
|
||||
it('should correctly identify pending actions', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Pending actions identification
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: The sponsor has sponsorships awaiting approval
|
||||
// And: The sponsor has pending payments
|
||||
// When: GetPendingActionsUseCase.execute() is called
|
||||
// Then: All pending actions should be identified
|
||||
// And: Each action should have correct priority
|
||||
// And: EventPublisher should emit PendingActionsAccessedEvent
|
||||
// Upcoming season (start date is in the future)
|
||||
const season2 = Season.create({
|
||||
id: 'season-2',
|
||||
leagueId: 'league-2',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 2',
|
||||
startDate: new Date(Date.now() + 86400000),
|
||||
endDate: new Date(Date.now() + 172800000),
|
||||
});
|
||||
await seasonRepository.create(season2);
|
||||
|
||||
// Completed season (end date is in the past)
|
||||
const season3 = Season.create({
|
||||
id: 'season-3',
|
||||
leagueId: 'league-3',
|
||||
gameId: 'game-1',
|
||||
name: 'Season 3',
|
||||
startDate: new Date(Date.now() - 172800000),
|
||||
endDate: new Date(Date.now() - 86400000),
|
||||
});
|
||||
await seasonRepository.create(season3);
|
||||
|
||||
const sponsorship1 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship1);
|
||||
|
||||
const sponsorship2 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-2',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-2',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(500, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship2);
|
||||
|
||||
const sponsorship3 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-3',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-3',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(300, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship3);
|
||||
|
||||
// When: GetSponsorDashboardUseCase.execute() is called
|
||||
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The sponsored leagues should have correct status
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dashboard = result.unwrap();
|
||||
|
||||
expect(dashboard.sponsoredLeagues).toHaveLength(3);
|
||||
|
||||
// League 1 should be active (current date is between start and end)
|
||||
expect(dashboard.sponsoredLeagues[0].status).toBe('active');
|
||||
|
||||
// League 2 should be upcoming (start date is in the future)
|
||||
expect(dashboard.sponsoredLeagues[1].status).toBe('upcoming');
|
||||
|
||||
// League 3 should be completed (end date is in the past)
|
||||
expect(dashboard.sponsoredLeagues[2].status).toBe('completed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,345 +1,339 @@
|
||||
/**
|
||||
* Integration Test: Sponsor League Detail Use Case Orchestration
|
||||
*
|
||||
*
|
||||
* Tests the orchestration logic of sponsor league detail-related Use Cases:
|
||||
* - GetLeagueDetailUseCase: Retrieves detailed league information
|
||||
* - GetLeagueStatisticsUseCase: Retrieves league statistics
|
||||
* - GetSponsorshipSlotsUseCase: Retrieves sponsorship slots information
|
||||
* - GetLeagueScheduleUseCase: Retrieves league schedule
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - GetEntitySponsorshipPricingUseCase: Retrieves sponsorship pricing for leagues
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemorySponsorRepository } from '../../../adapters/sponsors/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetLeagueDetailUseCase } from '../../../core/sponsors/use-cases/GetLeagueDetailUseCase';
|
||||
import { GetLeagueStatisticsUseCase } from '../../../core/sponsors/use-cases/GetLeagueStatisticsUseCase';
|
||||
import { GetSponsorshipSlotsUseCase } from '../../../core/sponsors/use-cases/GetSponsorshipSlotsUseCase';
|
||||
import { GetLeagueScheduleUseCase } from '../../../core/sponsors/use-cases/GetLeagueScheduleUseCase';
|
||||
import { GetLeagueDetailQuery } from '../../../core/sponsors/ports/GetLeagueDetailQuery';
|
||||
import { GetLeagueStatisticsQuery } from '../../../core/sponsors/ports/GetLeagueStatisticsQuery';
|
||||
import { GetSponsorshipSlotsQuery } from '../../../core/sponsors/ports/GetSponsorshipSlotsQuery';
|
||||
import { GetLeagueScheduleQuery } from '../../../core/sponsors/ports/GetLeagueScheduleQuery';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemorySponsorshipPricingRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository';
|
||||
import { GetEntitySponsorshipPricingUseCase } from '../../../core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Sponsor League Detail Use Case Orchestration', () => {
|
||||
let sponsorRepository: InMemorySponsorRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getLeagueDetailUseCase: GetLeagueDetailUseCase;
|
||||
let getLeagueStatisticsUseCase: GetLeagueStatisticsUseCase;
|
||||
let getSponsorshipSlotsUseCase: GetSponsorshipSlotsUseCase;
|
||||
let getLeagueScheduleUseCase: GetLeagueScheduleUseCase;
|
||||
let sponsorshipPricingRepository: InMemorySponsorshipPricingRepository;
|
||||
let getEntitySponsorshipPricingUseCase: GetEntitySponsorshipPricingUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// sponsorRepository = new InMemorySponsorRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getLeagueDetailUseCase = new GetLeagueDetailUseCase({
|
||||
// sponsorRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getLeagueStatisticsUseCase = new GetLeagueStatisticsUseCase({
|
||||
// sponsorRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getSponsorshipSlotsUseCase = new GetSponsorshipSlotsUseCase({
|
||||
// sponsorRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getLeagueScheduleUseCase = new GetLeagueScheduleUseCase({
|
||||
// sponsorRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
sponsorshipPricingRepository = new InMemorySponsorshipPricingRepository(mockLogger);
|
||||
getEntitySponsorshipPricingUseCase = new GetEntitySponsorshipPricingUseCase(
|
||||
sponsorshipPricingRepository,
|
||||
mockLogger,
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// sponsorRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
sponsorshipPricingRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetLeagueDetailUseCase - Success Path', () => {
|
||||
it('should retrieve detailed league information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor views league detail
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has name "Premier League"
|
||||
// And: The league has description "Top tier racing league"
|
||||
// And: The league has logo URL
|
||||
// And: The league has category "Professional"
|
||||
// When: GetLeagueDetailUseCase.execute() is called with sponsor ID and league ID
|
||||
// Then: The result should show league name
|
||||
// And: The result should show league description
|
||||
// And: The result should show league logo
|
||||
// And: The result should show league category
|
||||
// And: EventPublisher should emit LeagueDetailAccessedEvent
|
||||
describe('GetEntitySponsorshipPricingUseCase - Success Path', () => {
|
||||
it('should retrieve sponsorship pricing for a league', async () => {
|
||||
// Given: A league exists with ID "league-123"
|
||||
const leagueId = 'league-123';
|
||||
|
||||
// And: The league has sponsorship pricing configured
|
||||
const pricing = {
|
||||
entityType: 'league' as const,
|
||||
entityId: leagueId,
|
||||
acceptingApplications: true,
|
||||
mainSlot: {
|
||||
price: { amount: 10000, currency: 'USD' },
|
||||
benefits: ['Primary logo placement', 'League page header banner'],
|
||||
},
|
||||
secondarySlots: {
|
||||
price: { amount: 2000, currency: 'USD' },
|
||||
benefits: ['Secondary logo on liveries', 'League page sidebar placement'],
|
||||
},
|
||||
};
|
||||
await sponsorshipPricingRepository.create(pricing);
|
||||
|
||||
// When: GetEntitySponsorshipPricingUseCase.execute() is called
|
||||
const result = await getEntitySponsorshipPricingUseCase.execute({
|
||||
entityType: 'league',
|
||||
entityId: leagueId,
|
||||
});
|
||||
|
||||
// Then: The result should contain sponsorship pricing
|
||||
expect(result.isOk()).toBe(true);
|
||||
const pricingResult = result.unwrap();
|
||||
|
||||
// And: The entity type should be correct
|
||||
expect(pricingResult.entityType).toBe('league');
|
||||
|
||||
// And: The entity ID should be correct
|
||||
expect(pricingResult.entityId).toBe(leagueId);
|
||||
|
||||
// And: The league should be accepting applications
|
||||
expect(pricingResult.acceptingApplications).toBe(true);
|
||||
|
||||
// And: The tiers should contain main slot
|
||||
expect(pricingResult.tiers).toHaveLength(2);
|
||||
expect(pricingResult.tiers[0].name).toBe('main');
|
||||
expect(pricingResult.tiers[0].price.amount).toBe(10000);
|
||||
expect(pricingResult.tiers[0].price.currency).toBe('USD');
|
||||
expect(pricingResult.tiers[0].benefits).toContain('Primary logo placement');
|
||||
|
||||
// And: The tiers should contain secondary slot
|
||||
expect(pricingResult.tiers[1].name).toBe('secondary');
|
||||
expect(pricingResult.tiers[1].price.amount).toBe(2000);
|
||||
expect(pricingResult.tiers[1].price.currency).toBe('USD');
|
||||
expect(pricingResult.tiers[1].benefits).toContain('Secondary logo on liveries');
|
||||
});
|
||||
|
||||
it('should retrieve league detail with minimal data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League with minimal data
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has name "Test League"
|
||||
// When: GetLeagueDetailUseCase.execute() is called with sponsor ID and league ID
|
||||
// Then: The result should show league name
|
||||
// And: EventPublisher should emit LeagueDetailAccessedEvent
|
||||
it('should retrieve sponsorship pricing with only main slot', async () => {
|
||||
// Given: A league exists with ID "league-123"
|
||||
const leagueId = 'league-123';
|
||||
|
||||
// And: The league has sponsorship pricing configured with only main slot
|
||||
const pricing = {
|
||||
entityType: 'league' as const,
|
||||
entityId: leagueId,
|
||||
acceptingApplications: true,
|
||||
mainSlot: {
|
||||
price: { amount: 10000, currency: 'USD' },
|
||||
benefits: ['Primary logo placement', 'League page header banner'],
|
||||
},
|
||||
};
|
||||
await sponsorshipPricingRepository.create(pricing);
|
||||
|
||||
// When: GetEntitySponsorshipPricingUseCase.execute() is called
|
||||
const result = await getEntitySponsorshipPricingUseCase.execute({
|
||||
entityType: 'league',
|
||||
entityId: leagueId,
|
||||
});
|
||||
|
||||
// Then: The result should contain sponsorship pricing
|
||||
expect(result.isOk()).toBe(true);
|
||||
const pricingResult = result.unwrap();
|
||||
|
||||
// And: The tiers should contain only main slot
|
||||
expect(pricingResult.tiers).toHaveLength(1);
|
||||
expect(pricingResult.tiers[0].name).toBe('main');
|
||||
expect(pricingResult.tiers[0].price.amount).toBe(10000);
|
||||
});
|
||||
|
||||
it('should retrieve sponsorship pricing with custom requirements', async () => {
|
||||
// Given: A league exists with ID "league-123"
|
||||
const leagueId = 'league-123';
|
||||
|
||||
// And: The league has sponsorship pricing configured with custom requirements
|
||||
const pricing = {
|
||||
entityType: 'league' as const,
|
||||
entityId: leagueId,
|
||||
acceptingApplications: true,
|
||||
customRequirements: 'Must have racing experience',
|
||||
mainSlot: {
|
||||
price: { amount: 10000, currency: 'USD' },
|
||||
benefits: ['Primary logo placement'],
|
||||
},
|
||||
};
|
||||
await sponsorshipPricingRepository.create(pricing);
|
||||
|
||||
// When: GetEntitySponsorshipPricingUseCase.execute() is called
|
||||
const result = await getEntitySponsorshipPricingUseCase.execute({
|
||||
entityType: 'league',
|
||||
entityId: leagueId,
|
||||
});
|
||||
|
||||
// Then: The result should contain sponsorship pricing
|
||||
expect(result.isOk()).toBe(true);
|
||||
const pricingResult = result.unwrap();
|
||||
|
||||
// And: The custom requirements should be included
|
||||
expect(pricingResult.customRequirements).toBe('Must have racing experience');
|
||||
});
|
||||
|
||||
it('should retrieve sponsorship pricing with not accepting applications', async () => {
|
||||
// Given: A league exists with ID "league-123"
|
||||
const leagueId = 'league-123';
|
||||
|
||||
// And: The league has sponsorship pricing configured but not accepting applications
|
||||
const pricing = {
|
||||
entityType: 'league' as const,
|
||||
entityId: leagueId,
|
||||
acceptingApplications: false,
|
||||
mainSlot: {
|
||||
price: { amount: 10000, currency: 'USD' },
|
||||
benefits: ['Primary logo placement'],
|
||||
},
|
||||
};
|
||||
await sponsorshipPricingRepository.create(pricing);
|
||||
|
||||
// When: GetEntitySponsorshipPricingUseCase.execute() is called
|
||||
const result = await getEntitySponsorshipPricingUseCase.execute({
|
||||
entityType: 'league',
|
||||
entityId: leagueId,
|
||||
});
|
||||
|
||||
// Then: The result should contain sponsorship pricing
|
||||
expect(result.isOk()).toBe(true);
|
||||
const pricingResult = result.unwrap();
|
||||
|
||||
// And: The league should not be accepting applications
|
||||
expect(pricingResult.acceptingApplications).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetLeagueDetailUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// And: A league exists with ID "league-456"
|
||||
// When: GetLeagueDetailUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
describe('GetEntitySponsorshipPricingUseCase - Error Handling', () => {
|
||||
it('should return error when pricing is not configured', async () => {
|
||||
// Given: A league exists with ID "league-123"
|
||||
const leagueId = 'league-123';
|
||||
|
||||
// And: The league has no sponsorship pricing configured
|
||||
// When: GetEntitySponsorshipPricingUseCase.execute() is called
|
||||
const result = await getEntitySponsorshipPricingUseCase.execute({
|
||||
entityType: 'league',
|
||||
entityId: leagueId,
|
||||
});
|
||||
|
||||
it('should throw error when league does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent league
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: No league exists with the given ID
|
||||
// When: GetLeagueDetailUseCase.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: A sponsor exists with ID "sponsor-123"
|
||||
// And: An invalid league ID (e.g., empty string, null, undefined)
|
||||
// When: GetLeagueDetailUseCase.execute() is called with invalid league ID
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('PRICING_NOT_CONFIGURED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetLeagueStatisticsUseCase - Success Path', () => {
|
||||
it('should retrieve league statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League with statistics
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has 500 total drivers
|
||||
// And: The league has 300 active drivers
|
||||
// And: The league has 100 total races
|
||||
// And: The league has average race duration of 45 minutes
|
||||
// And: The league has popularity score of 85
|
||||
// When: GetLeagueStatisticsUseCase.execute() is called with sponsor ID and league ID
|
||||
// Then: The result should show total drivers: 500
|
||||
// And: The result should show active drivers: 300
|
||||
// And: The result should show total races: 100
|
||||
// And: The result should show average race duration: 45 minutes
|
||||
// And: The result should show popularity score: 85
|
||||
// And: EventPublisher should emit LeagueStatisticsAccessedEvent
|
||||
describe('Sponsor League Detail Data Orchestration', () => {
|
||||
it('should correctly retrieve sponsorship pricing with all tiers', async () => {
|
||||
// Given: A league exists with ID "league-123"
|
||||
const leagueId = 'league-123';
|
||||
|
||||
// And: The league has sponsorship pricing configured with both main and secondary slots
|
||||
const pricing = {
|
||||
entityType: 'league' as const,
|
||||
entityId: leagueId,
|
||||
acceptingApplications: true,
|
||||
customRequirements: 'Must have racing experience',
|
||||
mainSlot: {
|
||||
price: { amount: 10000, currency: 'USD' },
|
||||
benefits: [
|
||||
'Primary logo placement on all liveries',
|
||||
'League page header banner',
|
||||
'Race results page branding',
|
||||
'Social media feature posts',
|
||||
'Newsletter sponsor spot',
|
||||
],
|
||||
},
|
||||
secondarySlots: {
|
||||
price: { amount: 2000, currency: 'USD' },
|
||||
benefits: [
|
||||
'Secondary logo on liveries',
|
||||
'League page sidebar placement',
|
||||
'Race results mention',
|
||||
'Social media mentions',
|
||||
],
|
||||
},
|
||||
};
|
||||
await sponsorshipPricingRepository.create(pricing);
|
||||
|
||||
// When: GetEntitySponsorshipPricingUseCase.execute() is called
|
||||
const result = await getEntitySponsorshipPricingUseCase.execute({
|
||||
entityType: 'league',
|
||||
entityId: leagueId,
|
||||
});
|
||||
|
||||
// Then: The sponsorship pricing should be correctly retrieved
|
||||
expect(result.isOk()).toBe(true);
|
||||
const pricingResult = result.unwrap();
|
||||
|
||||
// And: The entity type should be correct
|
||||
expect(pricingResult.entityType).toBe('league');
|
||||
|
||||
// And: The entity ID should be correct
|
||||
expect(pricingResult.entityId).toBe(leagueId);
|
||||
|
||||
// And: The league should be accepting applications
|
||||
expect(pricingResult.acceptingApplications).toBe(true);
|
||||
|
||||
// And: The custom requirements should be included
|
||||
expect(pricingResult.customRequirements).toBe('Must have racing experience');
|
||||
|
||||
// And: The tiers should contain both main and secondary slots
|
||||
expect(pricingResult.tiers).toHaveLength(2);
|
||||
|
||||
// And: The main slot should have correct price and benefits
|
||||
expect(pricingResult.tiers[0].name).toBe('main');
|
||||
expect(pricingResult.tiers[0].price.amount).toBe(10000);
|
||||
expect(pricingResult.tiers[0].price.currency).toBe('USD');
|
||||
expect(pricingResult.tiers[0].benefits).toHaveLength(5);
|
||||
expect(pricingResult.tiers[0].benefits).toContain('Primary logo placement on all liveries');
|
||||
|
||||
// And: The secondary slot should have correct price and benefits
|
||||
expect(pricingResult.tiers[1].name).toBe('secondary');
|
||||
expect(pricingResult.tiers[1].price.amount).toBe(2000);
|
||||
expect(pricingResult.tiers[1].price.currency).toBe('USD');
|
||||
expect(pricingResult.tiers[1].benefits).toHaveLength(4);
|
||||
expect(pricingResult.tiers[1].benefits).toContain('Secondary logo on liveries');
|
||||
});
|
||||
|
||||
it('should retrieve statistics with zero values', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League with no statistics
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has no drivers
|
||||
// And: The league has no races
|
||||
// When: GetLeagueStatisticsUseCase.execute() is called with sponsor ID and league ID
|
||||
// Then: The result should show total drivers: 0
|
||||
// And: The result should show active drivers: 0
|
||||
// And: The result should show total races: 0
|
||||
// And: The result should show average race duration: 0
|
||||
// And: The result should show popularity score: 0
|
||||
// And: EventPublisher should emit LeagueStatisticsAccessedEvent
|
||||
});
|
||||
});
|
||||
it('should correctly retrieve sponsorship pricing for different entity types', async () => {
|
||||
// Given: A league exists with ID "league-123"
|
||||
const leagueId = 'league-123';
|
||||
|
||||
// And: The league has sponsorship pricing configured
|
||||
const leaguePricing = {
|
||||
entityType: 'league' as const,
|
||||
entityId: leagueId,
|
||||
acceptingApplications: true,
|
||||
mainSlot: {
|
||||
price: { amount: 10000, currency: 'USD' },
|
||||
benefits: ['Primary logo placement'],
|
||||
},
|
||||
};
|
||||
await sponsorshipPricingRepository.create(leaguePricing);
|
||||
|
||||
describe('GetLeagueStatisticsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// And: A league exists with ID "league-456"
|
||||
// When: GetLeagueStatisticsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
// And: A team exists with ID "team-456"
|
||||
const teamId = 'team-456';
|
||||
|
||||
// And: The team has sponsorship pricing configured
|
||||
const teamPricing = {
|
||||
entityType: 'team' as const,
|
||||
entityId: teamId,
|
||||
acceptingApplications: true,
|
||||
mainSlot: {
|
||||
price: { amount: 5000, currency: 'USD' },
|
||||
benefits: ['Team logo placement'],
|
||||
},
|
||||
};
|
||||
await sponsorshipPricingRepository.create(teamPricing);
|
||||
|
||||
it('should throw error when league does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent league
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: No league exists with the given ID
|
||||
// When: GetLeagueStatisticsUseCase.execute() is called with non-existent league ID
|
||||
// Then: Should throw LeagueNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
// When: GetEntitySponsorshipPricingUseCase.execute() is called for league
|
||||
const leagueResult = await getEntitySponsorshipPricingUseCase.execute({
|
||||
entityType: 'league',
|
||||
entityId: leagueId,
|
||||
});
|
||||
|
||||
describe('GetSponsorshipSlotsUseCase - Success Path', () => {
|
||||
it('should retrieve sponsorship slots information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League with sponsorship slots
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has main sponsor slot available
|
||||
// And: The league has 5 secondary sponsor slots available
|
||||
// And: The main slot has pricing of $10000
|
||||
// And: The secondary slots have pricing of $2000 each
|
||||
// When: GetSponsorshipSlotsUseCase.execute() is called with sponsor ID and league ID
|
||||
// Then: The result should show main sponsor slot details
|
||||
// And: The result should show secondary sponsor slots details
|
||||
// And: The result should show available slots count
|
||||
// And: EventPublisher should emit SponsorshipSlotsAccessedEvent
|
||||
});
|
||||
// Then: The league pricing should be retrieved
|
||||
expect(leagueResult.isOk()).toBe(true);
|
||||
const leaguePricingResult = leagueResult.unwrap();
|
||||
expect(leaguePricingResult.entityType).toBe('league');
|
||||
expect(leaguePricingResult.entityId).toBe(leagueId);
|
||||
expect(leaguePricingResult.tiers[0].price.amount).toBe(10000);
|
||||
|
||||
it('should retrieve slots with no available slots', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League with no available slots
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has no available sponsorship slots
|
||||
// When: GetSponsorshipSlotsUseCase.execute() is called with sponsor ID and league ID
|
||||
// Then: The result should show no available slots
|
||||
// And: EventPublisher should emit SponsorshipSlotsAccessedEvent
|
||||
});
|
||||
});
|
||||
// When: GetEntitySponsorshipPricingUseCase.execute() is called for team
|
||||
const teamResult = await getEntitySponsorshipPricingUseCase.execute({
|
||||
entityType: 'team',
|
||||
entityId: teamId,
|
||||
});
|
||||
|
||||
describe('GetSponsorshipSlotsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// And: A league exists with ID "league-456"
|
||||
// When: GetSponsorshipSlotsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when league does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent league
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: No league exists with the given ID
|
||||
// When: GetSponsorshipSlotsUseCase.execute() is called with non-existent league ID
|
||||
// Then: Should throw LeagueNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetLeagueScheduleUseCase - Success Path', () => {
|
||||
it('should retrieve league schedule', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League with schedule
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has 5 upcoming races
|
||||
// When: GetLeagueScheduleUseCase.execute() is called with sponsor ID and league ID
|
||||
// Then: The result should show upcoming races
|
||||
// And: Each race should show race date
|
||||
// And: Each race should show race location
|
||||
// And: Each race should show race type
|
||||
// And: EventPublisher should emit LeagueScheduleAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve schedule with no upcoming races', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League with no upcoming races
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has no upcoming races
|
||||
// When: GetLeagueScheduleUseCase.execute() is called with sponsor ID and league ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit LeagueScheduleAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetLeagueScheduleUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// And: A league exists with ID "league-456"
|
||||
// When: GetLeagueScheduleUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when league does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent league
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: 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
|
||||
});
|
||||
});
|
||||
|
||||
describe('League Detail Data Orchestration', () => {
|
||||
it('should correctly retrieve league detail with all information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League detail orchestration
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has complete information
|
||||
// When: GetLeagueDetailUseCase.execute() is called
|
||||
// Then: The result should contain all league information
|
||||
// And: Each field should be populated correctly
|
||||
// And: EventPublisher should emit LeagueDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly aggregate league statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League statistics aggregation
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has 500 total drivers
|
||||
// And: The league has 300 active drivers
|
||||
// And: The league has 100 total races
|
||||
// When: GetLeagueStatisticsUseCase.execute() is called
|
||||
// Then: Total drivers should be 500
|
||||
// And: Active drivers should be 300
|
||||
// And: Total races should be 100
|
||||
// And: EventPublisher should emit LeagueStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly retrieve sponsorship slots', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsorship slots retrieval
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has main sponsor slot available
|
||||
// And: The league has 5 secondary sponsor slots available
|
||||
// When: GetSponsorshipSlotsUseCase.execute() is called
|
||||
// Then: Main sponsor slot should be available
|
||||
// And: Secondary sponsor slots count should be 5
|
||||
// And: EventPublisher should emit SponsorshipSlotsAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly retrieve league schedule', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League schedule retrieval
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: A league exists with ID "league-456"
|
||||
// And: The league has 5 upcoming races
|
||||
// When: GetLeagueScheduleUseCase.execute() is called
|
||||
// Then: All 5 races should be returned
|
||||
// And: Each race should have correct details
|
||||
// And: EventPublisher should emit LeagueScheduleAccessedEvent
|
||||
// Then: The team pricing should be retrieved
|
||||
expect(teamResult.isOk()).toBe(true);
|
||||
const teamPricingResult = teamResult.unwrap();
|
||||
expect(teamPricingResult.entityType).toBe('team');
|
||||
expect(teamPricingResult.entityId).toBe(teamId);
|
||||
expect(teamPricingResult.tiers[0].price.amount).toBe(5000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,331 +1,658 @@
|
||||
/**
|
||||
* Integration Test: Sponsor Leagues Use Case Orchestration
|
||||
*
|
||||
*
|
||||
* Tests the orchestration logic of sponsor leagues-related Use Cases:
|
||||
* - GetAvailableLeaguesUseCase: Retrieves available leagues for sponsorship
|
||||
* - GetLeagueStatisticsUseCase: Retrieves league statistics
|
||||
* - FilterLeaguesUseCase: Filters leagues by availability
|
||||
* - SearchLeaguesUseCase: Searches leagues by query
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - GetSponsorSponsorshipsUseCase: Retrieves sponsor's sponsorships/campaigns
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemorySponsorRepository } from '../../../adapters/sponsors/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetAvailableLeaguesUseCase } from '../../../core/sponsors/use-cases/GetAvailableLeaguesUseCase';
|
||||
import { GetLeagueStatisticsUseCase } from '../../../core/sponsors/use-cases/GetLeagueStatisticsUseCase';
|
||||
import { FilterLeaguesUseCase } from '../../../core/sponsors/use-cases/FilterLeaguesUseCase';
|
||||
import { SearchLeaguesUseCase } from '../../../core/sponsors/use-cases/SearchLeaguesUseCase';
|
||||
import { GetAvailableLeaguesQuery } from '../../../core/sponsors/ports/GetAvailableLeaguesQuery';
|
||||
import { GetLeagueStatisticsQuery } from '../../../core/sponsors/ports/GetLeagueStatisticsQuery';
|
||||
import { FilterLeaguesCommand } from '../../../core/sponsors/ports/FilterLeaguesCommand';
|
||||
import { SearchLeaguesCommand } from '../../../core/sponsors/ports/SearchLeaguesCommand';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemorySponsorRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemorySeasonSponsorshipRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
||||
import { InMemorySeasonRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { GetSponsorSponsorshipsUseCase } from '../../../core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
|
||||
import { Sponsor } from '../../../core/racing/domain/entities/sponsor/Sponsor';
|
||||
import { SeasonSponsorship } from '../../../core/racing/domain/entities/season/SeasonSponsorship';
|
||||
import { Season } from '../../../core/racing/domain/entities/season/Season';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { LeagueMembership } from '../../../core/racing/domain/entities/LeagueMembership';
|
||||
import { Race } from '../../../core/racing/domain/entities/Race';
|
||||
import { Money } from '../../../core/racing/domain/value-objects/Money';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Sponsor Leagues Use Case Orchestration', () => {
|
||||
let sponsorRepository: InMemorySponsorRepository;
|
||||
let seasonSponsorshipRepository: InMemorySeasonSponsorshipRepository;
|
||||
let seasonRepository: InMemorySeasonRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getAvailableLeaguesUseCase: GetAvailableLeaguesUseCase;
|
||||
let getLeagueStatisticsUseCase: GetLeagueStatisticsUseCase;
|
||||
let filterLeaguesUseCase: FilterLeaguesUseCase;
|
||||
let searchLeaguesUseCase: SearchLeaguesUseCase;
|
||||
let leagueMembershipRepository: InMemoryLeagueMembershipRepository;
|
||||
let raceRepository: InMemoryRaceRepository;
|
||||
let getSponsorSponsorshipsUseCase: GetSponsorSponsorshipsUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// sponsorRepository = new InMemorySponsorRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getAvailableLeaguesUseCase = new GetAvailableLeaguesUseCase({
|
||||
// sponsorRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// getLeagueStatisticsUseCase = new GetLeagueStatisticsUseCase({
|
||||
// sponsorRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// filterLeaguesUseCase = new FilterLeaguesUseCase({
|
||||
// sponsorRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// searchLeaguesUseCase = new SearchLeaguesUseCase({
|
||||
// sponsorRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
sponsorRepository = new InMemorySponsorRepository(mockLogger);
|
||||
seasonSponsorshipRepository = new InMemorySeasonSponsorshipRepository(mockLogger);
|
||||
seasonRepository = new InMemorySeasonRepository(mockLogger);
|
||||
leagueRepository = new InMemoryLeagueRepository(mockLogger);
|
||||
leagueMembershipRepository = new InMemoryLeagueMembershipRepository(mockLogger);
|
||||
raceRepository = new InMemoryRaceRepository(mockLogger);
|
||||
|
||||
getSponsorSponsorshipsUseCase = new GetSponsorSponsorshipsUseCase(
|
||||
sponsorRepository,
|
||||
seasonSponsorshipRepository,
|
||||
seasonRepository,
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRepository,
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// sponsorRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
sponsorRepository.clear();
|
||||
seasonSponsorshipRepository.clear();
|
||||
seasonRepository.clear();
|
||||
leagueRepository.clear();
|
||||
leagueMembershipRepository.clear();
|
||||
raceRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetAvailableLeaguesUseCase - Success Path', () => {
|
||||
it('should retrieve available leagues for sponsorship', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with available leagues
|
||||
describe('GetSponsorSponsorshipsUseCase - Success Path', () => {
|
||||
it('should retrieve all sponsorships for a sponsor', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are 5 leagues available for sponsorship
|
||||
// When: GetAvailableLeaguesUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should contain all 5 leagues
|
||||
// And: Each league should display its details
|
||||
// And: EventPublisher should emit AvailableLeaguesAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 3 sponsorships with different statuses
|
||||
const league1 = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league1);
|
||||
|
||||
const league2 = League.create({
|
||||
id: 'league-2',
|
||||
name: 'League 2',
|
||||
description: 'Description 2',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
await leagueRepository.create(league2);
|
||||
|
||||
const league3 = League.create({
|
||||
id: 'league-3',
|
||||
name: 'League 3',
|
||||
description: 'Description 3',
|
||||
ownerId: 'owner-3',
|
||||
});
|
||||
await leagueRepository.create(league3);
|
||||
|
||||
const season1 = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season1);
|
||||
|
||||
const season2 = Season.create({
|
||||
id: 'season-2',
|
||||
leagueId: 'league-2',
|
||||
name: 'Season 2',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season2);
|
||||
|
||||
const season3 = Season.create({
|
||||
id: 'season-3',
|
||||
leagueId: 'league-3',
|
||||
name: 'Season 3',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season3);
|
||||
|
||||
const sponsorship1 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship1);
|
||||
|
||||
const sponsorship2 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-2',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-2',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(500, 'USD'),
|
||||
status: 'pending',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship2);
|
||||
|
||||
const sponsorship3 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-3',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-3',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(300, 'USD'),
|
||||
status: 'completed',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship3);
|
||||
|
||||
// And: The sponsor has different numbers of drivers and races in each league
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
driverId: `driver-1-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
driverId: `driver-2-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
driverId: `driver-3-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
track: 'Track 1',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
track: 'Track 2',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
track: 'Track 3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain sponsor sponsorships
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
// And: The sponsor name should be correct
|
||||
expect(sponsorships.sponsor.name.toString()).toBe('Test Company');
|
||||
|
||||
// And: The sponsorships should contain all 3 sponsorships
|
||||
expect(sponsorships.sponsorships).toHaveLength(3);
|
||||
|
||||
// And: The summary should show correct values
|
||||
expect(sponsorships.summary.totalSponsorships).toBe(3);
|
||||
expect(sponsorships.summary.activeSponsorships).toBe(1);
|
||||
expect(sponsorships.summary.totalInvestment.amount).toBe(1800); // 1000 + 500 + 300
|
||||
expect(sponsorships.summary.totalPlatformFees.amount).toBe(180); // 100 + 50 + 30
|
||||
|
||||
// And: Each sponsorship should have correct metrics
|
||||
const sponsorship1Summary = sponsorships.sponsorships.find(s => s.sponsorship.id === 'sponsorship-1');
|
||||
expect(sponsorship1Summary).toBeDefined();
|
||||
expect(sponsorship1Summary?.metrics.drivers).toBe(10);
|
||||
expect(sponsorship1Summary?.metrics.races).toBe(5);
|
||||
expect(sponsorship1Summary?.metrics.completedRaces).toBe(5);
|
||||
expect(sponsorship1Summary?.metrics.impressions).toBe(5000); // 5 * 10 * 100
|
||||
});
|
||||
|
||||
it('should retrieve leagues with minimal data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with minimal leagues
|
||||
it('should retrieve sponsorships with minimal data', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There is 1 league available for sponsorship
|
||||
// When: GetAvailableLeaguesUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should contain the single league
|
||||
// And: EventPublisher should emit AvailableLeaguesAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 1 sponsorship
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league);
|
||||
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season);
|
||||
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain sponsor sponsorships
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
// And: The sponsorships should contain 1 sponsorship
|
||||
expect(sponsorships.sponsorships).toHaveLength(1);
|
||||
|
||||
// And: The summary should show correct values
|
||||
expect(sponsorships.summary.totalSponsorships).toBe(1);
|
||||
expect(sponsorships.summary.activeSponsorships).toBe(1);
|
||||
expect(sponsorships.summary.totalInvestment.amount).toBe(1000);
|
||||
expect(sponsorships.summary.totalPlatformFees.amount).toBe(100);
|
||||
});
|
||||
|
||||
it('should retrieve leagues with empty result', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no available leagues
|
||||
it('should retrieve sponsorships with empty result when no sponsorships exist', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are no leagues available for sponsorship
|
||||
// When: GetAvailableLeaguesUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit AvailableLeaguesAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has no sponsorships
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called with sponsor ID
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The result should contain sponsor sponsorships
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
// And: The sponsorships should be empty
|
||||
expect(sponsorships.sponsorships).toHaveLength(0);
|
||||
|
||||
// And: The summary should show zero values
|
||||
expect(sponsorships.summary.totalSponsorships).toBe(0);
|
||||
expect(sponsorships.summary.activeSponsorships).toBe(0);
|
||||
expect(sponsorships.summary.totalInvestment.amount).toBe(0);
|
||||
expect(sponsorships.summary.totalPlatformFees.amount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetAvailableLeaguesUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
describe('GetSponsorSponsorshipsUseCase - Error Handling', () => {
|
||||
it('should return error when sponsor does not exist', async () => {
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetAvailableLeaguesUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called with non-existent sponsor ID
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'non-existent-sponsor' });
|
||||
|
||||
it('should throw error when sponsor ID is invalid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid sponsor ID
|
||||
// Given: An invalid sponsor ID (e.g., empty string, null, undefined)
|
||||
// When: GetAvailableLeaguesUseCase.execute() is called with invalid sponsor ID
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('SPONSOR_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetLeagueStatisticsUseCase - Success Path', () => {
|
||||
it('should retrieve league statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with league statistics
|
||||
describe('Sponsor Leagues Data Orchestration', () => {
|
||||
it('should correctly aggregate sponsorship metrics across multiple sponsorships', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are 10 leagues available
|
||||
// And: There are 3 main sponsor slots available
|
||||
// And: There are 15 secondary sponsor slots available
|
||||
// And: There are 500 total drivers
|
||||
// And: Average CPM is $50
|
||||
// When: GetLeagueStatisticsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show total leagues count: 10
|
||||
// And: The result should show main sponsor slots available: 3
|
||||
// And: The result should show secondary sponsor slots available: 15
|
||||
// And: The result should show total drivers count: 500
|
||||
// And: The result should show average CPM: $50
|
||||
// And: EventPublisher should emit LeagueStatisticsAccessedEvent
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
// And: The sponsor has 3 sponsorships with different investments
|
||||
const league1 = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league1);
|
||||
|
||||
const league2 = League.create({
|
||||
id: 'league-2',
|
||||
name: 'League 2',
|
||||
description: 'Description 2',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
await leagueRepository.create(league2);
|
||||
|
||||
const league3 = League.create({
|
||||
id: 'league-3',
|
||||
name: 'League 3',
|
||||
description: 'Description 3',
|
||||
ownerId: 'owner-3',
|
||||
});
|
||||
await leagueRepository.create(league3);
|
||||
|
||||
const season1 = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season1);
|
||||
|
||||
const season2 = Season.create({
|
||||
id: 'season-2',
|
||||
leagueId: 'league-2',
|
||||
name: 'Season 2',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season2);
|
||||
|
||||
const season3 = Season.create({
|
||||
id: 'season-3',
|
||||
leagueId: 'league-3',
|
||||
name: 'Season 3',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season3);
|
||||
|
||||
const sponsorship1 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship1);
|
||||
|
||||
const sponsorship2 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-2',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-2',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(2000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship2);
|
||||
|
||||
const sponsorship3 = SeasonSponsorship.create({
|
||||
id: 'sponsorship-3',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-3',
|
||||
tier: 'secondary',
|
||||
pricing: Money.create(3000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship3);
|
||||
|
||||
// And: The sponsor has different numbers of drivers and races in each league
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
driverId: `driver-1-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
driverId: `driver-2-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
driverId: `driver-3-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-1-${i}`,
|
||||
leagueId: 'league-1',
|
||||
track: 'Track 1',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-2-${i}`,
|
||||
leagueId: 'league-2',
|
||||
track: 'Track 2',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-3-${i}`,
|
||||
leagueId: 'league-3',
|
||||
track: 'Track 3',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: The metrics should be correctly aggregated
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
// Total drivers: 10 + 5 + 8 = 23
|
||||
expect(sponsorships.sponsorships[0].metrics.drivers).toBe(10);
|
||||
expect(sponsorships.sponsorships[1].metrics.drivers).toBe(5);
|
||||
expect(sponsorships.sponsorships[2].metrics.drivers).toBe(8);
|
||||
|
||||
// Total races: 5 + 3 + 4 = 12
|
||||
expect(sponsorships.sponsorships[0].metrics.races).toBe(5);
|
||||
expect(sponsorships.sponsorships[1].metrics.races).toBe(3);
|
||||
expect(sponsorships.sponsorships[2].metrics.races).toBe(4);
|
||||
|
||||
// Total investment: 1000 + 2000 + 3000 = 6000
|
||||
expect(sponsorships.summary.totalInvestment.amount).toBe(6000);
|
||||
|
||||
// Total platform fees: 100 + 200 + 300 = 600
|
||||
expect(sponsorships.summary.totalPlatformFees.amount).toBe(600);
|
||||
});
|
||||
|
||||
it('should retrieve statistics with zero values', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with no leagues
|
||||
it('should correctly calculate impressions based on completed races and drivers', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are no leagues available
|
||||
// When: GetLeagueStatisticsUseCase.execute() is called with sponsor ID
|
||||
// Then: The result should show all counts as 0
|
||||
// And: The result should show average CPM as 0
|
||||
// And: EventPublisher should emit LeagueStatisticsAccessedEvent
|
||||
});
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
describe('GetLeagueStatisticsUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: GetLeagueStatisticsUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
// And: The sponsor has 1 league with 10 drivers and 5 completed races
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league);
|
||||
|
||||
describe('FilterLeaguesUseCase - Success Path', () => {
|
||||
it('should filter leagues by "All" availability', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter by All
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season);
|
||||
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const membership = LeagueMembership.create({
|
||||
id: `membership-${i}`,
|
||||
leagueId: 'league-1',
|
||||
driverId: `driver-${i}`,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
});
|
||||
await leagueMembershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const race = Race.create({
|
||||
id: `race-${i}`,
|
||||
leagueId: 'league-1',
|
||||
track: 'Track 1',
|
||||
scheduledAt: new Date(`2025-0${i}-01`),
|
||||
status: 'completed',
|
||||
});
|
||||
await raceRepository.create(race);
|
||||
}
|
||||
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
// Then: Impressions should be calculated correctly
|
||||
// Impressions = completed races * drivers * 100 = 5 * 10 * 100 = 5000
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
expect(sponsorships.sponsorships[0].metrics.impressions).toBe(5000);
|
||||
});
|
||||
|
||||
it('should correctly calculate platform fees and net amounts', async () => {
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are 5 leagues (3 with main slot available, 2 with secondary slots available)
|
||||
// When: FilterLeaguesUseCase.execute() is called with availability "All"
|
||||
// Then: The result should contain all 5 leagues
|
||||
// And: EventPublisher should emit LeaguesFilteredEvent
|
||||
});
|
||||
const sponsor = Sponsor.create({
|
||||
id: 'sponsor-123',
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
await sponsorRepository.create(sponsor);
|
||||
|
||||
it('should filter leagues by "Main Slot Available" availability', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter by Main Slot Available
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are 5 leagues (3 with main slot available, 2 with secondary slots available)
|
||||
// When: FilterLeaguesUseCase.execute() is called with availability "Main Slot Available"
|
||||
// Then: The result should contain only 3 leagues with main slot available
|
||||
// And: EventPublisher should emit LeaguesFilteredEvent
|
||||
});
|
||||
// And: The sponsor has 1 sponsorship
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'League 1',
|
||||
description: 'Description 1',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
await leagueRepository.create(league);
|
||||
|
||||
it('should filter leagues by "Secondary Slot Available" availability', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter by Secondary Slot Available
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are 5 leagues (3 with main slot available, 2 with secondary slots available)
|
||||
// When: FilterLeaguesUseCase.execute() is called with availability "Secondary Slot Available"
|
||||
// Then: The result should contain only 2 leagues with secondary slots available
|
||||
// And: EventPublisher should emit LeaguesFilteredEvent
|
||||
});
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
});
|
||||
await seasonRepository.create(season);
|
||||
|
||||
it('should return empty result when no leagues match filter', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Filter with no matches
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are 2 leagues with main slot available
|
||||
// When: FilterLeaguesUseCase.execute() is called with availability "Secondary Slot Available"
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit LeaguesFilteredEvent
|
||||
});
|
||||
});
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: 'sponsorship-1',
|
||||
sponsorId: 'sponsor-123',
|
||||
seasonId: 'season-1',
|
||||
tier: 'main',
|
||||
pricing: Money.create(1000, 'USD'),
|
||||
status: 'active',
|
||||
});
|
||||
await seasonSponsorshipRepository.create(sponsorship);
|
||||
|
||||
describe('FilterLeaguesUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: FilterLeaguesUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
// When: GetSponsorSponsorshipsUseCase.execute() is called
|
||||
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
|
||||
|
||||
it('should throw error with invalid availability', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid availability
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// When: FilterLeaguesUseCase.execute() is called with invalid availability
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
// Then: Platform fees and net amounts should be calculated correctly
|
||||
expect(result.isOk()).toBe(true);
|
||||
const sponsorships = result.unwrap();
|
||||
|
||||
describe('SearchLeaguesUseCase - Success Path', () => {
|
||||
it('should search leagues by league name', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search by league name
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are leagues named: "Premier League", "League A", "League B"
|
||||
// When: SearchLeaguesUseCase.execute() is called with query "Premier League"
|
||||
// Then: The result should contain only "Premier League"
|
||||
// And: EventPublisher should emit LeaguesSearchedEvent
|
||||
});
|
||||
// Platform fee = 10% of pricing = 100
|
||||
expect(sponsorships.sponsorships[0].financials.platformFee.amount).toBe(100);
|
||||
|
||||
it('should search leagues by partial match', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search by partial match
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are leagues named: "Premier League", "League A", "League B"
|
||||
// When: SearchLeaguesUseCase.execute() is called with query "League"
|
||||
// Then: The result should contain all three leagues
|
||||
// And: EventPublisher should emit LeaguesSearchedEvent
|
||||
});
|
||||
|
||||
it('should return empty result when no leagues match search', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search with no matches
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are leagues named: "League A", "League B"
|
||||
// When: SearchLeaguesUseCase.execute() is called with query "NonExistent"
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit LeaguesSearchedEvent
|
||||
});
|
||||
|
||||
it('should return all leagues when search query is empty', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Search with empty query
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are 3 leagues available
|
||||
// When: SearchLeaguesUseCase.execute() is called with empty query
|
||||
// Then: The result should contain all 3 leagues
|
||||
// And: EventPublisher should emit LeaguesSearchedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchLeaguesUseCase - Error Handling', () => {
|
||||
it('should throw error when sponsor does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given ID
|
||||
// When: SearchLeaguesUseCase.execute() is called with non-existent sponsor ID
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error with invalid query', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid query
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// When: SearchLeaguesUseCase.execute() is called with invalid query (e.g., null, undefined)
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('League Data Orchestration', () => {
|
||||
it('should correctly aggregate league statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League statistics aggregation
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are 5 leagues with different slot availability
|
||||
// And: There are 3 main sponsor slots available
|
||||
// And: There are 15 secondary sponsor slots available
|
||||
// And: There are 500 total drivers
|
||||
// And: Average CPM is $50
|
||||
// When: GetLeagueStatisticsUseCase.execute() is called
|
||||
// Then: Total leagues should be 5
|
||||
// And: Main sponsor slots available should be 3
|
||||
// And: Secondary sponsor slots available should be 15
|
||||
// And: Total drivers count should be 500
|
||||
// And: Average CPM should be $50
|
||||
// And: EventPublisher should emit LeagueStatisticsAccessedEvent
|
||||
});
|
||||
|
||||
it('should correctly filter leagues by availability', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League availability filtering
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are leagues with different slot availability
|
||||
// When: FilterLeaguesUseCase.execute() is called with "Main Slot Available"
|
||||
// Then: Only leagues with main slot available should be returned
|
||||
// And: Each league should have correct availability
|
||||
// And: EventPublisher should emit LeaguesFilteredEvent
|
||||
});
|
||||
|
||||
it('should correctly search leagues by name', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League name search
|
||||
// Given: A sponsor exists with ID "sponsor-123"
|
||||
// And: There are leagues with different names
|
||||
// When: SearchLeaguesUseCase.execute() is called with league name
|
||||
// Then: Only leagues with matching names should be returned
|
||||
// And: Each league should have correct name
|
||||
// And: EventPublisher should emit LeaguesSearchedEvent
|
||||
// Net amount = pricing - platform fee = 1000 - 100 = 900
|
||||
expect(sponsorships.sponsorships[0].financials.netAmount.amount).toBe(900);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,241 +1,282 @@
|
||||
/**
|
||||
* Integration Test: Sponsor Signup Use Case Orchestration
|
||||
*
|
||||
*
|
||||
* Tests the orchestration logic of sponsor signup-related Use Cases:
|
||||
* - CreateSponsorUseCase: Creates a new sponsor account
|
||||
* - SponsorLoginUseCase: Authenticates a sponsor
|
||||
* - SponsorLogoutUseCase: Logs out a sponsor
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemorySponsorRepository } from '../../../adapters/sponsors/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { CreateSponsorUseCase } from '../../../core/sponsors/use-cases/CreateSponsorUseCase';
|
||||
import { SponsorLoginUseCase } from '../../../core/sponsors/use-cases/SponsorLoginUseCase';
|
||||
import { SponsorLogoutUseCase } from '../../../core/sponsors/use-cases/SponsorLogoutUseCase';
|
||||
import { CreateSponsorCommand } from '../../../core/sponsors/ports/CreateSponsorCommand';
|
||||
import { SponsorLoginCommand } from '../../../core/sponsors/ports/SponsorLoginCommand';
|
||||
import { SponsorLogoutCommand } from '../../../core/sponsors/ports/SponsorLogoutCommand';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemorySponsorRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { CreateSponsorUseCase } from '../../../core/racing/application/use-cases/CreateSponsorUseCase';
|
||||
import { Sponsor } from '../../../core/racing/domain/entities/sponsor/Sponsor';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Sponsor Signup Use Case Orchestration', () => {
|
||||
let sponsorRepository: InMemorySponsorRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let createSponsorUseCase: CreateSponsorUseCase;
|
||||
let sponsorLoginUseCase: SponsorLoginUseCase;
|
||||
let sponsorLogoutUseCase: SponsorLogoutUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// sponsorRepository = new InMemorySponsorRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// createSponsorUseCase = new CreateSponsorUseCase({
|
||||
// sponsorRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// sponsorLoginUseCase = new SponsorLoginUseCase({
|
||||
// sponsorRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// sponsorLogoutUseCase = new SponsorLogoutUseCase({
|
||||
// sponsorRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
sponsorRepository = new InMemorySponsorRepository(mockLogger);
|
||||
createSponsorUseCase = new CreateSponsorUseCase(sponsorRepository, mockLogger);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// sponsorRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
sponsorRepository.clear();
|
||||
});
|
||||
|
||||
describe('CreateSponsorUseCase - Success Path', () => {
|
||||
it('should create a new sponsor account with valid information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor creates account
|
||||
// Given: No sponsor exists with the given email
|
||||
const sponsorId = 'sponsor-123';
|
||||
const sponsorData = {
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
websiteUrl: 'https://testcompany.com',
|
||||
logoUrl: 'https://testcompany.com/logo.png',
|
||||
};
|
||||
|
||||
// When: CreateSponsorUseCase.execute() is called with valid sponsor data
|
||||
// Then: The sponsor should be created in the repository
|
||||
const result = await createSponsorUseCase.execute(sponsorData);
|
||||
|
||||
// Then: The sponsor should be created successfully
|
||||
expect(result.isOk()).toBe(true);
|
||||
const createdSponsor = result.unwrap().sponsor;
|
||||
|
||||
// And: The sponsor should have a unique ID
|
||||
expect(createdSponsor.id.toString()).toBeDefined();
|
||||
|
||||
// And: The sponsor should have the provided company name
|
||||
expect(createdSponsor.name.toString()).toBe('Test Company');
|
||||
|
||||
// And: The sponsor should have the provided contact email
|
||||
expect(createdSponsor.contactEmail.toString()).toBe('test@example.com');
|
||||
|
||||
// And: The sponsor should have the provided website URL
|
||||
// And: The sponsor should have the provided sponsorship interests
|
||||
expect(createdSponsor.websiteUrl?.toString()).toBe('https://testcompany.com');
|
||||
|
||||
// And: The sponsor should have the provided logo URL
|
||||
expect(createdSponsor.logoUrl?.toString()).toBe('https://testcompany.com/logo.png');
|
||||
|
||||
// And: The sponsor should have a created timestamp
|
||||
// And: EventPublisher should emit SponsorCreatedEvent
|
||||
expect(createdSponsor.createdAt).toBeDefined();
|
||||
|
||||
// And: The sponsor should be retrievable from the repository
|
||||
const retrievedSponsor = await sponsorRepository.findById(createdSponsor.id.toString());
|
||||
expect(retrievedSponsor).toBeDefined();
|
||||
expect(retrievedSponsor?.name.toString()).toBe('Test Company');
|
||||
});
|
||||
|
||||
it('should create a sponsor with multiple sponsorship interests', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor creates account with multiple interests
|
||||
// Given: No sponsor exists with the given email
|
||||
// When: CreateSponsorUseCase.execute() is called with multiple sponsorship interests
|
||||
// Then: The sponsor should be created with all selected interests
|
||||
// And: Each interest should be stored correctly
|
||||
// And: EventPublisher should emit SponsorCreatedEvent
|
||||
it('should create a sponsor with minimal data', async () => {
|
||||
// Given: No sponsor exists
|
||||
const sponsorData = {
|
||||
name: 'Minimal Company',
|
||||
contactEmail: 'minimal@example.com',
|
||||
};
|
||||
|
||||
// When: CreateSponsorUseCase.execute() is called with minimal data
|
||||
const result = await createSponsorUseCase.execute(sponsorData);
|
||||
|
||||
// Then: The sponsor should be created successfully
|
||||
expect(result.isOk()).toBe(true);
|
||||
const createdSponsor = result.unwrap().sponsor;
|
||||
|
||||
// And: The sponsor should have the provided company name
|
||||
expect(createdSponsor.name.toString()).toBe('Minimal Company');
|
||||
|
||||
// And: The sponsor should have the provided contact email
|
||||
expect(createdSponsor.contactEmail.toString()).toBe('minimal@example.com');
|
||||
|
||||
// And: Optional fields should be undefined
|
||||
expect(createdSponsor.websiteUrl).toBeUndefined();
|
||||
expect(createdSponsor.logoUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create a sponsor with optional company logo', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor creates account with logo
|
||||
// Given: No sponsor exists with the given email
|
||||
// When: CreateSponsorUseCase.execute() is called with a company logo
|
||||
// Then: The sponsor should be created with the logo reference
|
||||
// And: The logo should be stored in the media repository
|
||||
// And: EventPublisher should emit SponsorCreatedEvent
|
||||
});
|
||||
it('should create a sponsor with optional fields only', async () => {
|
||||
// Given: No sponsor exists
|
||||
const sponsorData = {
|
||||
name: 'Optional Fields Company',
|
||||
contactEmail: 'optional@example.com',
|
||||
websiteUrl: 'https://optional.com',
|
||||
};
|
||||
|
||||
it('should create a sponsor with default settings', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor creates account with default settings
|
||||
// Given: No sponsor exists with the given email
|
||||
// When: CreateSponsorUseCase.execute() is called
|
||||
// Then: The sponsor should be created with default notification preferences
|
||||
// And: The sponsor should be created with default privacy settings
|
||||
// And: EventPublisher should emit SponsorCreatedEvent
|
||||
// When: CreateSponsorUseCase.execute() is called with optional fields
|
||||
const result = await createSponsorUseCase.execute(sponsorData);
|
||||
|
||||
// Then: The sponsor should be created successfully
|
||||
expect(result.isOk()).toBe(true);
|
||||
const createdSponsor = result.unwrap().sponsor;
|
||||
|
||||
// And: The sponsor should have the provided website URL
|
||||
expect(createdSponsor.websiteUrl?.toString()).toBe('https://optional.com');
|
||||
|
||||
// And: Logo URL should be undefined
|
||||
expect(createdSponsor.logoUrl).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateSponsorUseCase - Validation', () => {
|
||||
it('should reject sponsor creation with duplicate email', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Duplicate email
|
||||
// Given: A sponsor exists with email "sponsor@example.com"
|
||||
const existingSponsor = Sponsor.create({
|
||||
id: 'existing-sponsor',
|
||||
name: 'Existing Company',
|
||||
contactEmail: 'sponsor@example.com',
|
||||
});
|
||||
await sponsorRepository.create(existingSponsor);
|
||||
|
||||
// When: CreateSponsorUseCase.execute() is called with the same email
|
||||
// Then: Should throw DuplicateEmailError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
const result = await createSponsorUseCase.execute({
|
||||
name: 'New Company',
|
||||
contactEmail: 'sponsor@example.com',
|
||||
});
|
||||
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('REPOSITORY_ERROR');
|
||||
});
|
||||
|
||||
it('should reject sponsor creation with invalid email format', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid email format
|
||||
// Given: No sponsor exists
|
||||
// When: CreateSponsorUseCase.execute() is called with invalid email
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
const result = await createSponsorUseCase.execute({
|
||||
name: 'Test Company',
|
||||
contactEmail: 'invalid-email',
|
||||
});
|
||||
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
expect(error.details.message).toContain('Invalid sponsor contact email format');
|
||||
});
|
||||
|
||||
it('should reject sponsor creation with missing required fields', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Missing required fields
|
||||
// Given: No sponsor exists
|
||||
// When: CreateSponsorUseCase.execute() is called without company name
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
const result = await createSponsorUseCase.execute({
|
||||
name: '',
|
||||
contactEmail: 'test@example.com',
|
||||
});
|
||||
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
expect(error.details.message).toContain('Sponsor name is required');
|
||||
});
|
||||
|
||||
it('should reject sponsor creation with invalid website URL', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid website URL
|
||||
// Given: No sponsor exists
|
||||
// When: CreateSponsorUseCase.execute() is called with invalid URL
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
const result = await createSponsorUseCase.execute({
|
||||
name: 'Test Company',
|
||||
contactEmail: 'test@example.com',
|
||||
websiteUrl: 'not-a-valid-url',
|
||||
});
|
||||
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
expect(error.details.message).toContain('Invalid sponsor website URL');
|
||||
});
|
||||
|
||||
it('should reject sponsor creation with invalid password', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid password
|
||||
it('should reject sponsor creation with missing email', async () => {
|
||||
// Given: No sponsor exists
|
||||
// When: CreateSponsorUseCase.execute() is called with weak password
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
// When: CreateSponsorUseCase.execute() is called without email
|
||||
const result = await createSponsorUseCase.execute({
|
||||
name: 'Test Company',
|
||||
contactEmail: '',
|
||||
});
|
||||
|
||||
describe('SponsorLoginUseCase - Success Path', () => {
|
||||
it('should authenticate sponsor with valid credentials', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor logs in
|
||||
// Given: A sponsor exists with email "sponsor@example.com" and password "password123"
|
||||
// When: SponsorLoginUseCase.execute() is called with valid credentials
|
||||
// Then: The sponsor should be authenticated
|
||||
// And: The sponsor should receive an authentication token
|
||||
// And: EventPublisher should emit SponsorLoggedInEvent
|
||||
});
|
||||
|
||||
it('should authenticate sponsor with correct email and password', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor logs in with correct credentials
|
||||
// Given: A sponsor exists with specific credentials
|
||||
// When: SponsorLoginUseCase.execute() is called with matching credentials
|
||||
// Then: The sponsor should be authenticated
|
||||
// And: EventPublisher should emit SponsorLoggedInEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('SponsorLoginUseCase - Error Handling', () => {
|
||||
it('should reject login with non-existent email', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent sponsor
|
||||
// Given: No sponsor exists with the given email
|
||||
// When: SponsorLoginUseCase.execute() is called
|
||||
// Then: Should throw SponsorNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject login with incorrect password', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Incorrect password
|
||||
// Given: A sponsor exists with email "sponsor@example.com"
|
||||
// When: SponsorLoginUseCase.execute() is called with wrong password
|
||||
// Then: Should throw InvalidCredentialsError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject login with invalid email format', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid email format
|
||||
// Given: No sponsor exists
|
||||
// When: SponsorLoginUseCase.execute() is called with invalid email
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('SponsorLogoutUseCase - Success Path', () => {
|
||||
it('should log out authenticated sponsor', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor logs out
|
||||
// Given: A sponsor is authenticated
|
||||
// When: SponsorLogoutUseCase.execute() is called
|
||||
// Then: The sponsor should be logged out
|
||||
// And: EventPublisher should emit SponsorLoggedOutEvent
|
||||
// Then: Should return an error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
expect(error.details.message).toContain('Sponsor contact email is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sponsor Data Orchestration', () => {
|
||||
it('should correctly create sponsor with sponsorship interests', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with multiple interests
|
||||
it('should correctly create sponsor with all optional fields', async () => {
|
||||
// Given: No sponsor exists
|
||||
// When: CreateSponsorUseCase.execute() is called with interests: ["League", "Team", "Driver"]
|
||||
// Then: The sponsor should have all three interests stored
|
||||
// And: Each interest should be retrievable
|
||||
// And: EventPublisher should emit SponsorCreatedEvent
|
||||
const sponsorData = {
|
||||
name: 'Full Featured Company',
|
||||
contactEmail: 'full@example.com',
|
||||
websiteUrl: 'https://fullfeatured.com',
|
||||
logoUrl: 'https://fullfeatured.com/logo.png',
|
||||
};
|
||||
|
||||
// When: CreateSponsorUseCase.execute() is called with all fields
|
||||
const result = await createSponsorUseCase.execute(sponsorData);
|
||||
|
||||
// Then: The sponsor should be created with all fields
|
||||
expect(result.isOk()).toBe(true);
|
||||
const createdSponsor = result.unwrap().sponsor;
|
||||
|
||||
expect(createdSponsor.name.toString()).toBe('Full Featured Company');
|
||||
expect(createdSponsor.contactEmail.toString()).toBe('full@example.com');
|
||||
expect(createdSponsor.websiteUrl?.toString()).toBe('https://fullfeatured.com');
|
||||
expect(createdSponsor.logoUrl?.toString()).toBe('https://fullfeatured.com/logo.png');
|
||||
expect(createdSponsor.createdAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should correctly create sponsor with default notification preferences', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with default notifications
|
||||
// Given: No sponsor exists
|
||||
// When: CreateSponsorUseCase.execute() is called
|
||||
// Then: The sponsor should have default notification preferences
|
||||
// And: All notification types should be enabled by default
|
||||
// And: EventPublisher should emit SponsorCreatedEvent
|
||||
it('should generate unique IDs for each sponsor', async () => {
|
||||
// Given: No sponsors exist
|
||||
const sponsorData1 = {
|
||||
name: 'Company 1',
|
||||
contactEmail: 'company1@example.com',
|
||||
};
|
||||
const sponsorData2 = {
|
||||
name: 'Company 2',
|
||||
contactEmail: 'company2@example.com',
|
||||
};
|
||||
|
||||
// When: Creating two sponsors
|
||||
const result1 = await createSponsorUseCase.execute(sponsorData1);
|
||||
const result2 = await createSponsorUseCase.execute(sponsorData2);
|
||||
|
||||
// Then: Both should succeed and have unique IDs
|
||||
expect(result1.isOk()).toBe(true);
|
||||
expect(result2.isOk()).toBe(true);
|
||||
|
||||
const sponsor1 = result1.unwrap().sponsor;
|
||||
const sponsor2 = result2.unwrap().sponsor;
|
||||
|
||||
expect(sponsor1.id.toString()).not.toBe(sponsor2.id.toString());
|
||||
});
|
||||
|
||||
it('should correctly create sponsor with default privacy settings', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sponsor with default privacy
|
||||
it('should persist sponsor in repository after creation', async () => {
|
||||
// Given: No sponsor exists
|
||||
// When: CreateSponsorUseCase.execute() is called
|
||||
// Then: The sponsor should have default privacy settings
|
||||
// And: Public profile should be enabled by default
|
||||
// And: EventPublisher should emit SponsorCreatedEvent
|
||||
const sponsorData = {
|
||||
name: 'Persistent Company',
|
||||
contactEmail: 'persistent@example.com',
|
||||
};
|
||||
|
||||
// When: Creating a sponsor
|
||||
const result = await createSponsorUseCase.execute(sponsorData);
|
||||
|
||||
// Then: The sponsor should be retrievable from the repository
|
||||
expect(result.isOk()).toBe(true);
|
||||
const createdSponsor = result.unwrap().sponsor;
|
||||
|
||||
const retrievedSponsor = await sponsorRepository.findById(createdSponsor.id.toString());
|
||||
expect(retrievedSponsor).toBeDefined();
|
||||
expect(retrievedSponsor?.name.toString()).toBe('Persistent Company');
|
||||
expect(retrievedSponsor?.contactEmail.toString()).toBe('persistent@example.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,663 +2,200 @@
|
||||
* Integration Test: Team Admin Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of team admin-related Use Cases:
|
||||
* - RemoveTeamMemberUseCase: Admin removes team member
|
||||
* - PromoteTeamMemberUseCase: Admin promotes team member to captain
|
||||
* - UpdateTeamDetailsUseCase: Admin updates team details
|
||||
* - DeleteTeamUseCase: Admin deletes team
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers, File Storage)
|
||||
* - UpdateTeamUseCase: Admin updates team details
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemoryTeamRepository } from '../../../adapters/teams/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { InMemoryFileStorage } from '../../../adapters/files/InMemoryFileStorage';
|
||||
import { RemoveTeamMemberUseCase } from '../../../core/teams/use-cases/RemoveTeamMemberUseCase';
|
||||
import { PromoteTeamMemberUseCase } from '../../../core/teams/use-cases/PromoteTeamMemberUseCase';
|
||||
import { UpdateTeamDetailsUseCase } from '../../../core/teams/use-cases/UpdateTeamDetailsUseCase';
|
||||
import { DeleteTeamUseCase } from '../../../core/teams/use-cases/DeleteTeamUseCase';
|
||||
import { RemoveTeamMemberCommand } from '../../../core/teams/ports/RemoveTeamMemberCommand';
|
||||
import { PromoteTeamMemberCommand } from '../../../core/teams/ports/PromoteTeamMemberCommand';
|
||||
import { UpdateTeamDetailsCommand } from '../../../core/teams/ports/UpdateTeamDetailsCommand';
|
||||
import { DeleteTeamCommand } from '../../../core/teams/ports/DeleteTeamCommand';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { UpdateTeamUseCase } from '../../../core/racing/application/use-cases/UpdateTeamUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Team Admin Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let fileStorage: InMemoryFileStorage;
|
||||
let removeTeamMemberUseCase: RemoveTeamMemberUseCase;
|
||||
let promoteTeamMemberUseCase: PromoteTeamMemberUseCase;
|
||||
let updateTeamDetailsUseCase: UpdateTeamDetailsUseCase;
|
||||
let deleteTeamUseCase: DeleteTeamUseCase;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let updateTeamUseCase: UpdateTeamUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories, event publisher, and file storage
|
||||
// teamRepository = new InMemoryTeamRepository();
|
||||
// driverRepository = new InMemoryDriverRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// fileStorage = new InMemoryFileStorage();
|
||||
// removeTeamMemberUseCase = new RemoveTeamMemberUseCase({
|
||||
// teamRepository,
|
||||
// driverRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// promoteTeamMemberUseCase = new PromoteTeamMemberUseCase({
|
||||
// teamRepository,
|
||||
// driverRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
// updateTeamDetailsUseCase = new UpdateTeamDetailsUseCase({
|
||||
// teamRepository,
|
||||
// driverRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// fileStorage,
|
||||
// });
|
||||
// deleteTeamUseCase = new DeleteTeamUseCase({
|
||||
// teamRepository,
|
||||
// driverRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
updateTeamUseCase = new UpdateTeamUseCase(teamRepository, membershipRepository);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// teamRepository.clear();
|
||||
// driverRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
// fileStorage.clear();
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
});
|
||||
|
||||
describe('RemoveTeamMemberUseCase - Success Path', () => {
|
||||
it('should remove a team member', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin removes team member
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// And: A driver is a member of the team
|
||||
// When: RemoveTeamMemberUseCase.execute() is called
|
||||
// Then: The driver should be removed from the team roster
|
||||
// And: EventPublisher should emit TeamMemberRemovedEvent
|
||||
describe('UpdateTeamUseCase - Success Path', () => {
|
||||
it('should update team details when called by owner', async () => {
|
||||
// Scenario: Owner updates team details
|
||||
// Given: A team exists
|
||||
const teamId = 't1';
|
||||
const ownerId = 'o1';
|
||||
const team = Team.create({ id: teamId, name: 'Old Name', tag: 'OLD', description: 'Old Desc', ownerId, leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is the owner
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: UpdateTeamUseCase.execute() is called
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: ownerId,
|
||||
updates: {
|
||||
name: 'New Name',
|
||||
tag: 'NEW',
|
||||
description: 'New Desc'
|
||||
}
|
||||
});
|
||||
|
||||
// Then: The team should be updated successfully
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: updatedTeam } = result.unwrap();
|
||||
expect(updatedTeam.name.toString()).toBe('New Name');
|
||||
expect(updatedTeam.tag.toString()).toBe('NEW');
|
||||
expect(updatedTeam.description.toString()).toBe('New Desc');
|
||||
|
||||
// And: The changes should be in the repository
|
||||
const savedTeam = await teamRepository.findById(teamId);
|
||||
expect(savedTeam?.name.toString()).toBe('New Name');
|
||||
});
|
||||
|
||||
it('should remove a team member with removal reason', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin removes team member with reason
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// And: A driver is a member of the team
|
||||
// When: RemoveTeamMemberUseCase.execute() is called with removal reason
|
||||
// Then: The driver should be removed from the team roster
|
||||
// And: EventPublisher should emit TeamMemberRemovedEvent
|
||||
});
|
||||
it('should update team details when called by manager', async () => {
|
||||
// Scenario: Manager updates team details
|
||||
// Given: A team exists
|
||||
const teamId = 't2';
|
||||
const managerId = 'm2';
|
||||
const team = Team.create({ id: teamId, name: 'Team 2', tag: 'T2', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is a manager
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: managerId,
|
||||
role: 'manager',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
it('should remove a team member when team has minimum members', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team has minimum members
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with minimum members (e.g., 2 members)
|
||||
// And: A driver is a member of the team
|
||||
// When: RemoveTeamMemberUseCase.execute() is called
|
||||
// Then: The driver should be removed from the team roster
|
||||
// And: EventPublisher should emit TeamMemberRemovedEvent
|
||||
// When: UpdateTeamUseCase.execute() is called
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: managerId,
|
||||
updates: {
|
||||
name: 'Updated by Manager'
|
||||
}
|
||||
});
|
||||
|
||||
// Then: The team should be updated successfully
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: updatedTeam } = result.unwrap();
|
||||
expect(updatedTeam.name.toString()).toBe('Updated by Manager');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RemoveTeamMemberUseCase - Validation', () => {
|
||||
it('should reject removal when removing the captain', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Attempt to remove captain
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: RemoveTeamMemberUseCase.execute() is called with captain ID
|
||||
// Then: Should throw CannotRemoveCaptainError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
describe('UpdateTeamUseCase - Validation', () => {
|
||||
it('should reject update when called by regular member', async () => {
|
||||
// Scenario: Regular member tries to update team
|
||||
// Given: A team exists
|
||||
const teamId = 't3';
|
||||
const memberId = 'd3';
|
||||
const team = Team.create({ id: teamId, name: 'Team 3', tag: 'T3', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is a regular member
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: memberId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: UpdateTeamUseCase.execute() is called
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: memberId,
|
||||
updates: {
|
||||
name: 'Unauthorized Update'
|
||||
}
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('PERMISSION_DENIED');
|
||||
});
|
||||
|
||||
it('should reject removal when member does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team member
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: A driver is not a member of the team
|
||||
// When: RemoveTeamMemberUseCase.execute() is called
|
||||
// Then: Should throw TeamMemberNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
it('should reject update when called by non-member', async () => {
|
||||
// Scenario: Non-member tries to update team
|
||||
// Given: A team exists
|
||||
const teamId = 't4';
|
||||
const team = Team.create({ id: teamId, name: 'Team 4', tag: 'T4', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: UpdateTeamUseCase.execute() is called
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: 'non-member',
|
||||
updates: {
|
||||
name: 'Unauthorized Update'
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject removal with invalid reason length', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid reason length
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// And: A driver is a member of the team
|
||||
// When: RemoveTeamMemberUseCase.execute() is called with reason exceeding limit
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('PERMISSION_DENIED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RemoveTeamMemberUseCase - Error Handling', () => {
|
||||
it('should throw error when team captain does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team captain
|
||||
// Given: No team captain exists with the given ID
|
||||
// When: RemoveTeamMemberUseCase.execute() is called with non-existent captain ID
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
describe('UpdateTeamUseCase - Error Handling', () => {
|
||||
it('should throw error when team does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team
|
||||
// Given: A team captain exists
|
||||
// And: No team exists with the given ID
|
||||
// When: RemoveTeamMemberUseCase.execute() is called with non-existent team ID
|
||||
// Then: Should throw TeamNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
// Given: A driver exists who is a manager of some team
|
||||
const managerId = 'm5';
|
||||
await membershipRepository.saveMembership({
|
||||
teamId: 'some-team',
|
||||
driverId: managerId,
|
||||
role: 'manager',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: TeamRepository throws an error during update
|
||||
// When: RemoveTeamMemberUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
// When: UpdateTeamUseCase.execute() is called with non-existent team ID
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId: 'nonexistent',
|
||||
updatedBy: managerId,
|
||||
updates: {
|
||||
name: 'New Name'
|
||||
}
|
||||
});
|
||||
|
||||
describe('PromoteTeamMemberUseCase - Success Path', () => {
|
||||
it('should promote a team member to captain', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin promotes member to captain
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// And: A driver is a member of the team
|
||||
// When: PromoteTeamMemberUseCase.execute() is called
|
||||
// Then: The driver should become the new captain
|
||||
// And: The previous captain should be demoted to admin
|
||||
// And: EventPublisher should emit TeamMemberPromotedEvent
|
||||
// And: EventPublisher should emit TeamCaptainChangedEvent
|
||||
});
|
||||
|
||||
it('should promote a team member with promotion reason', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin promotes member with reason
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// And: A driver is a member of the team
|
||||
// When: PromoteTeamMemberUseCase.execute() is called with promotion reason
|
||||
// Then: The driver should become the new captain
|
||||
// And: EventPublisher should emit TeamMemberPromotedEvent
|
||||
});
|
||||
|
||||
it('should promote a team member when team has minimum members', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team has minimum members
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with minimum members (e.g., 2 members)
|
||||
// And: A driver is a member of the team
|
||||
// When: PromoteTeamMemberUseCase.execute() is called
|
||||
// Then: The driver should become the new captain
|
||||
// And: EventPublisher should emit TeamMemberPromotedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('PromoteTeamMemberUseCase - Validation', () => {
|
||||
it('should reject promotion when member does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team member
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: A driver is not a member of the team
|
||||
// When: PromoteTeamMemberUseCase.execute() is called
|
||||
// Then: Should throw TeamMemberNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject promotion with invalid reason length', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid reason length
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// And: A driver is a member of the team
|
||||
// When: PromoteTeamMemberUseCase.execute() is called with reason exceeding limit
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('PromoteTeamMemberUseCase - Error Handling', () => {
|
||||
it('should throw error when team captain does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team captain
|
||||
// Given: No team captain exists with the given ID
|
||||
// When: PromoteTeamMemberUseCase.execute() is called with non-existent captain ID
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when team does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team
|
||||
// Given: A team captain exists
|
||||
// And: No team exists with the given ID
|
||||
// When: PromoteTeamMemberUseCase.execute() is called with non-existent team ID
|
||||
// Then: Should throw TeamNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: TeamRepository throws an error during update
|
||||
// When: PromoteTeamMemberUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateTeamDetailsUseCase - Success Path', () => {
|
||||
it('should update team details', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin updates team details
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called
|
||||
// Then: The team details should be updated
|
||||
// And: EventPublisher should emit TeamDetailsUpdatedEvent
|
||||
});
|
||||
|
||||
it('should update team details with logo', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin updates team logo
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: A logo file is provided
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with logo
|
||||
// Then: The logo should be stored in file storage
|
||||
// And: The team should reference the new logo URL
|
||||
// And: EventPublisher should emit TeamDetailsUpdatedEvent
|
||||
});
|
||||
|
||||
it('should update team details with description', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin updates team description
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with description
|
||||
// Then: The team description should be updated
|
||||
// And: EventPublisher should emit TeamDetailsUpdatedEvent
|
||||
});
|
||||
|
||||
it('should update team details with roster size', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin updates roster size
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with roster size
|
||||
// Then: The team roster size should be updated
|
||||
// And: EventPublisher should emit TeamDetailsUpdatedEvent
|
||||
});
|
||||
|
||||
it('should update team details with social links', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin updates social links
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with social links
|
||||
// Then: The team social links should be updated
|
||||
// And: EventPublisher should emit TeamDetailsUpdatedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateTeamDetailsUseCase - Validation', () => {
|
||||
it('should reject update with empty team name', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Update with empty name
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with empty team name
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject update with invalid team name format', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Update with invalid name format
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with invalid team name
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject update with team name exceeding character limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Update with name exceeding limit
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with name exceeding limit
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject update with description exceeding character limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Update with description exceeding limit
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with description exceeding limit
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject update with invalid roster size', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Update with invalid roster size
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with invalid roster size
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject update with invalid logo format', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Update with invalid logo format
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with invalid logo format
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject update with oversized logo', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Update with oversized logo
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with oversized logo
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject update when team name already exists', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Duplicate team name
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: Another team with the same name already exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with duplicate team name
|
||||
// Then: Should throw TeamNameAlreadyExistsError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject update with roster size exceeding league limits', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Roster size exceeds league limit
|
||||
// Given: A team captain exists
|
||||
// And: A team exists in a league with max roster size of 10
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with roster size 15
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateTeamDetailsUseCase - Error Handling', () => {
|
||||
it('should throw error when team captain does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team captain
|
||||
// Given: No team captain exists with the given ID
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with non-existent captain ID
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when team does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team
|
||||
// Given: A team captain exists
|
||||
// And: No team exists with the given ID
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with non-existent team ID
|
||||
// Then: Should throw TeamNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when league does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent league
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: No league exists with the given ID
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with non-existent league ID
|
||||
// Then: Should throw LeagueNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: TeamRepository throws an error during update
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle file storage errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: File storage throws error
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: FileStorage throws an error during upload
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with logo
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteTeamUseCase - Success Path', () => {
|
||||
it('should delete a team', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin deletes team
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: DeleteTeamUseCase.execute() is called
|
||||
// Then: The team should be deleted from the repository
|
||||
// And: EventPublisher should emit TeamDeletedEvent
|
||||
});
|
||||
|
||||
it('should delete a team with deletion reason', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Admin deletes team with reason
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: DeleteTeamUseCase.execute() is called with deletion reason
|
||||
// Then: The team should be deleted
|
||||
// And: EventPublisher should emit TeamDeletedEvent
|
||||
});
|
||||
|
||||
it('should delete a team with members', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Delete team with members
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// When: DeleteTeamUseCase.execute() is called
|
||||
// Then: The team should be deleted
|
||||
// And: All team members should be removed from the team
|
||||
// And: EventPublisher should emit TeamDeletedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteTeamUseCase - Validation', () => {
|
||||
it('should reject deletion with invalid reason length', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid reason length
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: DeleteTeamUseCase.execute() is called with reason exceeding limit
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteTeamUseCase - Error Handling', () => {
|
||||
it('should throw error when team captain does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team captain
|
||||
// Given: No team captain exists with the given ID
|
||||
// When: DeleteTeamUseCase.execute() is called with non-existent captain ID
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should throw error when team does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team
|
||||
// Given: A team captain exists
|
||||
// And: No team exists with the given ID
|
||||
// When: DeleteTeamUseCase.execute() is called with non-existent team ID
|
||||
// Then: Should throw TeamNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// And: TeamRepository throws an error during delete
|
||||
// When: DeleteTeamUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Team Admin Data Orchestration', () => {
|
||||
it('should correctly track team roster after member removal', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Roster tracking after removal
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// When: RemoveTeamMemberUseCase.execute() is called
|
||||
// Then: The team roster should be updated
|
||||
// And: The removed member should not be in the roster
|
||||
});
|
||||
|
||||
it('should correctly track team captain after promotion', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Captain tracking after promotion
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// When: PromoteTeamMemberUseCase.execute() is called
|
||||
// Then: The promoted member should be the new captain
|
||||
// And: The previous captain should be demoted to admin
|
||||
});
|
||||
|
||||
it('should correctly update team details', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team details update
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called
|
||||
// Then: The team details should be updated in the repository
|
||||
// And: The updated details should be reflected in the team
|
||||
});
|
||||
|
||||
it('should correctly delete team and all related data', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team deletion
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with members and data
|
||||
// When: DeleteTeamUseCase.execute() is called
|
||||
// Then: The team should be deleted from the repository
|
||||
// And: All team-related data should be removed
|
||||
});
|
||||
|
||||
it('should validate roster size against league limits on update', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Roster size validation on update
|
||||
// Given: A team captain exists
|
||||
// And: A team exists in a league with max roster size of 10
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called with roster size 15
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Team Admin Event Orchestration', () => {
|
||||
it('should emit TeamMemberRemovedEvent with correct payload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event emission on member removal
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// When: RemoveTeamMemberUseCase.execute() is called
|
||||
// Then: EventPublisher should emit TeamMemberRemovedEvent
|
||||
// And: The event should contain team ID, removed member ID, and captain ID
|
||||
});
|
||||
|
||||
it('should emit TeamMemberPromotedEvent with correct payload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event emission on member promotion
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// When: PromoteTeamMemberUseCase.execute() is called
|
||||
// Then: EventPublisher should emit TeamMemberPromotedEvent
|
||||
// And: The event should contain team ID, promoted member ID, and captain ID
|
||||
});
|
||||
|
||||
it('should emit TeamCaptainChangedEvent with correct payload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event emission on captain change
|
||||
// Given: A team captain exists
|
||||
// And: A team exists with multiple members
|
||||
// When: PromoteTeamMemberUseCase.execute() is called
|
||||
// Then: EventPublisher should emit TeamCaptainChangedEvent
|
||||
// And: The event should contain team ID, new captain ID, and old captain ID
|
||||
});
|
||||
|
||||
it('should emit TeamDetailsUpdatedEvent with correct payload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event emission on team details update
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: UpdateTeamDetailsUseCase.execute() is called
|
||||
// Then: EventPublisher should emit TeamDetailsUpdatedEvent
|
||||
// And: The event should contain team ID and updated fields
|
||||
});
|
||||
|
||||
it('should emit TeamDeletedEvent with correct payload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event emission on team deletion
|
||||
// Given: A team captain exists
|
||||
// And: A team exists
|
||||
// When: DeleteTeamUseCase.execute() is called
|
||||
// Then: EventPublisher should emit TeamDeletedEvent
|
||||
// And: The event should contain team ID and captain ID
|
||||
});
|
||||
|
||||
it('should not emit events on validation failure', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No events on validation failure
|
||||
// Given: Invalid parameters
|
||||
// When: Any use case is called with invalid data
|
||||
// Then: EventPublisher should NOT emit any events
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('PERMISSION_DENIED'); // Because membership check fails first
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,344 +1,403 @@
|
||||
/**
|
||||
* Integration Test: Team Creation Use Case Orchestration
|
||||
*
|
||||
*
|
||||
* Tests the orchestration logic of team creation-related Use Cases:
|
||||
* - CreateTeamUseCase: Creates a new team with name, description, logo, league, tier, and roster size
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers, File Storage)
|
||||
* - CreateTeamUseCase: Creates a new team with name, description, and leagues
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemoryTeamRepository } from '../../../adapters/teams/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { InMemoryFileStorage } from '../../../adapters/files/InMemoryFileStorage';
|
||||
import { CreateTeamUseCase } from '../../../core/teams/use-cases/CreateTeamUseCase';
|
||||
import { CreateTeamCommand } from '../../../core/teams/ports/CreateTeamCommand';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { CreateTeamUseCase } from '../../../core/racing/application/use-cases/CreateTeamUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Driver } from '../../../core/racing/domain/entities/Driver';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Team Creation Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let fileStorage: InMemoryFileStorage;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let createTeamUseCase: CreateTeamUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories, event publisher, and file storage
|
||||
// teamRepository = new InMemoryTeamRepository();
|
||||
// driverRepository = new InMemoryDriverRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// fileStorage = new InMemoryFileStorage();
|
||||
// createTeamUseCase = new CreateTeamUseCase({
|
||||
// teamRepository,
|
||||
// driverRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// fileStorage,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
createTeamUseCase = new CreateTeamUseCase(teamRepository, membershipRepository, mockLogger);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// teamRepository.clear();
|
||||
// driverRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
// fileStorage.clear();
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
});
|
||||
|
||||
describe('CreateTeamUseCase - Success Path', () => {
|
||||
it('should create a team with all required fields', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with complete information
|
||||
// Given: A driver exists
|
||||
const driverId = 'd1';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '1', name: 'John Doe', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
// And: A tier exists
|
||||
const leagueId = 'l1';
|
||||
const league = League.create({ id: leagueId, name: 'League 1', description: 'Test League', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with valid command
|
||||
// Then: The team should be created in the repository
|
||||
// And: The team should have the correct name, description, and settings
|
||||
// And: The team should be associated with the correct driver as captain
|
||||
// And: The team should be associated with the correct league
|
||||
// And: EventPublisher should emit TeamCreatedEvent
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The team should be created successfully
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
|
||||
// And: The team should have the correct properties
|
||||
expect(team.name.toString()).toBe('Test Team');
|
||||
expect(team.tag.toString()).toBe('TT');
|
||||
expect(team.description.toString()).toBe('A test team');
|
||||
expect(team.ownerId.toString()).toBe(driverId);
|
||||
expect(team.leagues.map(l => l.toString())).toContain(leagueId);
|
||||
|
||||
// And: The team should be in the repository
|
||||
const savedTeam = await teamRepository.findById(team.id.toString());
|
||||
expect(savedTeam).toBeDefined();
|
||||
expect(savedTeam?.name.toString()).toBe('Test Team');
|
||||
|
||||
// And: The driver should have an owner membership
|
||||
const membership = await membershipRepository.getMembership(team.id.toString(), driverId);
|
||||
expect(membership).toBeDefined();
|
||||
expect(membership?.role).toBe('owner');
|
||||
expect(membership?.status).toBe('active');
|
||||
});
|
||||
|
||||
it('should create a team with optional description', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with description
|
||||
// Given: A driver exists
|
||||
const driverId = 'd2';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '2', name: 'Jane Doe', country: 'UK' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l2';
|
||||
const league = League.create({ id: leagueId, name: 'League 2', description: 'Test League 2', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with description
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Team With Description',
|
||||
tag: 'TWD',
|
||||
description: 'This team has a detailed description',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The team should be created with the description
|
||||
// And: EventPublisher should emit TeamCreatedEvent
|
||||
});
|
||||
|
||||
it('should create a team with custom roster size', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with custom roster size
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// When: CreateTeamUseCase.execute() is called with roster size
|
||||
// Then: The team should be created with the specified roster size
|
||||
// And: EventPublisher should emit TeamCreatedEvent
|
||||
});
|
||||
|
||||
it('should create a team with logo upload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with logo
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// And: A logo file is provided
|
||||
// When: CreateTeamUseCase.execute() is called with logo
|
||||
// Then: The logo should be stored in file storage
|
||||
// And: The team should reference the logo URL
|
||||
// And: EventPublisher should emit TeamCreatedEvent
|
||||
});
|
||||
|
||||
it('should create a team with initial member invitations', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with invitations
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// And: Other drivers exist to invite
|
||||
// When: CreateTeamUseCase.execute() is called with invitations
|
||||
// Then: The team should be created
|
||||
// And: Invitation records should be created for each invited driver
|
||||
// And: EventPublisher should emit TeamCreatedEvent
|
||||
// And: EventPublisher should emit TeamInvitationCreatedEvent for each invitation
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.description.toString()).toBe('This team has a detailed description');
|
||||
});
|
||||
|
||||
it('should create a team with minimal required fields', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with minimal information
|
||||
// Given: A driver exists
|
||||
const driverId = 'd3';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '3', name: 'Bob Smith', country: 'CA' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l3';
|
||||
const league = League.create({ id: leagueId, name: 'League 3', description: 'Test League 3', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with only required fields
|
||||
// Then: The team should be created with default values for optional fields
|
||||
// And: EventPublisher should emit TeamCreatedEvent
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Minimal Team',
|
||||
tag: 'MT',
|
||||
description: '',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The team should be created with default values
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.name.toString()).toBe('Minimal Team');
|
||||
expect(team.tag.toString()).toBe('MT');
|
||||
expect(team.description.toString()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateTeamUseCase - Validation', () => {
|
||||
it('should reject team creation with empty team name', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with empty name
|
||||
// Given: A driver exists
|
||||
const driverId = 'd4';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '4', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l4';
|
||||
const league = League.create({ id: leagueId, name: 'League 4', description: 'Test League 4', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with empty team name
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: '',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
it('should reject team creation with invalid team name format', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with invalid name format
|
||||
// Given: A driver exists
|
||||
const driverId = 'd5';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '5', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l5';
|
||||
const league = League.create({ id: leagueId, name: 'League 5', description: 'Test League 5', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with invalid team name
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Invalid!@#$%',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
it('should reject team creation with team name exceeding character limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with name exceeding limit
|
||||
it('should reject team creation when driver already belongs to a team', async () => {
|
||||
// Scenario: Driver already belongs to a team
|
||||
// Given: A driver exists
|
||||
const driverId = 'd6';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '6', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
// When: CreateTeamUseCase.execute() is called with name exceeding limit
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
const leagueId = 'l6';
|
||||
const league = League.create({ id: leagueId, name: 'League 6', description: 'Test League 6', ownerId: 'owner' });
|
||||
|
||||
// And: The driver already belongs to a team
|
||||
const existingTeam = Team.create({ id: 'existing', name: 'Existing Team', tag: 'ET', description: 'Existing', ownerId: driverId, leagues: [] });
|
||||
await teamRepository.create(existingTeam);
|
||||
await membershipRepository.saveMembership({
|
||||
teamId: 'existing',
|
||||
driverId: driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'New Team',
|
||||
tag: 'NT',
|
||||
description: 'A new team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
it('should reject team creation with description exceeding character limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with description exceeding limit
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// When: CreateTeamUseCase.execute() is called with description exceeding limit
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject team creation with invalid roster size', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with invalid roster size
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// When: CreateTeamUseCase.execute() is called with invalid roster size
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject team creation with invalid logo format', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with invalid logo format
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// When: CreateTeamUseCase.execute() is called with invalid logo format
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should reject team creation with oversized logo', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team creation with oversized logo
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// When: CreateTeamUseCase.execute() is called with oversized logo
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
expect(error.details.message).toContain('already belongs to a team');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateTeamUseCase - Error Handling', () => {
|
||||
it('should throw error when driver does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent driver
|
||||
// Given: No driver exists with the given ID
|
||||
const nonExistentDriverId = 'nonexistent';
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l7';
|
||||
const league = League.create({ id: leagueId, name: 'League 7', description: 'Test League 7', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with non-existent driver ID
|
||||
// Then: Should throw DriverNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: nonExistentDriverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
it('should throw error when league does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent league
|
||||
// Given: A driver exists
|
||||
const driverId = 'd8';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '8', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: No league exists with the given ID
|
||||
const nonExistentLeagueId = 'nonexistent';
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with non-existent league ID
|
||||
// Then: Should throw LeagueNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [nonExistentLeagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('LEAGUE_NOT_FOUND');
|
||||
});
|
||||
|
||||
it('should throw error when team name already exists', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Duplicate team name
|
||||
// Given: A driver exists
|
||||
const driverId = 'd9';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '9', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l9';
|
||||
const league = League.create({ id: leagueId, name: 'League 9', description: 'Test League 9', ownerId: 'owner' });
|
||||
|
||||
// And: A team with the same name already exists
|
||||
const existingTeam = Team.create({ id: 'existing2', name: 'Duplicate Team', tag: 'DT', description: 'Existing', ownerId: 'other', leagues: [] });
|
||||
await teamRepository.create(existingTeam);
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with duplicate team name
|
||||
// Then: Should throw TeamNameAlreadyExistsError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Duplicate Team',
|
||||
tag: 'DT2',
|
||||
description: 'A new team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
it('should throw error when driver is already captain of another team', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver already captain
|
||||
// Given: A driver exists
|
||||
// And: The driver is already captain of another team
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
// Then: Should throw DriverAlreadyCaptainError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Repository throws error
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// And: TeamRepository throws an error during save
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should handle file storage errors gracefully', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: File storage throws error
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// And: FileStorage throws an error during upload
|
||||
// When: CreateTeamUseCase.execute() is called with logo
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
expect(error.details.message).toContain('already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateTeamUseCase - Business Logic', () => {
|
||||
it('should set the creating driver as team captain', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Driver becomes captain
|
||||
// Given: A driver exists
|
||||
const driverId = 'd10';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '10', name: 'Captain Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l10';
|
||||
const league = League.create({ id: leagueId, name: 'League 10', description: 'Test League 10', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Captain Team',
|
||||
tag: 'CT',
|
||||
description: 'A team with captain',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The creating driver should be set as team captain
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
|
||||
// And: The captain role should be recorded in the team roster
|
||||
});
|
||||
|
||||
it('should validate roster size against league limits', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Roster size validation
|
||||
// Given: A driver exists
|
||||
// And: A league exists with max roster size of 10
|
||||
// When: CreateTeamUseCase.execute() is called with roster size 15
|
||||
// Then: Should throw ValidationError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
|
||||
it('should assign default tier if not specified', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Default tier assignment
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// When: CreateTeamUseCase.execute() is called without tier
|
||||
// Then: The team should be assigned a default tier
|
||||
// And: EventPublisher should emit TeamCreatedEvent
|
||||
const membership = await membershipRepository.getMembership(team.id.toString(), driverId);
|
||||
expect(membership).toBeDefined();
|
||||
expect(membership?.role).toBe('owner');
|
||||
});
|
||||
|
||||
it('should generate unique team ID', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Unique team ID generation
|
||||
// Given: A driver exists
|
||||
const driverId = 'd11';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '11', name: 'Unique Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l11';
|
||||
const league = League.create({ id: leagueId, name: 'League 11', description: 'Test League 11', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Unique Team',
|
||||
tag: 'UT',
|
||||
description: 'A unique team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The team should have a unique ID
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.id.toString()).toBeDefined();
|
||||
expect(team.id.toString().length).toBeGreaterThan(0);
|
||||
|
||||
// And: The ID should not conflict with existing teams
|
||||
const existingTeam = await teamRepository.findById(team.id.toString());
|
||||
expect(existingTeam).toBeDefined();
|
||||
expect(existingTeam?.id.toString()).toBe(team.id.toString());
|
||||
});
|
||||
|
||||
it('should set creation timestamp', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Creation timestamp
|
||||
// Given: A driver exists
|
||||
const driverId = 'd12';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '12', name: 'Timestamp Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l12';
|
||||
const league = League.create({ id: leagueId, name: 'League 12', description: 'Test League 12', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
const beforeCreate = new Date();
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Timestamp Team',
|
||||
tag: 'TT',
|
||||
description: 'A team with timestamp',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
const afterCreate = new Date();
|
||||
|
||||
// Then: The team should have a creation timestamp
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.createdAt).toBeDefined();
|
||||
|
||||
// And: The timestamp should be current or recent
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateTeamUseCase - Event Orchestration', () => {
|
||||
it('should emit TeamCreatedEvent with correct payload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event emission
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
// Then: EventPublisher should emit TeamCreatedEvent
|
||||
// And: The event should contain team ID, name, captain ID, and league ID
|
||||
});
|
||||
|
||||
it('should emit TeamInvitationCreatedEvent for each invitation', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invitation events
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// And: Other drivers exist to invite
|
||||
// When: CreateTeamUseCase.execute() is called with invitations
|
||||
// Then: EventPublisher should emit TeamInvitationCreatedEvent for each invitation
|
||||
// And: Each event should contain invitation ID, team ID, and invited driver ID
|
||||
});
|
||||
|
||||
it('should not emit events on validation failure', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No events on validation failure
|
||||
// Given: A driver exists
|
||||
// And: A league exists
|
||||
// When: CreateTeamUseCase.execute() is called with invalid data
|
||||
// Then: EventPublisher should NOT emit any events
|
||||
const createdAt = team.createdAt.toDate();
|
||||
expect(createdAt.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAt.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,346 +2,130 @@
|
||||
* Integration Test: Team Detail Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of team detail-related Use Cases:
|
||||
* - GetTeamDetailUseCase: Retrieves detailed team information including roster, performance, achievements, and history
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - GetTeamDetailsUseCase: Retrieves detailed team information including roster and management permissions
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemoryTeamRepository } from '../../../adapters/teams/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetTeamDetailUseCase } from '../../../core/teams/use-cases/GetTeamDetailUseCase';
|
||||
import { GetTeamDetailQuery } from '../../../core/teams/ports/GetTeamDetailQuery';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { GetTeamDetailsUseCase } from '../../../core/racing/application/use-cases/GetTeamDetailsUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Team Detail Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getTeamDetailUseCase: GetTeamDetailUseCase;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let getTeamDetailsUseCase: GetTeamDetailsUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// teamRepository = new InMemoryTeamRepository();
|
||||
// driverRepository = new InMemoryDriverRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getTeamDetailUseCase = new GetTeamDetailUseCase({
|
||||
// teamRepository,
|
||||
// driverRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
getTeamDetailsUseCase = new GetTeamDetailsUseCase(teamRepository, membershipRepository);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// teamRepository.clear();
|
||||
// driverRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetTeamDetailUseCase - Success Path', () => {
|
||||
it('should retrieve complete team detail with all information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with complete information
|
||||
// Given: A team exists with multiple members
|
||||
// And: The team has captain, admins, and drivers
|
||||
// And: The team has performance statistics
|
||||
// And: The team has achievements
|
||||
// And: The team has race history
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should contain all team information
|
||||
// And: The result should show team name, description, and logo
|
||||
// And: The result should show team roster with roles
|
||||
// And: The result should show team performance statistics
|
||||
// And: The result should show team achievements
|
||||
// And: The result should show team race history
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
describe('GetTeamDetailsUseCase - Success Path', () => {
|
||||
it('should retrieve team detail with membership and management permissions for owner', async () => {
|
||||
// Scenario: Team owner views team details
|
||||
// Given: A team exists
|
||||
const teamId = 't1';
|
||||
const ownerId = 'd1';
|
||||
const team = Team.create({ id: teamId, name: 'Team 1', tag: 'T1', description: 'Desc', ownerId, leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is the owner
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: GetTeamDetailsUseCase.execute() is called
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId, driverId: ownerId });
|
||||
|
||||
// Then: The result should contain team information and management permissions
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.team.id.toString()).toBe(teamId);
|
||||
expect(data.membership?.role).toBe('owner');
|
||||
expect(data.canManage).toBe(true);
|
||||
});
|
||||
|
||||
it('should retrieve team detail with minimal roster', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with minimal roster
|
||||
// Given: A team exists with only the captain
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should contain team information
|
||||
// And: The roster should show only the captain
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
it('should retrieve team detail for a non-member', async () => {
|
||||
// Scenario: Non-member views team details
|
||||
// Given: A team exists
|
||||
const teamId = 't2';
|
||||
const team = Team.create({ id: teamId, name: 'Team 2', tag: 'T2', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: GetTeamDetailsUseCase.execute() is called with a driver who is not a member
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId, driverId: 'non-member' });
|
||||
|
||||
// Then: The result should contain team information but no membership and no management permissions
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.team.id.toString()).toBe(teamId);
|
||||
expect(data.membership).toBeNull();
|
||||
expect(data.canManage).toBe(false);
|
||||
});
|
||||
|
||||
it('should retrieve team detail with pending join requests', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with pending requests
|
||||
// Given: A team exists with pending join requests
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should contain pending requests
|
||||
// And: Each request should display driver name and request date
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
it('should retrieve team detail for a regular member', async () => {
|
||||
// Scenario: Regular member views team details
|
||||
// Given: A team exists
|
||||
const teamId = 't3';
|
||||
const memberId = 'd3';
|
||||
const team = Team.create({ id: teamId, name: 'Team 3', tag: 'T3', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is a regular member
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: memberId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
it('should retrieve team detail with team performance statistics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with performance statistics
|
||||
// Given: A team exists with performance data
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should show win rate
|
||||
// And: The result should show podium finishes
|
||||
// And: The result should show total races
|
||||
// And: The result should show championship points
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
// When: GetTeamDetailsUseCase.execute() is called
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId, driverId: memberId });
|
||||
|
||||
it('should retrieve team detail with team achievements', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with achievements
|
||||
// Given: A team exists with achievements
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should show achievement badges
|
||||
// And: The result should show achievement names
|
||||
// And: The result should show achievement dates
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team detail with team race history', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with race history
|
||||
// Given: A team exists with race history
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should show past races
|
||||
// And: The result should show race results
|
||||
// And: The result should show race dates
|
||||
// And: The result should show race tracks
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team detail with league information', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with league information
|
||||
// Given: A team exists in a league
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should show league name
|
||||
// And: The result should show league tier
|
||||
// And: The result should show league season
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team detail with social links', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with social links
|
||||
// Given: A team exists with social links
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should show social media links
|
||||
// And: The result should show website link
|
||||
// And: The result should show Discord link
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team detail with roster size limit', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with roster size limit
|
||||
// Given: A team exists with roster size limit
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should show current roster size
|
||||
// And: The result should show maximum roster size
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team detail with team full indicator', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team is full
|
||||
// Given: A team exists and is full
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should show team is full
|
||||
// And: The result should not show join request option
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
// Then: The result should contain team information and membership but no management permissions
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.team.id.toString()).toBe(teamId);
|
||||
expect(data.membership?.role).toBe('driver');
|
||||
expect(data.canManage).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamDetailUseCase - Edge Cases', () => {
|
||||
it('should handle team with no career history', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with no career history
|
||||
// Given: A team exists
|
||||
// And: The team has no career history
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should contain team detail
|
||||
// And: Career history section should be empty
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle team with no recent race results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with no recent race results
|
||||
// Given: A team exists
|
||||
// And: The team has no recent race results
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should contain team detail
|
||||
// And: Recent race results section should be empty
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle team with no championship standings', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with no championship standings
|
||||
// Given: A team exists
|
||||
// And: The team has no championship standings
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should contain team detail
|
||||
// And: Championship standings section should be empty
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle team with no data at all', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team with absolutely no data
|
||||
// Given: A team exists
|
||||
// And: The team has no statistics
|
||||
// And: The team has no career history
|
||||
// And: The team has no recent race results
|
||||
// And: The team has no championship standings
|
||||
// And: The team has no social links
|
||||
// When: GetTeamDetailUseCase.execute() is called with team ID
|
||||
// Then: The result should contain basic team info
|
||||
// And: All sections should be empty or show default values
|
||||
// And: EventPublisher should emit TeamDetailAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamDetailUseCase - Error Handling', () => {
|
||||
describe('GetTeamDetailsUseCase - Error Handling', () => {
|
||||
it('should throw error when team does not exist', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Non-existent team
|
||||
// Given: No team exists with the given ID
|
||||
// When: GetTeamDetailUseCase.execute() is called with non-existent team ID
|
||||
// Then: Should throw TeamNotFoundError
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
// When: GetTeamDetailsUseCase.execute() is called with non-existent team ID
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId: 'nonexistent', driverId: 'any' });
|
||||
|
||||
it('should throw error when team ID is invalid', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Invalid team ID
|
||||
// Given: An invalid team ID (e.g., empty string, null, undefined)
|
||||
// When: GetTeamDetailUseCase.execute() is called with invalid team 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 team exists
|
||||
// And: TeamRepository throws an error during query
|
||||
// When: GetTeamDetailUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Team Detail Data Orchestration', () => {
|
||||
it('should correctly calculate team statistics from race results', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team statistics calculation
|
||||
// Given: A team exists
|
||||
// And: The team has 10 completed races
|
||||
// And: The team has 3 wins
|
||||
// And: The team has 5 podiums
|
||||
// When: GetTeamDetailUseCase.execute() is called
|
||||
// Then: Team 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 team exists
|
||||
// And: The team has participated in 2 leagues
|
||||
// And: The team has been on 3 teams across seasons
|
||||
// When: GetTeamDetailUseCase.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 team exists
|
||||
// And: The team has 5 recent race results
|
||||
// When: GetTeamDetailUseCase.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 team exists
|
||||
// And: The team 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: GetTeamDetailUseCase.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 team exists
|
||||
// And: The team has social links (Discord, Twitter, iRacing)
|
||||
// When: GetTeamDetailUseCase.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 roster with roles', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team roster formatting
|
||||
// Given: A team exists
|
||||
// And: The team has captain, admins, and drivers
|
||||
// When: GetTeamDetailUseCase.execute() is called
|
||||
// Then: Team roster should show:
|
||||
// - Captain: Highlighted with badge
|
||||
// - Admins: Listed with admin role
|
||||
// - Drivers: Listed with driver role
|
||||
// - Each member should show name, avatar, and join date
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamDetailUseCase - Event Orchestration', () => {
|
||||
it('should emit TeamDetailAccessedEvent with correct payload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event emission
|
||||
// Given: A team exists
|
||||
// When: GetTeamDetailUseCase.execute() is called
|
||||
// Then: EventPublisher should emit TeamDetailAccessedEvent
|
||||
// And: The event should contain team ID and requesting driver ID
|
||||
});
|
||||
|
||||
it('should not emit events on validation failure', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No events on validation failure
|
||||
// Given: No team exists
|
||||
// When: GetTeamDetailUseCase.execute() is called with invalid data
|
||||
// Then: EventPublisher should NOT emit any events
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('TEAM_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,323 +2,97 @@
|
||||
* Integration Test: Team Leaderboard Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of team leaderboard-related Use Cases:
|
||||
* - GetTeamLeaderboardUseCase: Retrieves ranked list of teams with performance metrics
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - GetTeamsLeaderboardUseCase: Retrieves ranked list of teams with performance metrics
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemoryTeamRepository } from '../../../adapters/teams/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetTeamLeaderboardUseCase } from '../../../core/teams/use-cases/GetTeamLeaderboardUseCase';
|
||||
import { GetTeamLeaderboardQuery } from '../../../core/teams/ports/GetTeamLeaderboardQuery';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { GetTeamsLeaderboardUseCase } from '../../../core/racing/application/use-cases/GetTeamsLeaderboardUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Team Leaderboard Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getTeamLeaderboardUseCase: GetTeamLeaderboardUseCase;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let getTeamsLeaderboardUseCase: GetTeamsLeaderboardUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// teamRepository = new InMemoryTeamRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getTeamLeaderboardUseCase = new GetTeamLeaderboardUseCase({
|
||||
// teamRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
|
||||
// Mock driver stats provider
|
||||
const getDriverStats = (driverId: string) => {
|
||||
const statsMap: Record<string, { rating: number, wins: number, totalRaces: number }> = {
|
||||
'd1': { rating: 2000, wins: 10, totalRaces: 50 },
|
||||
'd2': { rating: 1500, wins: 5, totalRaces: 30 },
|
||||
'd3': { rating: 1000, wins: 2, totalRaces: 20 },
|
||||
};
|
||||
return statsMap[driverId] || null;
|
||||
};
|
||||
|
||||
getTeamsLeaderboardUseCase = new GetTeamsLeaderboardUseCase(
|
||||
teamRepository,
|
||||
membershipRepository,
|
||||
getDriverStats,
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// teamRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetTeamLeaderboardUseCase - Success Path', () => {
|
||||
it('should retrieve complete team leaderboard with all teams', async () => {
|
||||
// TODO: Implement test
|
||||
describe('GetTeamsLeaderboardUseCase - Success Path', () => {
|
||||
it('should retrieve ranked team leaderboard with performance metrics', async () => {
|
||||
// Scenario: Leaderboard with multiple teams
|
||||
// Given: Multiple teams exist with different performance metrics
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: The result should contain all teams
|
||||
// And: Teams should be ranked by points
|
||||
// And: Each team should show position, name, and points
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
// Given: Multiple teams exist
|
||||
const team1 = Team.create({ id: 't1', name: 'Pro Team', tag: 'PRO', description: 'Desc', ownerId: 'o1', leagues: [] });
|
||||
const team2 = Team.create({ id: 't2', name: 'Am Team', tag: 'AM', description: 'Desc', ownerId: 'o2', leagues: [] });
|
||||
await teamRepository.create(team1);
|
||||
await teamRepository.create(team2);
|
||||
|
||||
// And: Teams have members with different stats
|
||||
await membershipRepository.saveMembership({ teamId: 't1', driverId: 'd1', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
await membershipRepository.saveMembership({ teamId: 't2', driverId: 'd3', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
|
||||
// When: GetTeamsLeaderboardUseCase.execute() is called
|
||||
const result = await getTeamsLeaderboardUseCase.execute({ leagueId: 'any' });
|
||||
|
||||
// Then: The result should contain ranked teams
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { items, topItems } = result.unwrap();
|
||||
expect(items).toHaveLength(2);
|
||||
|
||||
// And: Teams should be ranked by rating (Pro Team has d1 with 2000, Am Team has d3 with 1000)
|
||||
expect(topItems[0]?.team.id.toString()).toBe('t1');
|
||||
expect(topItems[0]?.rating).toBe(2000);
|
||||
expect(topItems[1]?.team.id.toString()).toBe('t2');
|
||||
expect(topItems[1]?.rating).toBe(1000);
|
||||
});
|
||||
|
||||
it('should retrieve team leaderboard with performance metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Leaderboard with performance metrics
|
||||
// Given: Teams exist with performance data
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: Each team should show total points
|
||||
// And: Each team should show win count
|
||||
// And: Each team should show podium count
|
||||
// And: Each team should show race count
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team leaderboard filtered by league', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Leaderboard filtered by league
|
||||
// Given: Teams exist in multiple leagues
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with league filter
|
||||
// Then: The result should contain only teams from that league
|
||||
// And: Teams should be ranked by points within the league
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team leaderboard filtered by season', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Leaderboard filtered by season
|
||||
// Given: Teams exist with data from multiple seasons
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with season filter
|
||||
// Then: The result should contain only teams from that season
|
||||
// And: Teams should be ranked by points within the season
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team leaderboard filtered by tier', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Leaderboard filtered by tier
|
||||
// Given: Teams exist in different tiers
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with tier filter
|
||||
// Then: The result should contain only teams from that tier
|
||||
// And: Teams should be ranked by points within the tier
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team leaderboard sorted by different criteria', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Leaderboard sorted by different criteria
|
||||
// Given: Teams exist with various metrics
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with sort criteria
|
||||
// Then: Teams should be sorted by the specified criteria
|
||||
// And: The sort order should be correct
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team leaderboard with pagination', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Leaderboard with pagination
|
||||
// Given: Many teams exist
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with pagination
|
||||
// Then: The result should contain only the specified page
|
||||
// And: The result should show total count
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team leaderboard with top teams highlighted', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Top teams highlighted
|
||||
// Given: Teams exist with rankings
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: Top 3 teams should be highlighted
|
||||
// And: Top teams should have gold, silver, bronze badges
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team leaderboard with own team highlighted', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Own team highlighted
|
||||
// Given: Teams exist and driver is member of a team
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with driver ID
|
||||
// Then: The driver's team should be highlighted
|
||||
// And: The team should have a "Your Team" indicator
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve team leaderboard with filters applied', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Multiple filters applied
|
||||
// Given: Teams exist in multiple leagues and seasons
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with multiple filters
|
||||
// Then: The result should show active filters
|
||||
// And: The result should contain only matching teams
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamLeaderboardUseCase - Edge Cases', () => {
|
||||
it('should handle empty leaderboard', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No teams exist
|
||||
// Given: No teams exist
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// When: GetTeamsLeaderboardUseCase.execute() is called
|
||||
const result = await getTeamsLeaderboardUseCase.execute({ leagueId: 'any' });
|
||||
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle empty leaderboard after filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No teams match filters
|
||||
// Given: Teams exist but none match the filters
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with filters
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle leaderboard with single team', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Only one team exists
|
||||
// Given: Only one team exists
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: The result should contain only that team
|
||||
// And: The team should be ranked 1st
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle leaderboard with teams having equal points', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams with equal points
|
||||
// Given: Multiple teams have the same points
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: Teams should be ranked by tie-breaker criteria
|
||||
// And: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamLeaderboardUseCase - 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: GetTeamLeaderboardUseCase.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: GetTeamLeaderboardUseCase.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: Teams exist
|
||||
// And: TeamRepository throws an error during query
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Team Leaderboard Data Orchestration', () => {
|
||||
it('should correctly calculate team rankings from performance metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team ranking calculation
|
||||
// Given: Teams exist with different performance metrics
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: Teams should be ranked by points
|
||||
// And: Teams with more wins should rank higher when points are equal
|
||||
// And: Teams with more podiums should rank higher when wins are equal
|
||||
});
|
||||
|
||||
it('should correctly format team performance metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Performance metrics formatting
|
||||
// Given: Teams exist with performance data
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: Each team should show:
|
||||
// - Total points (formatted as number)
|
||||
// - Win count (formatted as number)
|
||||
// - Podium count (formatted as number)
|
||||
// - Race count (formatted as number)
|
||||
// - Win rate (formatted as percentage)
|
||||
});
|
||||
|
||||
it('should correctly filter teams by league', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League filtering
|
||||
// Given: Teams exist in multiple leagues
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with league filter
|
||||
// Then: Only teams from the specified league should be included
|
||||
// And: Teams should be ranked by points within the league
|
||||
});
|
||||
|
||||
it('should correctly filter teams by season', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Season filtering
|
||||
// Given: Teams exist with data from multiple seasons
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with season filter
|
||||
// Then: Only teams from the specified season should be included
|
||||
// And: Teams should be ranked by points within the season
|
||||
});
|
||||
|
||||
it('should correctly filter teams by tier', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Tier filtering
|
||||
// Given: Teams exist in different tiers
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with tier filter
|
||||
// Then: Only teams from the specified tier should be included
|
||||
// And: Teams should be ranked by points within the tier
|
||||
});
|
||||
|
||||
it('should correctly sort teams by different criteria', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sorting by different criteria
|
||||
// Given: Teams exist with various metrics
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with sort criteria
|
||||
// Then: Teams should be sorted by the specified criteria
|
||||
// And: The sort order should be correct
|
||||
});
|
||||
|
||||
it('should correctly paginate team leaderboard', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Pagination
|
||||
// Given: Many teams exist
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with pagination
|
||||
// Then: Only the specified page should be returned
|
||||
// And: Total count should be accurate
|
||||
});
|
||||
|
||||
it('should correctly highlight top teams', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Top team highlighting
|
||||
// Given: Teams exist with rankings
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: Top 3 teams should be marked as top teams
|
||||
// And: Top teams should have appropriate badges
|
||||
});
|
||||
|
||||
it('should correctly highlight own team', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Own team highlighting
|
||||
// Given: Teams exist and driver is member of a team
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with driver ID
|
||||
// Then: The driver's team should be marked as own team
|
||||
// And: The team should have a "Your Team" indicator
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamLeaderboardUseCase - Event Orchestration', () => {
|
||||
it('should emit TeamLeaderboardAccessedEvent with correct payload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event emission
|
||||
// Given: Teams exist
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called
|
||||
// Then: EventPublisher should emit TeamLeaderboardAccessedEvent
|
||||
// And: The event should contain filter and sort parameters
|
||||
});
|
||||
|
||||
it('should not emit events on validation failure', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No events on validation failure
|
||||
// Given: Invalid parameters
|
||||
// When: GetTeamLeaderboardUseCase.execute() is called with invalid data
|
||||
// Then: EventPublisher should NOT emit any events
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { items } = result.unwrap();
|
||||
expect(items).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,328 +2,104 @@
|
||||
* Integration Test: Teams List Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of teams list-related Use Cases:
|
||||
* - GetTeamsListUseCase: Retrieves list of teams with filtering, sorting, and search capabilities
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
|
||||
* - GetAllTeamsUseCase: Retrieves list of teams with enrichment (member count, stats)
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - 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 { InMemoryTeamRepository } from '../../../adapters/teams/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
|
||||
import { GetTeamsListUseCase } from '../../../core/teams/use-cases/GetTeamsListUseCase';
|
||||
import { GetTeamsListQuery } from '../../../core/teams/ports/GetTeamsListQuery';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { InMemoryTeamStatsRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamStatsRepository';
|
||||
import { GetAllTeamsUseCase } from '../../../core/racing/application/use-cases/GetAllTeamsUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Teams List Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let leagueRepository: InMemoryLeagueRepository;
|
||||
let eventPublisher: InMemoryEventPublisher;
|
||||
let getTeamsListUseCase: GetTeamsListUseCase;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let statsRepository: InMemoryTeamStatsRepository;
|
||||
let getAllTeamsUseCase: GetAllTeamsUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Initialize In-Memory repositories and event publisher
|
||||
// teamRepository = new InMemoryTeamRepository();
|
||||
// leagueRepository = new InMemoryLeagueRepository();
|
||||
// eventPublisher = new InMemoryEventPublisher();
|
||||
// getTeamsListUseCase = new GetTeamsListUseCase({
|
||||
// teamRepository,
|
||||
// leagueRepository,
|
||||
// eventPublisher,
|
||||
// });
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
statsRepository = new InMemoryTeamStatsRepository();
|
||||
getAllTeamsUseCase = new GetAllTeamsUseCase(teamRepository, membershipRepository, statsRepository, mockLogger);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Clear all In-Memory repositories before each test
|
||||
// teamRepository.clear();
|
||||
// leagueRepository.clear();
|
||||
// eventPublisher.clear();
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
statsRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetTeamsListUseCase - Success Path', () => {
|
||||
it('should retrieve complete teams list with all teams', async () => {
|
||||
// TODO: Implement test
|
||||
describe('GetAllTeamsUseCase - Success Path', () => {
|
||||
it('should retrieve complete teams list with all teams and enrichment', async () => {
|
||||
// Scenario: Teams list with multiple teams
|
||||
// Given: Multiple teams exist
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: The result should contain all teams
|
||||
// And: Each team should show name, logo, and member count
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
const team1 = Team.create({ id: 't1', name: 'Team 1', tag: 'T1', description: 'Desc 1', ownerId: 'o1', leagues: [] });
|
||||
const team2 = Team.create({ id: 't2', name: 'Team 2', tag: 'T2', description: 'Desc 2', ownerId: 'o2', leagues: [] });
|
||||
await teamRepository.create(team1);
|
||||
await teamRepository.create(team2);
|
||||
|
||||
// And: Teams have members
|
||||
await membershipRepository.saveMembership({ teamId: 't1', driverId: 'd1', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
await membershipRepository.saveMembership({ teamId: 't1', driverId: 'd2', role: 'driver', status: 'active', joinedAt: new Date() });
|
||||
await membershipRepository.saveMembership({ teamId: 't2', driverId: 'd3', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
|
||||
// And: Teams have stats
|
||||
await statsRepository.saveTeamStats('t1', {
|
||||
totalWins: 5,
|
||||
totalRaces: 20,
|
||||
rating: 1500,
|
||||
performanceLevel: 'intermediate',
|
||||
specialization: 'sprint',
|
||||
region: 'EU',
|
||||
languages: ['en'],
|
||||
isRecruiting: true
|
||||
});
|
||||
|
||||
// When: GetAllTeamsUseCase.execute() is called
|
||||
const result = await getAllTeamsUseCase.execute({});
|
||||
|
||||
// Then: The result should contain all teams with enrichment
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { teams, totalCount } = result.unwrap();
|
||||
expect(totalCount).toBe(2);
|
||||
|
||||
const enriched1 = teams.find(t => t.team.id.toString() === 't1');
|
||||
expect(enriched1).toBeDefined();
|
||||
expect(enriched1?.memberCount).toBe(2);
|
||||
expect(enriched1?.totalWins).toBe(5);
|
||||
expect(enriched1?.rating).toBe(1500);
|
||||
|
||||
const enriched2 = teams.find(t => t.team.id.toString() === 't2');
|
||||
expect(enriched2).toBeDefined();
|
||||
expect(enriched2?.memberCount).toBe(1);
|
||||
expect(enriched2?.totalWins).toBe(0); // Default value
|
||||
});
|
||||
|
||||
it('should retrieve teams list with team details', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams list with detailed information
|
||||
// Given: Teams exist with various details
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: Each team should show team name
|
||||
// And: Each team should show team logo
|
||||
// And: Each team should show number of members
|
||||
// And: Each team should show performance stats
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve teams list with search filter', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams list with search
|
||||
// Given: Teams exist with various names
|
||||
// When: GetTeamsListUseCase.execute() is called with search term
|
||||
// Then: The result should contain only matching teams
|
||||
// And: The result should show search results count
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve teams list filtered by league', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams list filtered by league
|
||||
// Given: Teams exist in multiple leagues
|
||||
// When: GetTeamsListUseCase.execute() is called with league filter
|
||||
// Then: The result should contain only teams from that league
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve teams list filtered by performance tier', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams list filtered by tier
|
||||
// Given: Teams exist in different tiers
|
||||
// When: GetTeamsListUseCase.execute() is called with tier filter
|
||||
// Then: The result should contain only teams from that tier
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve teams list sorted by different criteria', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams list sorted by different criteria
|
||||
// Given: Teams exist with various metrics
|
||||
// When: GetTeamsListUseCase.execute() is called with sort criteria
|
||||
// Then: Teams should be sorted by the specified criteria
|
||||
// And: The sort order should be correct
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve teams list with pagination', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams list with pagination
|
||||
// Given: Many teams exist
|
||||
// When: GetTeamsListUseCase.execute() is called with pagination
|
||||
// Then: The result should contain only the specified page
|
||||
// And: The result should show total count
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve teams list with team achievements', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams list with achievements
|
||||
// Given: Teams exist with achievements
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: Each team should show achievement badges
|
||||
// And: Each team should show number of achievements
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve teams list with team performance metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams list with performance metrics
|
||||
// Given: Teams exist with performance data
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: Each team should show win rate
|
||||
// And: Each team should show podium finishes
|
||||
// And: Each team should show recent race results
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve teams list with team roster preview', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams list with roster preview
|
||||
// Given: Teams exist with members
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: Each team should show preview of team members
|
||||
// And: Each team should show the team captain
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should retrieve teams list with filters applied', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Multiple filters applied
|
||||
// Given: Teams exist in multiple leagues and tiers
|
||||
// When: GetTeamsListUseCase.execute() is called with multiple filters
|
||||
// Then: The result should show active filters
|
||||
// And: The result should contain only matching teams
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamsListUseCase - Edge Cases', () => {
|
||||
it('should handle empty teams list', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No teams exist
|
||||
// Given: No teams exist
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// When: GetAllTeamsUseCase.execute() is called
|
||||
const result = await getAllTeamsUseCase.execute({});
|
||||
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle empty teams list after filtering', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No teams match filters
|
||||
// Given: Teams exist but none match the filters
|
||||
// When: GetTeamsListUseCase.execute() is called with filters
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle empty teams list after search', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No teams match search
|
||||
// Given: Teams exist but none match the search term
|
||||
// When: GetTeamsListUseCase.execute() is called with search term
|
||||
// Then: The result should be empty
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle teams list with single team', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Only one team exists
|
||||
// Given: Only one team exists
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: The result should contain only that team
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
|
||||
it('should handle teams list with teams having equal metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Teams with equal metrics
|
||||
// Given: Multiple teams have the same metrics
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: Teams should be sorted by tie-breaker criteria
|
||||
// And: EventPublisher should emit TeamsListAccessedEvent
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamsListUseCase - 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: GetTeamsListUseCase.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: GetTeamsListUseCase.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: Teams exist
|
||||
// And: TeamRepository throws an error during query
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: Should propagate the error appropriately
|
||||
// And: EventPublisher should NOT emit any events
|
||||
});
|
||||
});
|
||||
|
||||
describe('Teams List Data Orchestration', () => {
|
||||
it('should correctly filter teams by league', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: League filtering
|
||||
// Given: Teams exist in multiple leagues
|
||||
// When: GetTeamsListUseCase.execute() is called with league filter
|
||||
// Then: Only teams from the specified league should be included
|
||||
// And: Teams should be sorted by the specified criteria
|
||||
});
|
||||
|
||||
it('should correctly filter teams by tier', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Tier filtering
|
||||
// Given: Teams exist in different tiers
|
||||
// When: GetTeamsListUseCase.execute() is called with tier filter
|
||||
// Then: Only teams from the specified tier should be included
|
||||
// And: Teams should be sorted by the specified criteria
|
||||
});
|
||||
|
||||
it('should correctly search teams by name', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Team name search
|
||||
// Given: Teams exist with various names
|
||||
// When: GetTeamsListUseCase.execute() is called with search term
|
||||
// Then: Only teams matching the search term should be included
|
||||
// And: Search should be case-insensitive
|
||||
});
|
||||
|
||||
it('should correctly sort teams by different criteria', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Sorting by different criteria
|
||||
// Given: Teams exist with various metrics
|
||||
// When: GetTeamsListUseCase.execute() is called with sort criteria
|
||||
// Then: Teams should be sorted by the specified criteria
|
||||
// And: The sort order should be correct
|
||||
});
|
||||
|
||||
it('should correctly paginate teams list', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Pagination
|
||||
// Given: Many teams exist
|
||||
// When: GetTeamsListUseCase.execute() is called with pagination
|
||||
// Then: Only the specified page should be returned
|
||||
// And: Total count should be accurate
|
||||
});
|
||||
|
||||
it('should correctly format team achievements', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Achievement formatting
|
||||
// Given: Teams exist with achievements
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: Each team should show achievement badges
|
||||
// And: Each team should show number of achievements
|
||||
});
|
||||
|
||||
it('should correctly format team performance metrics', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Performance metrics formatting
|
||||
// Given: Teams exist with performance data
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: Each team should show:
|
||||
// - Win rate (formatted as percentage)
|
||||
// - Podium finishes (formatted as number)
|
||||
// - Recent race results (formatted with position and points)
|
||||
});
|
||||
|
||||
it('should correctly format team roster preview', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Roster preview formatting
|
||||
// Given: Teams exist with members
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: Each team should show preview of team members
|
||||
// And: Each team should show the team captain
|
||||
// And: Preview should be limited to a few members
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamsListUseCase - Event Orchestration', () => {
|
||||
it('should emit TeamsListAccessedEvent with correct payload', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: Event emission
|
||||
// Given: Teams exist
|
||||
// When: GetTeamsListUseCase.execute() is called
|
||||
// Then: EventPublisher should emit TeamsListAccessedEvent
|
||||
// And: The event should contain filter, sort, and search parameters
|
||||
});
|
||||
|
||||
it('should not emit events on validation failure', async () => {
|
||||
// TODO: Implement test
|
||||
// Scenario: No events on validation failure
|
||||
// Given: Invalid parameters
|
||||
// When: GetTeamsListUseCase.execute() is called with invalid data
|
||||
// Then: EventPublisher should NOT emit any events
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { teams, totalCount } = result.unwrap();
|
||||
expect(totalCount).toBe(0);
|
||||
expect(teams).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user