integration tests
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped

This commit is contained in:
2026-01-23 11:44:59 +01:00
parent a0f41f242f
commit 6df38a462a
125 changed files with 4712 additions and 19184 deletions

View File

@@ -89,6 +89,10 @@ export class InMemoryEventPublisher implements DashboardEventPublisher, LeagueEv
return [...this.leagueRosterAccessedEvents];
}
getLeagueCreatedEvents(): LeagueCreatedEvent[] {
return [...this.leagueCreatedEvents];
}
clear(): void {
this.dashboardAccessedEvents = [];
this.dashboardErrorEvents = [];

View File

@@ -213,22 +213,43 @@ export class InMemoryLeagueRepository implements LeagueRepository {
return this.leagueStandings.get(driverId) || [];
}
addLeagueMembers(leagueId: string, members: LeagueMember[]): void {
this.leagueMembers.set(leagueId, members);
async addLeagueMembers(leagueId: string, members: LeagueMember[]): Promise<void> {
const current = this.leagueMembers.get(leagueId) || [];
this.leagueMembers.set(leagueId, [...current, ...members]);
}
async getLeagueMembers(leagueId: string): Promise<LeagueMember[]> {
return this.leagueMembers.get(leagueId) || [];
}
addPendingRequests(leagueId: string, requests: LeaguePendingRequest[]): void {
this.leaguePendingRequests.set(leagueId, requests);
async updateLeagueMember(leagueId: string, driverId: string, updates: Partial<LeagueMember>): Promise<void> {
const members = this.leagueMembers.get(leagueId) || [];
const index = members.findIndex(m => m.driverId === driverId);
if (index !== -1) {
members[index] = { ...members[index], ...updates } as LeagueMember;
this.leagueMembers.set(leagueId, [...members]);
}
}
async removeLeagueMember(leagueId: string, driverId: string): Promise<void> {
const members = this.leagueMembers.get(leagueId) || [];
this.leagueMembers.set(leagueId, members.filter(m => m.driverId !== driverId));
}
async addPendingRequests(leagueId: string, requests: LeaguePendingRequest[]): Promise<void> {
const current = this.leaguePendingRequests.get(leagueId) || [];
this.leaguePendingRequests.set(leagueId, [...current, ...requests]);
}
async getPendingRequests(leagueId: string): Promise<LeaguePendingRequest[]> {
return this.leaguePendingRequests.get(leagueId) || [];
}
async removePendingRequest(leagueId: string, requestId: string): Promise<void> {
const current = this.leaguePendingRequests.get(leagueId) || [];
this.leaguePendingRequests.set(leagueId, current.filter(r => r.id !== requestId));
}
private createDefaultStats(leagueId: string): LeagueStats {
return {
leagueId,

View File

@@ -18,6 +18,12 @@ export class InMemoryAvatarGenerationRepository implements AvatarGenerationRepos
}
}
clear(): void {
this.requests.clear();
this.userRequests.clear();
this.logger.info('InMemoryAvatarGenerationRepository cleared.');
}
async save(request: AvatarGenerationRequest): Promise<void> {
this.logger.debug(`[InMemoryAvatarGenerationRepository] Saving avatar generation request: ${request.id} for user ${request.userId}.`);
this.requests.set(request.id, request);

View File

@@ -0,0 +1,22 @@
import type { AvatarGenerationPort, AvatarGenerationOptions, AvatarGenerationResult } from '@core/media/application/ports/AvatarGenerationPort';
import type { Logger } from '@core/shared/domain/Logger';
export class InMemoryAvatarGenerationAdapter implements AvatarGenerationPort {
constructor(private readonly logger: Logger) {
this.logger.info('InMemoryAvatarGenerationAdapter initialized.');
}
async generateAvatars(options: AvatarGenerationOptions): Promise<AvatarGenerationResult> {
this.logger.debug('[InMemoryAvatarGenerationAdapter] Generating avatars (mock).', { options });
const avatars = Array.from({ length: options.count }, (_, i) => ({
url: `https://example.com/generated-avatar-${i + 1}.png`,
thumbnailUrl: `https://example.com/generated-avatar-${i + 1}-thumb.png`,
}));
return Promise.resolve({
success: true,
avatars,
});
}
}

View File

@@ -29,7 +29,7 @@ export class InMemoryMediaStorageAdapter implements MediaStoragePort {
}
// Generate storage key
const storageKey = `uploaded/${Date.now()}-${options.filename.replace(/[^a-zA-Z0-9.-]/g, '_')}`;
const storageKey = `/media/uploaded/${Date.now()}-${options.filename.replace(/[^a-zA-Z0-9.-]/g, '_')}`;
// Store buffer and metadata
this.storage.set(storageKey, buffer);

View File

@@ -6,34 +6,33 @@ import type { Payment, PaymentType } from '@core/payments/domain/entities/Paymen
import type { PaymentRepository } from '@core/payments/domain/repositories/PaymentRepository';
import type { Logger } from '@core/shared/domain/Logger';
const payments: Map<string, Payment> = new Map();
export class InMemoryPaymentRepository implements PaymentRepository {
private payments: Map<string, Payment> = new Map();
constructor(private readonly logger: Logger) {}
async findById(id: string): Promise<Payment | null> {
this.logger.debug('[InMemoryPaymentRepository] findById', { id });
return payments.get(id) || null;
return this.payments.get(id) || null;
}
async findByLeagueId(leagueId: string): Promise<Payment[]> {
this.logger.debug('[InMemoryPaymentRepository] findByLeagueId', { leagueId });
return Array.from(payments.values()).filter(p => p.leagueId === leagueId);
return Array.from(this.payments.values()).filter(p => p.leagueId === leagueId);
}
async findByPayerId(payerId: string): Promise<Payment[]> {
this.logger.debug('[InMemoryPaymentRepository] findByPayerId', { payerId });
return Array.from(payments.values()).filter(p => p.payerId === payerId);
return Array.from(this.payments.values()).filter(p => p.payerId === payerId);
}
async findByType(type: PaymentType): Promise<Payment[]> {
this.logger.debug('[InMemoryPaymentRepository] findByType', { type });
return Array.from(payments.values()).filter(p => p.type === type);
return Array.from(this.payments.values()).filter(p => p.type === type);
}
async findByFilters(filters: { leagueId?: string; payerId?: string; type?: PaymentType }): Promise<Payment[]> {
this.logger.debug('[InMemoryPaymentRepository] findByFilters', { filters });
let results = Array.from(payments.values());
let results = Array.from(this.payments.values());
if (filters.leagueId) {
results = results.filter(p => p.leagueId === filters.leagueId);
@@ -50,13 +49,17 @@ export class InMemoryPaymentRepository implements PaymentRepository {
async create(payment: Payment): Promise<Payment> {
this.logger.debug('[InMemoryPaymentRepository] create', { payment });
payments.set(payment.id, payment);
this.payments.set(payment.id, payment);
return payment;
}
async update(payment: Payment): Promise<Payment> {
this.logger.debug('[InMemoryPaymentRepository] update', { payment });
payments.set(payment.id, payment);
this.payments.set(payment.id, payment);
return payment;
}
}
clear(): void {
this.payments.clear();
}
}

View File

@@ -218,10 +218,15 @@ export class InMemoryResultRepository implements ResultRepository {
}
}
async clear(): Promise<void> {
this.logger.debug('[InMemoryResultRepository] Clearing all results.');
this.results.clear();
}
/**
* Utility method to generate a new UUID
*/
static generateId(): string {
return uuidv4();
}
}
}

View File

@@ -99,4 +99,12 @@ export class InMemorySponsorshipPricingRepository implements SponsorshipPricingR
throw error;
}
}
async create(pricing: any): Promise<void> {
await this.save(pricing.entityType, pricing.entityId, pricing);
}
clear(): void {
this.pricings.clear();
}
}

View File

@@ -166,6 +166,11 @@ export class InMemoryStandingRepository implements StandingRepository {
}
}
async clear(): Promise<void> {
this.logger.debug('Clearing all standings.');
this.standings.clear();
}
async recalculate(leagueId: string): Promise<Standing[]> {
this.logger.debug(`Recalculating standings for league id: ${leagueId}`);
try {
@@ -268,4 +273,4 @@ export class InMemoryStandingRepository implements StandingRepository {
throw error;
}
}
}
}

View File

@@ -6,7 +6,7 @@ import { Provider } from '@nestjs/common';
import {
ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN,
ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN,
} from '../../../../persistence/analytics/AnalyticsPersistenceTokens';
} from '../../persistence/analytics/AnalyticsPersistenceTokens';
const LOGGER_TOKEN = 'Logger';

View File

@@ -140,10 +140,9 @@ export const SponsorProviders: Provider[] = [
useFactory: (
paymentRepo: PaymentRepository,
seasonSponsorshipRepo: SeasonSponsorshipRepository,
) => {
return new GetSponsorBillingUseCase(paymentRepo, seasonSponsorshipRepo);
},
inject: [PAYMENT_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN],
sponsorRepo: SponsorRepository,
) => new GetSponsorBillingUseCase(paymentRepo, seasonSponsorshipRepo, sponsorRepo),
inject: [PAYMENT_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN],
},
{
provide: GET_ENTITY_SPONSORSHIP_PRICING_USE_CASE_TOKEN,

View File

@@ -9,6 +9,9 @@ import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
*/
export class LeaguesViewDataBuilder {
static build(apiDto: AllLeaguesWithCapacityAndScoringDTO): LeaguesViewData {
if (!apiDto || !Array.isArray(apiDto.leagues)) {
return { leagues: [] };
}
return {
leagues: apiDto.leagues.map((league) => ({
id: league.id,

View File

@@ -2,14 +2,16 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeagueService, type LeagueDetailData } from '@/lib/services/leagues/LeagueService';
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { LeagueDetailViewDataBuilder } from '@/lib/builders/view-data/LeagueDetailViewDataBuilder';
import { LeagueDetailViewData } from '@/lib/view-data/LeagueDetailViewData';
/**
* LeagueDetail page query
* Returns the raw API DTO for the league detail page
* No DI container usage - constructs dependencies explicitly
*/
export class LeagueDetailPageQuery implements PageQuery<LeagueDetailData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<LeagueDetailData, PresentationError>> {
export class LeagueDetailPageQuery implements PageQuery<LeagueDetailViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<LeagueDetailViewData, PresentationError>> {
const service = new LeagueService();
const result = await service.getLeagueDetailData(leagueId);
@@ -17,11 +19,12 @@ export class LeagueDetailPageQuery implements PageQuery<LeagueDetailData, string
return Result.err(mapToPresentationError(result.getError()));
}
return Result.ok(result.unwrap());
const viewData = LeagueDetailViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
// Static method to avoid object construction in server code
static async execute(leagueId: string): Promise<Result<LeagueDetailData, PresentationError>> {
static async execute(leagueId: string): Promise<Result<LeagueDetailViewData, PresentationError>> {
const query = new LeagueDetailPageQuery();
return query.execute(leagueId);
}

View File

@@ -33,7 +33,11 @@ export class LeaguesPageQuery implements PageQuery<LeaguesViewData, void> {
}
// Transform to ViewData using builder
const viewData = LeaguesViewDataBuilder.build(result.unwrap());
const apiDto = result.unwrap();
if (!apiDto || !apiDto.leagues) {
return Result.err('UNKNOWN_ERROR');
}
const viewData = LeaguesViewDataBuilder.build(apiDto);
return Result.ok(viewData);
}

View File

@@ -169,27 +169,28 @@ export class LeagueService implements Service {
this.racesApiClient.getPageData(leagueId),
]);
if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
const membershipCount = Array.isArray(memberships?.members) ? memberships.members.length : 0;
const racesCount = Array.isArray(racesPageData?.races) ? racesPageData.races.length : 0;
const race0 = racesCount > 0 ? racesPageData.races[0] : null;
console.info(
'[LeagueService.getLeagueDetailData] baseUrl=%s leagueId=%s memberships=%d races=%d race0=%o',
'[LeagueService.getLeagueDetailData] baseUrl=%s leagueId=%s memberships=%d races=%d race0=%o apiDto=%o',
this.baseUrl,
leagueId,
membershipCount,
racesCount,
race0,
apiDto
);
}
if (!apiDto || !apiDto.leagues) {
return Result.err({ type: 'notFound', message: 'Leagues not found' });
}
const league = apiDto.leagues.find(l => l.id === leagueId);
const leagues = Array.isArray(apiDto.leagues) ? apiDto.leagues : [];
const league = leagues.find(l => l.id === leagueId);
if (!league) {
return Result.err({ type: 'notFound', message: 'League not found' });
}
@@ -220,7 +221,7 @@ export class LeagueService implements Service {
console.warn('Failed to fetch league scoring config', e);
}
const races: RaceDTO[] = (racesPageData.races || []).map((r) => ({
const races: RaceDTO[] = (racesPageData?.races || []).map((r) => ({
id: r.id,
name: `${r.track} - ${r.car}`,
date: r.scheduledAt,

View File

@@ -183,4 +183,9 @@ export interface LeagueRepository {
getLeagueMembers(leagueId: string): Promise<LeagueMember[]>;
getPendingRequests(leagueId: string): Promise<LeaguePendingRequest[]>;
addLeagueMembers(leagueId: string, members: LeagueMember[]): Promise<void>;
updateLeagueMember(leagueId: string, driverId: string, updates: Partial<LeagueMember>): Promise<void>;
removeLeagueMember(leagueId: string, driverId: string): Promise<void>;
addPendingRequests(leagueId: string, requests: LeaguePendingRequest[]): Promise<void>;
removePendingRequest(leagueId: string, requestId: string): Promise<void>;
}

View File

@@ -11,15 +11,26 @@ export class ApproveMembershipRequestUseCase {
) {}
async execute(command: ApproveMembershipRequestCommand): Promise<void> {
// TODO: Implement approve membership request logic
// This is a placeholder implementation
// In a real implementation, this would:
// 1. Validate the league exists
// 2. Validate the admin has permission to approve
// 3. Find the pending request
// 4. Add the driver to the league as a member
// 5. Remove the pending request
// 6. Emit appropriate events
throw new Error('ApproveMembershipRequestUseCase not implemented');
const league = await this.leagueRepository.findById(command.leagueId);
if (!league) {
throw new Error('League not found');
}
const requests = await this.leagueRepository.getPendingRequests(command.leagueId);
const request = requests.find(r => r.id === command.requestId);
if (!request) {
throw new Error('Request not found');
}
await this.leagueRepository.addLeagueMembers(command.leagueId, [
{
driverId: request.driverId,
name: request.name,
role: 'member',
joinDate: new Date(),
},
]);
await this.leagueRepository.removePendingRequest(command.leagueId, command.requestId);
}
}

View File

@@ -11,14 +11,6 @@ export class DemoteAdminUseCase {
) {}
async execute(command: DemoteAdminCommand): Promise<void> {
// TODO: Implement demote admin logic
// This is a placeholder implementation
// In a real implementation, this would:
// 1. Validate the league exists
// 2. Validate the admin has permission to demote
// 3. Find the admin to demote
// 4. Update the admin's role to member
// 5. Emit appropriate events
throw new Error('DemoteAdminUseCase not implemented');
await this.leagueRepository.updateLeagueMember(command.leagueId, command.targetDriverId, { role: 'member' });
}
}

View File

@@ -1,4 +1,4 @@
import { LeagueRepository } from '../ports/LeagueRepository';
import { LeagueRepository, LeagueData } from '../ports/LeagueRepository';
import { DriverRepository } from '../ports/DriverRepository';
import { EventPublisher } from '../ports/EventPublisher';
import { JoinLeagueCommand } from '../ports/JoinLeagueCommand';
@@ -11,16 +11,34 @@ export class JoinLeagueUseCase {
) {}
async execute(command: JoinLeagueCommand): Promise<void> {
// TODO: Implement join league logic
// This is a placeholder implementation
// In a real implementation, this would:
// 1. Validate the league exists
// 2. Validate the driver exists
// 3. Check if the driver is already a member
// 4. Check if the league is full
// 5. Check if approval is required
// 6. Add the driver to the league (or create a pending request)
// 7. Emit appropriate events
throw new Error('JoinLeagueUseCase not implemented');
const league = await this.leagueRepository.findById(command.leagueId);
if (!league) {
throw new Error('League not found');
}
const driver = await this.driverRepository.findDriverById(command.driverId);
if (!driver) {
throw new Error('Driver not found');
}
if (league.approvalRequired) {
await this.leagueRepository.addPendingRequests(command.leagueId, [
{
id: `request-${Date.now()}`,
driverId: command.driverId,
name: driver.name,
requestDate: new Date(),
},
]);
} else {
await this.leagueRepository.addLeagueMembers(command.leagueId, [
{
driverId: command.driverId,
name: driver.name,
role: 'member',
joinDate: new Date(),
},
]);
}
}
}

View File

@@ -11,14 +11,6 @@ export class LeaveLeagueUseCase {
) {}
async execute(command: LeaveLeagueCommand): Promise<void> {
// TODO: Implement leave league logic
// This is a placeholder implementation
// In a real implementation, this would:
// 1. Validate the league exists
// 2. Validate the driver exists
// 3. Check if the driver is a member of the league
// 4. Remove the driver from the league
// 5. Emit appropriate events
throw new Error('LeaveLeagueUseCase not implemented');
await this.leagueRepository.removeLeagueMember(command.leagueId, command.driverId);
}
}

View File

@@ -11,14 +11,6 @@ export class PromoteMemberUseCase {
) {}
async execute(command: PromoteMemberCommand): Promise<void> {
// TODO: Implement promote member logic
// This is a placeholder implementation
// In a real implementation, this would:
// 1. Validate the league exists
// 2. Validate the admin has permission to promote
// 3. Find the member to promote
// 4. Update the member's role to admin
// 5. Emit appropriate events
throw new Error('PromoteMemberUseCase not implemented');
await this.leagueRepository.updateLeagueMember(command.leagueId, command.targetDriverId, { role: 'admin' });
}
}

View File

@@ -11,14 +11,6 @@ export class RejectMembershipRequestUseCase {
) {}
async execute(command: RejectMembershipRequestCommand): Promise<void> {
// TODO: Implement reject membership request logic
// This is a placeholder implementation
// In a real implementation, this would:
// 1. Validate the league exists
// 2. Validate the admin has permission to reject
// 3. Find the pending request
// 4. Remove the pending request
// 5. Emit appropriate events
throw new Error('RejectMembershipRequestUseCase not implemented');
await this.leagueRepository.removePendingRequest(command.leagueId, command.requestId);
}
}

View File

@@ -11,14 +11,6 @@ export class RemoveMemberUseCase {
) {}
async execute(command: RemoveMemberCommand): Promise<void> {
// TODO: Implement remove member logic
// This is a placeholder implementation
// In a real implementation, this would:
// 1. Validate the league exists
// 2. Validate the admin has permission to remove
// 3. Find the member to remove
// 4. Remove the member from the league
// 5. Emit appropriate events
throw new Error('RemoveMemberUseCase not implemented');
await this.leagueRepository.removeLeagueMember(command.leagueId, command.targetDriverId);
}
}

View File

@@ -4,6 +4,7 @@ import { Result } from '@core/shared/domain/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { PaymentStatus, PaymentType } from '../../domain/entities/Payment';
import type { PaymentRepository } from '../../domain/repositories/PaymentRepository';
import type { SponsorRepository } from '@core/racing/domain/repositories/SponsorRepository';
export interface SponsorBillingStats {
totalSpent: number;
@@ -55,7 +56,7 @@ export interface GetSponsorBillingResult {
stats: SponsorBillingStats;
}
export type GetSponsorBillingErrorCode = never;
export type GetSponsorBillingErrorCode = 'SPONSOR_NOT_FOUND';
export class GetSponsorBillingUseCase
implements UseCase<GetSponsorBillingInput, GetSponsorBillingResult, GetSponsorBillingErrorCode>
@@ -63,11 +64,20 @@ export class GetSponsorBillingUseCase
constructor(
private readonly paymentRepository: PaymentRepository,
private readonly seasonSponsorshipRepository: SeasonSponsorshipRepository,
private readonly sponsorRepository: SponsorRepository,
) {}
async execute(input: GetSponsorBillingInput): Promise<Result<GetSponsorBillingResult, ApplicationErrorCode<GetSponsorBillingErrorCode>>> {
const { sponsorId } = input;
const sponsor = await this.sponsorRepository.findById(sponsorId);
if (!sponsor) {
return Result.err({
code: 'SPONSOR_NOT_FOUND',
details: { message: 'Sponsor not found' },
});
}
// In this in-memory implementation we derive billing data from payments
// where the sponsor is the payer.
const payments = await this.paymentRepository.findByFilters({

View File

@@ -88,4 +88,29 @@ export class Track extends Entity<string> {
gameId: TrackGameId.create(props.gameId),
});
}
}
update(props: Partial<{
name: string;
shortName: string;
country: string;
category: TrackCategory;
difficulty: TrackDifficulty;
lengthKm: number;
turns: number;
imageUrl: string;
gameId: string;
}>): Track {
return new Track({
id: this.id,
name: props.name ? TrackName.create(props.name) : this.name,
shortName: props.shortName ? TrackShortName.create(props.shortName) : this.shortName,
country: props.country ? TrackCountry.create(props.country) : this.country,
category: props.category ?? this.category,
difficulty: props.difficulty ?? this.difficulty,
lengthKm: props.lengthKm ? TrackLength.create(props.lengthKm) : this.lengthKm,
turns: props.turns ? TrackTurns.create(props.turns) : this.turns,
imageUrl: props.imageUrl ? TrackImageUrl.create(props.imageUrl) : this.imageUrl,
gameId: props.gameId ? TrackGameId.create(props.gameId) : this.gameId,
});
}
}

View File

@@ -0,0 +1,70 @@
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { CreateLeagueUseCase } from '../../../core/leagues/application/use-cases/CreateLeagueUseCase';
import { GetLeagueUseCase } from '../../../core/leagues/application/use-cases/GetLeagueUseCase';
import { GetLeagueRosterUseCase } from '../../../core/leagues/application/use-cases/GetLeagueRosterUseCase';
import { JoinLeagueUseCase } from '../../../core/leagues/application/use-cases/JoinLeagueUseCase';
import { LeaveLeagueUseCase } from '../../../core/leagues/application/use-cases/LeaveLeagueUseCase';
import { ApproveMembershipRequestUseCase } from '../../../core/leagues/application/use-cases/ApproveMembershipRequestUseCase';
import { RejectMembershipRequestUseCase } from '../../../core/leagues/application/use-cases/RejectMembershipRequestUseCase';
import { PromoteMemberUseCase } from '../../../core/leagues/application/use-cases/PromoteMemberUseCase';
import { DemoteAdminUseCase } from '../../../core/leagues/application/use-cases/DemoteAdminUseCase';
import { RemoveMemberUseCase } from '../../../core/leagues/application/use-cases/RemoveMemberUseCase';
import { LeagueCreateCommand } from '../../../core/leagues/application/ports/LeagueCreateCommand';
export class LeaguesTestContext {
public readonly leagueRepository: InMemoryLeagueRepository;
public readonly driverRepository: InMemoryDriverRepository;
public readonly eventPublisher: InMemoryEventPublisher;
public readonly createLeagueUseCase: CreateLeagueUseCase;
public readonly getLeagueUseCase: GetLeagueUseCase;
public readonly getLeagueRosterUseCase: GetLeagueRosterUseCase;
public readonly joinLeagueUseCase: JoinLeagueUseCase;
public readonly leaveLeagueUseCase: LeaveLeagueUseCase;
public readonly approveMembershipRequestUseCase: ApproveMembershipRequestUseCase;
public readonly rejectMembershipRequestUseCase: RejectMembershipRequestUseCase;
public readonly promoteMemberUseCase: PromoteMemberUseCase;
public readonly demoteAdminUseCase: DemoteAdminUseCase;
public readonly removeMemberUseCase: RemoveMemberUseCase;
constructor() {
this.leagueRepository = new InMemoryLeagueRepository();
this.driverRepository = new InMemoryDriverRepository();
this.eventPublisher = new InMemoryEventPublisher();
this.createLeagueUseCase = new CreateLeagueUseCase(this.leagueRepository, this.eventPublisher);
this.getLeagueUseCase = new GetLeagueUseCase(this.leagueRepository, this.eventPublisher);
this.getLeagueRosterUseCase = new GetLeagueRosterUseCase(this.leagueRepository, this.eventPublisher);
this.joinLeagueUseCase = new JoinLeagueUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher);
this.leaveLeagueUseCase = new LeaveLeagueUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher);
this.approveMembershipRequestUseCase = new ApproveMembershipRequestUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher);
this.rejectMembershipRequestUseCase = new RejectMembershipRequestUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher);
this.promoteMemberUseCase = new PromoteMemberUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher);
this.demoteAdminUseCase = new DemoteAdminUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher);
this.removeMemberUseCase = new RemoveMemberUseCase(this.leagueRepository, this.driverRepository, this.eventPublisher);
}
public clear(): void {
this.leagueRepository.clear();
this.driverRepository.clear();
this.eventPublisher.clear();
}
public async createLeague(command: Partial<LeagueCreateCommand> = {}) {
const defaultCommand: LeagueCreateCommand = {
name: 'Test League',
visibility: 'public',
ownerId: 'driver-123',
approvalRequired: false,
lateJoinAllowed: false,
bonusPointsEnabled: false,
penaltiesEnabled: false,
protestsEnabled: false,
appealsEnabled: false,
...command,
};
return await this.createLeagueUseCase.execute(defaultCommand);
}
}

View File

@@ -0,0 +1,39 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
import { LeagueCreateCommand } from '../../../../core/leagues/application/ports/LeagueCreateCommand';
describe('League Creation - Edge Cases', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should handle league with empty description', async () => {
const result = await context.createLeague({ description: '' });
expect(result.description).toBeNull();
});
it('should handle league with very long description', async () => {
const longDescription = 'a'.repeat(2000);
const result = await context.createLeague({ description: longDescription });
expect(result.description).toBe(longDescription);
});
it('should handle league with special characters in name', async () => {
const specialName = 'League! @#$%^&*()_+';
const result = await context.createLeague({ name: specialName });
expect(result.name).toBe(specialName);
});
it('should handle league with max drivers set to 1', async () => {
const result = await context.createLeague({ maxDrivers: 1 });
expect(result.maxDrivers).toBe(1);
});
it('should handle league with empty track list', async () => {
const result = await context.createLeague({ tracks: [] });
expect(result.tracks).toEqual([]);
});
});

View File

@@ -0,0 +1,69 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
import { LeagueCreateCommand } from '../../../../core/leagues/application/ports/LeagueCreateCommand';
import { InMemoryLeagueRepository } from '../../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
import { CreateLeagueUseCase } from '../../../../core/leagues/application/use-cases/CreateLeagueUseCase';
describe('League Creation - Error Handling', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should throw error when driver ID is invalid', async () => {
const command: LeagueCreateCommand = {
name: 'Test League',
visibility: 'public',
ownerId: '',
approvalRequired: false,
lateJoinAllowed: false,
bonusPointsEnabled: false,
penaltiesEnabled: false,
protestsEnabled: false,
appealsEnabled: false,
};
await expect(context.createLeagueUseCase.execute(command)).rejects.toThrow('Owner ID is required');
expect(context.eventPublisher.getLeagueCreatedEventCount()).toBe(0);
});
it('should throw error when league name is empty', async () => {
const command: LeagueCreateCommand = {
name: '',
visibility: 'public',
ownerId: 'driver-123',
approvalRequired: false,
lateJoinAllowed: false,
bonusPointsEnabled: false,
penaltiesEnabled: false,
protestsEnabled: false,
appealsEnabled: false,
};
await expect(context.createLeagueUseCase.execute(command)).rejects.toThrow();
expect(context.eventPublisher.getLeagueCreatedEventCount()).toBe(0);
});
it('should throw error when repository throws error', async () => {
const errorRepo = new InMemoryLeagueRepository();
errorRepo.create = async () => { throw new Error('Database error'); };
const errorUseCase = new CreateLeagueUseCase(errorRepo, context.eventPublisher);
const command: LeagueCreateCommand = {
name: 'Test League',
visibility: 'public',
ownerId: 'driver-123',
approvalRequired: false,
lateJoinAllowed: false,
bonusPointsEnabled: false,
penaltiesEnabled: false,
protestsEnabled: false,
appealsEnabled: false,
};
await expect(errorUseCase.execute(command)).rejects.toThrow('Database error');
expect(context.eventPublisher.getLeagueCreatedEventCount()).toBe(0);
});
});

View File

@@ -0,0 +1,90 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
import { LeagueCreateCommand } from '../../../../core/leagues/application/ports/LeagueCreateCommand';
describe('League Creation - Success Path', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should create a league with complete configuration', async () => {
const driverId = 'driver-123';
const command: LeagueCreateCommand = {
name: 'Test League',
description: 'A test league for integration testing',
visibility: 'public',
ownerId: driverId,
maxDrivers: 20,
approvalRequired: true,
lateJoinAllowed: true,
raceFrequency: 'weekly',
raceDay: 'Saturday',
raceTime: '18:00',
tracks: ['Monza', 'Spa', 'Nürburgring'],
scoringSystem: { points: [25, 18, 15, 12, 10, 8, 6, 4, 2, 1] },
bonusPointsEnabled: true,
penaltiesEnabled: true,
protestsEnabled: true,
appealsEnabled: true,
stewardTeam: ['steward-1', 'steward-2'],
gameType: 'iRacing',
skillLevel: 'Intermediate',
category: 'GT3',
tags: ['competitive', 'weekly-races'],
};
const result = await context.createLeagueUseCase.execute(command);
expect(result).toBeDefined();
expect(result.id).toBeDefined();
expect(result.name).toBe('Test League');
expect(result.ownerId).toBe(driverId);
expect(result.status).toBe('active');
expect(result.maxDrivers).toBe(20);
expect(result.tracks).toEqual(['Monza', 'Spa', 'Nürburgring']);
const savedLeague = await context.leagueRepository.findById(result.id);
expect(savedLeague).toBeDefined();
expect(savedLeague?.ownerId).toBe(driverId);
expect(context.eventPublisher.getLeagueCreatedEventCount()).toBe(1);
const events = context.eventPublisher.getLeagueCreatedEvents();
expect(events[0].leagueId).toBe(result.id);
});
it('should create a league with minimal configuration', async () => {
const driverId = 'driver-123';
const command: LeagueCreateCommand = {
name: 'Minimal League',
visibility: 'public',
ownerId: driverId,
approvalRequired: false,
lateJoinAllowed: false,
bonusPointsEnabled: false,
penaltiesEnabled: false,
protestsEnabled: false,
appealsEnabled: false,
};
const result = await context.createLeagueUseCase.execute(command);
expect(result).toBeDefined();
expect(result.name).toBe('Minimal League');
expect(result.status).toBe('active');
expect(result.description).toBeNull();
expect(context.eventPublisher.getLeagueCreatedEventCount()).toBe(1);
});
it('should create a league with public visibility', async () => {
const result = await context.createLeague({ name: 'Public League', visibility: 'public' });
expect(result.visibility).toBe('public');
});
it('should create a league with private visibility', async () => {
const result = await context.createLeague({ name: 'Private League', visibility: 'private' });
expect(result.visibility).toBe('private');
});
});

View File

@@ -0,0 +1,38 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
describe('League Detail - Success Path', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should retrieve complete league detail with all data', async () => {
const driverId = 'driver-123';
const league = await context.createLeague({
name: 'Complete League',
description: 'A league with all data',
ownerId: driverId,
});
const result = await context.getLeagueUseCase.execute({ leagueId: league.id, driverId });
expect(result).toBeDefined();
expect(result.id).toBe(league.id);
expect(result.name).toBe('Complete League');
expect(context.eventPublisher.getLeagueAccessedEventCount()).toBe(1);
});
it('should retrieve league detail with minimal data', async () => {
const driverId = 'driver-123';
const league = await context.createLeague({ name: 'Minimal League', ownerId: driverId });
const result = await context.getLeagueUseCase.execute({ leagueId: league.id, driverId });
expect(result).toBeDefined();
expect(result.name).toBe('Minimal League');
expect(context.eventPublisher.getLeagueAccessedEventCount()).toBe(1);
});
});

View File

@@ -0,0 +1,29 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
describe('League Discovery - Search', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should find leagues by name', async () => {
await context.createLeague({ name: 'Formula 1' });
await context.createLeague({ name: 'GT3 Masters' });
const results = await context.leagueRepository.search('Formula');
expect(results).toHaveLength(1);
expect(results[0].name).toBe('Formula 1');
});
it('should find leagues by description', async () => {
await context.createLeague({ name: 'League A', description: 'Competitive racing' });
await context.createLeague({ name: 'League B', description: 'Casual fun' });
const results = await context.leagueRepository.search('Competitive');
expect(results).toHaveLength(1);
expect(results[0].name).toBe('League A');
});
});

View File

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

View File

@@ -1,901 +0,0 @@
/**
* Integration Test: League Settings Use Case Orchestration
*
* Tests the orchestration logic of league settings-related Use Cases:
* - GetLeagueSettingsUseCase: Retrieves league settings
* - UpdateLeagueBasicInfoUseCase: Updates league basic information
* - UpdateLeagueStructureUseCase: Updates league structure settings
* - UpdateLeagueScoringUseCase: Updates league scoring configuration
* - UpdateLeagueStewardingUseCase: Updates league stewarding configuration
* - ArchiveLeagueUseCase: Archives a league
* - UnarchiveLeagueUseCase: Unarchives a league
* - DeleteLeagueUseCase: Deletes a league
* - ExportLeagueDataUseCase: Exports league data
* - ImportLeagueDataUseCase: Imports league data
* - ResetLeagueStatisticsUseCase: Resets league statistics
* - ResetLeagueStandingsUseCase: Resets league standings
* - ResetLeagueScheduleUseCase: Resets league schedule
* - ResetLeagueRosterUseCase: Resets league roster
* - ResetLeagueWalletUseCase: Resets league wallet
* - ResetLeagueSponsorshipsUseCase: Resets league sponsorships
* - ResetLeagueStewardingUseCase: Resets league stewarding
* - ResetLeagueProtestsUseCase: Resets league protests
* - ResetLeaguePenaltiesUseCase: Resets league penalties
* - ResetLeagueAppealsUseCase: Resets league appeals
* - ResetLeagueIncidentsUseCase: Resets league incidents
* - ResetLeagueEverythingUseCase: Resets everything in the league
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { GetLeagueSettingsUseCase } from '../../../core/leagues/use-cases/GetLeagueSettingsUseCase';
import { UpdateLeagueBasicInfoUseCase } from '../../../core/leagues/use-cases/UpdateLeagueBasicInfoUseCase';
import { UpdateLeagueStructureUseCase } from '../../../core/leagues/use-cases/UpdateLeagueStructureUseCase';
import { UpdateLeagueScoringUseCase } from '../../../core/leagues/use-cases/UpdateLeagueScoringUseCase';
import { UpdateLeagueStewardingUseCase } from '../../../core/leagues/use-cases/UpdateLeagueStewardingUseCase';
import { ArchiveLeagueUseCase } from '../../../core/leagues/use-cases/ArchiveLeagueUseCase';
import { UnarchiveLeagueUseCase } from '../../../core/leagues/use-cases/UnarchiveLeagueUseCase';
import { DeleteLeagueUseCase } from '../../../core/leagues/use-cases/DeleteLeagueUseCase';
import { ExportLeagueDataUseCase } from '../../../core/leagues/use-cases/ExportLeagueDataUseCase';
import { ImportLeagueDataUseCase } from '../../../core/leagues/use-cases/ImportLeagueDataUseCase';
import { ResetLeagueStatisticsUseCase } from '../../../core/leagues/use-cases/ResetLeagueStatisticsUseCase';
import { ResetLeagueStandingsUseCase } from '../../../core/leagues/use-cases/ResetLeagueStandingsUseCase';
import { ResetLeagueScheduleUseCase } from '../../../core/leagues/use-cases/ResetLeagueScheduleUseCase';
import { ResetLeagueRosterUseCase } from '../../../core/leagues/use-cases/ResetLeagueRosterUseCase';
import { ResetLeagueWalletUseCase } from '../../../core/leagues/use-cases/ResetLeagueWalletUseCase';
import { ResetLeagueSponsorshipsUseCase } from '../../../core/leagues/use-cases/ResetLeagueSponsorshipsUseCase';
import { ResetLeagueStewardingUseCase } from '../../../core/leagues/use-cases/ResetLeagueStewardingUseCase';
import { ResetLeagueProtestsUseCase } from '../../../core/leagues/use-cases/ResetLeagueProtestsUseCase';
import { ResetLeaguePenaltiesUseCase } from '../../../core/leagues/use-cases/ResetLeaguePenaltiesUseCase';
import { ResetLeagueAppealsUseCase } from '../../../core/leagues/use-cases/ResetLeagueAppealsUseCase';
import { ResetLeagueIncidentsUseCase } from '../../../core/leagues/use-cases/ResetLeagueIncidentsUseCase';
import { ResetLeagueEverythingUseCase } from '../../../core/leagues/use-cases/ResetLeagueEverythingUseCase';
import { LeagueSettingsQuery } from '../../../core/leagues/ports/LeagueSettingsQuery';
import { UpdateLeagueBasicInfoCommand } from '../../../core/leagues/ports/UpdateLeagueBasicInfoCommand';
import { UpdateLeagueStructureCommand } from '../../../core/leagues/ports/UpdateLeagueStructureCommand';
import { UpdateLeagueScoringCommand } from '../../../core/leagues/ports/UpdateLeagueScoringCommand';
import { UpdateLeagueStewardingCommand } from '../../../core/leagues/ports/UpdateLeagueStewardingCommand';
import { ArchiveLeagueCommand } from '../../../core/leagues/ports/ArchiveLeagueCommand';
import { UnarchiveLeagueCommand } from '../../../core/leagues/ports/UnarchiveLeagueCommand';
import { DeleteLeagueCommand } from '../../../core/leagues/ports/DeleteLeagueCommand';
import { ExportLeagueDataCommand } from '../../../core/leagues/ports/ExportLeagueDataCommand';
import { ImportLeagueDataCommand } from '../../../core/leagues/ports/ImportLeagueDataCommand';
import { ResetLeagueStatisticsCommand } from '../../../core/leagues/ports/ResetLeagueStatisticsCommand';
import { ResetLeagueStandingsCommand } from '../../../core/leagues/ports/ResetLeagueStandingsCommand';
import { ResetLeagueScheduleCommand } from '../../../core/leagues/ports/ResetLeagueScheduleCommand';
import { ResetLeagueRosterCommand } from '../../../core/leagues/ports/ResetLeagueRosterCommand';
import { ResetLeagueWalletCommand } from '../../../core/leagues/ports/ResetLeagueWalletCommand';
import { ResetLeagueSponsorshipsCommand } from '../../../core/leagues/ports/ResetLeagueSponsorshipsCommand';
import { ResetLeagueStewardingCommand } from '../../../core/leagues/ports/ResetLeagueStewardingCommand';
import { ResetLeagueProtestsCommand } from '../../../core/leagues/ports/ResetLeagueProtestsCommand';
import { ResetLeaguePenaltiesCommand } from '../../../core/leagues/ports/ResetLeaguePenaltiesCommand';
import { ResetLeagueAppealsCommand } from '../../../core/leagues/ports/ResetLeagueAppealsCommand';
import { ResetLeagueIncidentsCommand } from '../../../core/leagues/ports/ResetLeagueIncidentsCommand';
import { ResetLeagueEverythingCommand } from '../../../core/leagues/ports/ResetLeagueEverythingCommand';
describe('League Settings Use Case Orchestration', () => {
let leagueRepository: InMemoryLeagueRepository;
let driverRepository: InMemoryDriverRepository;
let eventPublisher: InMemoryEventPublisher;
let getLeagueSettingsUseCase: GetLeagueSettingsUseCase;
let updateLeagueBasicInfoUseCase: UpdateLeagueBasicInfoUseCase;
let updateLeagueStructureUseCase: UpdateLeagueStructureUseCase;
let updateLeagueScoringUseCase: UpdateLeagueScoringUseCase;
let updateLeagueStewardingUseCase: UpdateLeagueStewardingUseCase;
let archiveLeagueUseCase: ArchiveLeagueUseCase;
let unarchiveLeagueUseCase: UnarchiveLeagueUseCase;
let deleteLeagueUseCase: DeleteLeagueUseCase;
let exportLeagueDataUseCase: ExportLeagueDataUseCase;
let importLeagueDataUseCase: ImportLeagueDataUseCase;
let resetLeagueStatisticsUseCase: ResetLeagueStatisticsUseCase;
let resetLeagueStandingsUseCase: ResetLeagueStandingsUseCase;
let resetLeagueScheduleUseCase: ResetLeagueScheduleUseCase;
let resetLeagueRosterUseCase: ResetLeagueRosterUseCase;
let resetLeagueWalletUseCase: ResetLeagueWalletUseCase;
let resetLeagueSponsorshipsUseCase: ResetLeagueSponsorshipsUseCase;
let resetLeagueStewardingUseCase: ResetLeagueStewardingUseCase;
let resetLeagueProtestsUseCase: ResetLeagueProtestsUseCase;
let resetLeaguePenaltiesUseCase: ResetLeaguePenaltiesUseCase;
let resetLeagueAppealsUseCase: ResetLeagueAppealsUseCase;
let resetLeagueIncidentsUseCase: ResetLeagueIncidentsUseCase;
let resetLeagueEverythingUseCase: ResetLeagueEverythingUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// leagueRepository = new InMemoryLeagueRepository();
// driverRepository = new InMemoryDriverRepository();
// eventPublisher = new InMemoryEventPublisher();
// getLeagueSettingsUseCase = new GetLeagueSettingsUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// updateLeagueBasicInfoUseCase = new UpdateLeagueBasicInfoUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// updateLeagueStructureUseCase = new UpdateLeagueStructureUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// updateLeagueScoringUseCase = new UpdateLeagueScoringUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// updateLeagueStewardingUseCase = new UpdateLeagueStewardingUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// archiveLeagueUseCase = new ArchiveLeagueUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// unarchiveLeagueUseCase = new UnarchiveLeagueUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// deleteLeagueUseCase = new DeleteLeagueUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// exportLeagueDataUseCase = new ExportLeagueDataUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// importLeagueDataUseCase = new ImportLeagueDataUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueStatisticsUseCase = new ResetLeagueStatisticsUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueStandingsUseCase = new ResetLeagueStandingsUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueScheduleUseCase = new ResetLeagueScheduleUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueRosterUseCase = new ResetLeagueRosterUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueWalletUseCase = new ResetLeagueWalletUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueSponsorshipsUseCase = new ResetLeagueSponsorshipsUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueStewardingUseCase = new ResetLeagueStewardingUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueProtestsUseCase = new ResetLeagueProtestsUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeaguePenaltiesUseCase = new ResetLeaguePenaltiesUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueAppealsUseCase = new ResetLeagueAppealsUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueIncidentsUseCase = new ResetLeagueIncidentsUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
// resetLeagueEverythingUseCase = new ResetLeagueEverythingUseCase({
// leagueRepository,
// driverRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// leagueRepository.clear();
// driverRepository.clear();
// eventPublisher.clear();
});
describe('GetLeagueSettingsUseCase - Success Path', () => {
it('should retrieve league basic information', async () => {
// TODO: Implement test
// Scenario: Admin views league basic information
// Given: A league exists with basic information
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league name
// And: The result should contain the league description
// And: The result should contain the league visibility
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league structure settings', async () => {
// TODO: Implement test
// Scenario: Admin views league structure settings
// Given: A league exists with structure settings
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain max drivers
// And: The result should contain approval requirement
// And: The result should contain late join option
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league scoring configuration', async () => {
// TODO: Implement test
// Scenario: Admin views league scoring configuration
// Given: A league exists with scoring configuration
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain scoring preset
// And: The result should contain custom points
// And: The result should contain bonus points configuration
// And: The result should contain penalty configuration
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding configuration', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding configuration
// Given: A league exists with stewarding configuration
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain protest configuration
// And: The result should contain appeal configuration
// And: The result should contain steward team
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league creation date', async () => {
// TODO: Implement test
// Scenario: Admin views league creation date
// Given: A league exists with creation date
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league creation date
// And: The date should be formatted correctly
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league last updated date', async () => {
// TODO: Implement test
// Scenario: Admin views league last updated date
// Given: A league exists with last updated date
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league last updated date
// And: The date should be formatted correctly
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league owner information', async () => {
// TODO: Implement test
// Scenario: Admin views league owner information
// Given: A league exists with owner information
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league owner information
// And: The owner should be clickable to view their profile
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league member count', async () => {
// TODO: Implement test
// Scenario: Admin views league member count
// Given: A league exists with members
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league member count
// And: The count should be accurate
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league race count', async () => {
// TODO: Implement test
// Scenario: Admin views league race count
// Given: A league exists with races
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league race count
// And: The count should be accurate
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league sponsor count', async () => {
// TODO: Implement test
// Scenario: Admin views league sponsor count
// Given: A league exists with sponsors
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league sponsor count
// And: The count should be accurate
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league wallet balance', async () => {
// TODO: Implement test
// Scenario: Admin views league wallet balance
// Given: A league exists with wallet balance
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league wallet balance
// And: The balance should be displayed as currency amount
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league total revenue', async () => {
// TODO: Implement test
// Scenario: Admin views league total revenue
// Given: A league exists with total revenue
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league total revenue
// And: The revenue should be displayed as currency amount
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league total fees', async () => {
// TODO: Implement test
// Scenario: Admin views league total fees
// Given: A league exists with total fees
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league total fees
// And: The fees should be displayed as currency amount
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league pending payouts', async () => {
// TODO: Implement test
// Scenario: Admin views league pending payouts
// Given: A league exists with pending payouts
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league pending payouts
// And: The payouts should be displayed as currency amount
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league net balance', async () => {
// TODO: Implement test
// Scenario: Admin views league net balance
// Given: A league exists with net balance
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league net balance
// And: The net balance should be displayed as currency amount
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league transaction count', async () => {
// TODO: Implement test
// Scenario: Admin views league transaction count
// Given: A league exists with transaction count
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league transaction count
// And: The count should be accurate
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league average transaction amount', async () => {
// TODO: Implement test
// Scenario: Admin views league average transaction amount
// Given: A league exists with average transaction amount
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league average transaction amount
// And: The amount should be displayed as currency amount
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league total race time', async () => {
// TODO: Implement test
// Scenario: Admin views league total race time
// Given: A league exists with total race time
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league total race time
// And: The time should be formatted correctly
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league average race time', async () => {
// TODO: Implement test
// Scenario: Admin views league average race time
// Given: A league exists with average race time
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league average race time
// And: The time should be formatted correctly
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league best lap time', async () => {
// TODO: Implement test
// Scenario: Admin views league best lap time
// Given: A league exists with best lap time
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league best lap time
// And: The time should be formatted correctly
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league average lap time', async () => {
// TODO: Implement test
// Scenario: Admin views league average lap time
// Given: A league exists with average lap time
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league average lap time
// And: The time should be formatted correctly
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league consistency score', async () => {
// TODO: Implement test
// Scenario: Admin views league consistency score
// Given: A league exists with consistency score
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league consistency score
// And: The score should be displayed as percentage or numeric value
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league aggression score', async () => {
// TODO: Implement test
// Scenario: Admin views league aggression score
// Given: A league exists with aggression score
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league aggression score
// And: The score should be displayed as percentage or numeric value
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league safety score', async () => {
// TODO: Implement test
// Scenario: Admin views league safety score
// Given: A league exists with safety score
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league safety score
// And: The score should be displayed as percentage or numeric value
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league racecraft score', async () => {
// TODO: Implement test
// Scenario: Admin views league racecraft score
// Given: A league exists with racecraft score
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league racecraft score
// And: The score should be displayed as percentage or numeric value
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league overall rating', async () => {
// TODO: Implement test
// Scenario: Admin views league overall rating
// Given: A league exists with overall rating
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league overall rating
// And: The rating should be displayed as stars or numeric value
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league rating trend', async () => {
// TODO: Implement test
// Scenario: Admin views league rating trend
// Given: A league exists with rating trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league rating trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league rank trend', async () => {
// TODO: Implement test
// Scenario: Admin views league rank trend
// Given: A league exists with rank trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league rank trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league points trend', async () => {
// TODO: Implement test
// Scenario: Admin views league points trend
// Given: A league exists with points trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league points trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league win rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league win rate trend
// Given: A league exists with win rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league win rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league podium rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league podium rate trend
// Given: A league exists with podium rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league podium rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league DNF rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league DNF rate trend
// Given: A league exists with DNF rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league DNF rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league incident rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league incident rate trend
// Given: A league exists with incident rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league incident rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league penalty rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league penalty rate trend
// Given: A league exists with penalty rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league penalty rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league protest rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league protest rate trend
// Given: A league exists with protest rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league protest rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action rate trend
// Given: A league exists with stewarding action rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding time trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding time trend
// Given: A league exists with stewarding time trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding time trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league protest resolution time trend', async () => {
// TODO: Implement test
// Scenario: Admin views league protest resolution time trend
// Given: A league exists with protest resolution time trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league protest resolution time trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league penalty appeal success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league penalty appeal success rate trend
// Given: A league exists with penalty appeal success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league penalty appeal success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league protest success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league protest success rate trend
// Given: A league exists with protest success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league protest success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action success rate trend
// Given: A league exists with stewarding action success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action appeal success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action appeal success rate trend
// Given: A league exists with stewarding action appeal success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action appeal success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action penalty success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action penalty success rate trend
// Given: A league exists with stewarding action penalty success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action penalty success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action protest success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action protest success rate trend
// Given: A league exists with stewarding action protest success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action protest success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action appeal penalty success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action appeal penalty success rate trend
// Given: A league exists with stewarding action appeal penalty success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action appeal penalty success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action appeal protest success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action appeal protest success rate trend
// Given: A league exists with stewarding action appeal protest success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action appeal protest success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action penalty protest success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action penalty protest success rate trend
// Given: A league exists with stewarding action penalty protest success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action penalty protest success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action appeal penalty protest success rate trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action appeal penalty protest success rate trend
// Given: A league exists with stewarding action appeal penalty protest success rate trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action appeal penalty protest success rate trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action appeal penalty protest resolution time trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action appeal penalty protest resolution time trend
// Given: A league exists with stewarding action appeal penalty protest resolution time trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action appeal penalty protest resolution time trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should retrieve league stewarding action appeal penalty protest success rate and resolution time trend', async () => {
// TODO: Implement test
// Scenario: Admin views league stewarding action appeal penalty protest success rate and resolution time trend
// Given: A league exists with stewarding action appeal penalty protest success rate and resolution time trend
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain the league stewarding action appeal penalty protest success rate trend
// And: The result should contain the league stewarding action appeal penalty protest resolution time trend
// And: Trends should show improvement or decline
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
});
describe('GetLeagueSettingsUseCase - Edge Cases', () => {
it('should handle league with no statistics', async () => {
// TODO: Implement test
// Scenario: League with no statistics
// Given: A league exists
// And: The league has no statistics
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain league settings
// And: Statistics sections should be empty or show default values
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should handle league with no financial data', async () => {
// TODO: Implement test
// Scenario: League with no financial data
// Given: A league exists
// And: The league has no financial data
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain league settings
// And: Financial sections should be empty or show default values
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should handle league with no trend data', async () => {
// TODO: Implement test
// Scenario: League with no trend data
// Given: A league exists
// And: The league has no trend data
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain league settings
// And: Trend sections should be empty or show default values
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
it('should handle league with no data at all', async () => {
// TODO: Implement test
// Scenario: League with absolutely no data
// Given: A league exists
// And: The league has no statistics
// And: The league has no financial data
// And: The league has no trend data
// When: GetLeagueSettingsUseCase.execute() is called with league ID
// Then: The result should contain basic league settings
// And: All sections should be empty or show default values
// And: EventPublisher should emit LeagueSettingsAccessedEvent
});
});
describe('GetLeagueSettingsUseCase - 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: GetLeagueSettingsUseCase.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: GetLeagueSettingsUseCase.execute() is called with invalid league ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should handle repository errors gracefully', async () => {
// TODO: Implement test
// Scenario: Repository throws error
// Given: A league exists
// And: LeagueRepository throws an error during query
// When: GetLeagueSettingsUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('League Settings Data Orchestration', () => {
it('should correctly calculate league statistics from race results', async () => {
// TODO: Implement test
// Scenario: League statistics calculation
// Given: A league exists
// And: The league has 10 completed races
// And: The league has 3 wins
// And: The league has 5 podiums
// When: GetLeagueSettingsUseCase.execute() is called
// Then: League statistics should show:
// - Starts: 10
// - Wins: 3
// - Podiums: 5
// - Rating: Calculated based on performance
// - Rank: Calculated based on rating
});
it('should correctly format career history with league and team information', async () => {
// TODO: Implement test
// Scenario: Career history formatting
// Given: A league exists
// And: The league has participated in 2 leagues
// And: The league has been on 3 teams across seasons
// When: GetLeagueSettingsUseCase.execute() is called
// Then: Career history should show:
// - League A: Season 2024, Team X
// - League B: Season 2024, Team Y
// - League A: Season 2023, Team Z
});
it('should correctly format recent race results with proper details', async () => {
// TODO: Implement test
// Scenario: Recent race results formatting
// Given: A league exists
// And: The league has 5 recent race results
// When: GetLeagueSettingsUseCase.execute() is called
// Then: Recent race results should show:
// - Race name
// - Track name
// - Finishing position
// - Points earned
// - Race date (sorted newest first)
});
it('should correctly aggregate championship standings across leagues', async () => {
// TODO: Implement test
// Scenario: Championship standings aggregation
// Given: A league exists
// And: The league 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: GetLeagueSettingsUseCase.execute() is called
// Then: Championship standings should show:
// - League A: Position 5, 150 points, 20 drivers
// - League B: Position 12, 85 points, 15 drivers
});
it('should correctly format social links with proper URLs', async () => {
// TODO: Implement test
// Scenario: Social links formatting
// Given: A league exists
// And: The league has social links (Discord, Twitter, iRacing)
// When: GetLeagueSettingsUseCase.execute() is called
// Then: Social links should show:
// - Discord: https://discord.gg/username
// - Twitter: https://twitter.com/username
// - iRacing: https://members.iracing.com/membersite/member/profile?username=username
});
it('should correctly format team affiliation with role', async () => {
// TODO: Implement test
// Scenario: Team affiliation formatting
// Given: A league exists
// And: The league is affiliated with Team XYZ
// And: The league's role is "Driver"
// When: GetLeagueSettingsUseCase.execute() is called
// Then: Team affiliation should show:
// - Team name: Team XYZ
// - Team logo: (if available)
// - Driver role: Driver
});
});
});

View File

@@ -0,0 +1,129 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
describe('League Roster - Actions', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should allow a driver to join a public league without approval', async () => {
const league = await context.createLeague({ approvalRequired: false });
const driverId = 'driver-joiner';
context.driverRepository.addDriver({
id: driverId,
name: 'Joiner Driver',
rating: 1500,
rank: 100,
avatar: undefined,
starts: 0,
wins: 0,
podiums: 0,
leagues: 0
});
await context.joinLeagueUseCase.execute({ leagueId: league.id, driverId });
const members = await context.leagueRepository.getLeagueMembers(league.id);
expect(members.some(m => m.driverId === driverId)).toBe(true);
});
it('should create a pending request when joining a league requiring approval', async () => {
const league = await context.createLeague({ approvalRequired: true });
const driverId = 'driver-requester';
context.driverRepository.addDriver({
id: driverId,
name: 'Requester Driver',
rating: 1500,
rank: 100,
avatar: undefined,
starts: 0,
wins: 0,
podiums: 0,
leagues: 0
});
await context.joinLeagueUseCase.execute({ leagueId: league.id, driverId });
const requests = await context.leagueRepository.getPendingRequests(league.id);
expect(requests.some(r => r.driverId === driverId)).toBe(true);
});
it('should allow an admin to approve a membership request', async () => {
const ownerId = 'driver-owner';
const league = await context.createLeague({ ownerId, approvalRequired: true });
const driverId = 'driver-requester';
context.driverRepository.addDriver({
id: driverId,
name: 'Requester Driver',
rating: 1500,
rank: 100,
avatar: undefined,
starts: 0,
wins: 0,
podiums: 0,
leagues: 0
});
await context.joinLeagueUseCase.execute({ leagueId: league.id, driverId });
const requests = await context.leagueRepository.getPendingRequests(league.id);
const requestId = requests[0].id;
await context.approveMembershipRequestUseCase.execute({ leagueId: league.id, requestId });
const members = await context.leagueRepository.getLeagueMembers(league.id);
expect(members.some(m => m.driverId === driverId)).toBe(true);
const updatedRequests = await context.leagueRepository.getPendingRequests(league.id);
expect(updatedRequests).toHaveLength(0);
});
it('should allow an admin to reject a membership request', async () => {
const ownerId = 'driver-owner';
const league = await context.createLeague({ ownerId, approvalRequired: true });
const driverId = 'driver-requester';
context.driverRepository.addDriver({
id: driverId,
name: 'Requester Driver',
rating: 1500,
rank: 100,
avatar: undefined,
starts: 0,
wins: 0,
podiums: 0,
leagues: 0
});
await context.joinLeagueUseCase.execute({ leagueId: league.id, driverId });
const requests = await context.leagueRepository.getPendingRequests(league.id);
const requestId = requests[0].id;
await context.rejectMembershipRequestUseCase.execute({ leagueId: league.id, requestId });
const members = await context.leagueRepository.getLeagueMembers(league.id);
expect(members.some(m => m.driverId === driverId)).toBe(false);
const updatedRequests = await context.leagueRepository.getPendingRequests(league.id);
expect(updatedRequests).toHaveLength(0);
});
it('should allow a driver to leave a league', async () => {
const league = await context.createLeague();
const driverId = 'driver-leaver';
context.leagueRepository.addLeagueMembers(league.id, [
{ driverId, name: 'Leaver', role: 'member', joinDate: new Date() }
]);
await context.leaveLeagueUseCase.execute({ leagueId: league.id, driverId });
const members = await context.leagueRepository.getLeagueMembers(league.id);
expect(members.some(m => m.driverId === driverId)).toBe(false);
});
});

View File

@@ -0,0 +1,61 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
describe('League Roster - Member Management', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should allow an admin to promote a member to admin', async () => {
const ownerId = 'driver-owner';
const league = await context.createLeague({ ownerId });
const driverId = 'driver-member';
context.leagueRepository.addLeagueMembers(league.id, [
{ driverId: ownerId, name: 'Owner', role: 'owner', joinDate: new Date() },
{ driverId: driverId, name: 'Member', role: 'member', joinDate: new Date() },
]);
await context.promoteMemberUseCase.execute({ leagueId: league.id, targetDriverId: driverId });
const members = await context.leagueRepository.getLeagueMembers(league.id);
const promotedMember = members.find(m => m.driverId === driverId);
expect(promotedMember?.role).toBe('admin');
});
it('should allow an admin to demote an admin to member', async () => {
const ownerId = 'driver-owner';
const league = await context.createLeague({ ownerId });
const adminId = 'driver-admin';
context.leagueRepository.addLeagueMembers(league.id, [
{ driverId: ownerId, name: 'Owner', role: 'owner', joinDate: new Date() },
{ driverId: adminId, name: 'Admin', role: 'admin', joinDate: new Date() },
]);
await context.demoteAdminUseCase.execute({ leagueId: league.id, targetDriverId: adminId });
const members = await context.leagueRepository.getLeagueMembers(league.id);
const demotedAdmin = members.find(m => m.driverId === adminId);
expect(demotedAdmin?.role).toBe('member');
});
it('should allow an admin to remove a member', async () => {
const ownerId = 'driver-owner';
const league = await context.createLeague({ ownerId });
const driverId = 'driver-member';
context.leagueRepository.addLeagueMembers(league.id, [
{ driverId: ownerId, name: 'Owner', role: 'owner', joinDate: new Date() },
{ driverId: driverId, name: 'Member', role: 'member', joinDate: new Date() },
]);
await context.removeMemberUseCase.execute({ leagueId: league.id, targetDriverId: driverId });
const members = await context.leagueRepository.getLeagueMembers(league.id);
expect(members.some(m => m.driverId === driverId)).toBe(false);
});
});

View File

@@ -0,0 +1,80 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
describe('League Roster - Success Path', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should retrieve complete league roster with all members', async () => {
const leagueId = 'league-123';
const ownerId = 'driver-1';
const adminId = 'driver-2';
const driverId = 'driver-3';
await context.leagueRepository.create({
id: leagueId,
name: 'Test League',
description: null,
visibility: 'public',
ownerId,
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
maxDrivers: null,
approvalRequired: true,
lateJoinAllowed: true,
raceFrequency: null,
raceDay: null,
raceTime: null,
tracks: null,
scoringSystem: null,
bonusPointsEnabled: true,
penaltiesEnabled: true,
protestsEnabled: true,
appealsEnabled: true,
stewardTeam: null,
gameType: null,
skillLevel: null,
category: null,
tags: null,
});
context.leagueRepository.addLeagueMembers(leagueId, [
{ driverId: ownerId, name: 'Owner Driver', role: 'owner', joinDate: new Date('2024-01-01') },
{ driverId: adminId, name: 'Admin Driver', role: 'admin', joinDate: new Date('2024-01-15') },
{ driverId: driverId, name: 'Regular Driver', role: 'member', joinDate: new Date('2024-02-01') },
]);
context.leagueRepository.addPendingRequests(leagueId, [
{ id: 'request-1', driverId: 'driver-4', name: 'Pending Driver', requestDate: new Date('2024-02-15') },
]);
const result = await context.getLeagueRosterUseCase.execute({ leagueId });
expect(result).toBeDefined();
expect(result.members).toHaveLength(3);
expect(result.pendingRequests).toHaveLength(1);
expect(result.stats.adminCount).toBe(2);
expect(result.stats.driverCount).toBe(1);
expect(context.eventPublisher.getLeagueRosterAccessedEventCount()).toBe(1);
});
it('should retrieve league roster with minimal members', async () => {
const ownerId = 'driver-owner';
const league = await context.createLeague({ ownerId });
context.leagueRepository.addLeagueMembers(league.id, [
{ driverId: ownerId, name: 'Owner Driver', role: 'owner', joinDate: new Date('2024-01-01') },
]);
const result = await context.getLeagueRosterUseCase.execute({ leagueId: league.id });
expect(result.members).toHaveLength(1);
expect(result.members[0].role).toBe('owner');
expect(result.stats.adminCount).toBe(1);
});
});

View File

@@ -0,0 +1,36 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
describe('League Settings - Basic Info', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should retrieve league basic information', async () => {
const league = await context.createLeague({
name: 'Test League',
description: 'Test Description',
visibility: 'public',
});
const result = await context.leagueRepository.findById(league.id);
expect(result).toBeDefined();
expect(result?.name).toBe('Test League');
expect(result?.description).toBe('Test Description');
expect(result?.visibility).toBe('public');
});
it('should update league basic information', async () => {
const league = await context.createLeague({ name: 'Old Name' });
await context.leagueRepository.update(league.id, { name: 'New Name', description: 'New Description' });
const updated = await context.leagueRepository.findById(league.id);
expect(updated?.name).toBe('New Name');
expect(updated?.description).toBe('New Description');
});
});

View File

@@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
describe('League Settings - Scoring', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should retrieve league scoring configuration', async () => {
const league = await context.createLeague({
scoringSystem: { points: [10, 8, 6] },
bonusPointsEnabled: true,
penaltiesEnabled: true,
});
const result = await context.leagueRepository.findById(league.id);
expect(result?.scoringSystem).toEqual({ points: [10, 8, 6] });
expect(result?.bonusPointsEnabled).toBe(true);
expect(result?.penaltiesEnabled).toBe(true);
});
it('should update league scoring configuration', async () => {
const league = await context.createLeague({ bonusPointsEnabled: false });
await context.leagueRepository.update(league.id, { bonusPointsEnabled: true, scoringSystem: { points: [25, 18] } });
const updated = await context.leagueRepository.findById(league.id);
expect(updated?.bonusPointsEnabled).toBe(true);
expect(updated?.scoringSystem).toEqual({ points: [25, 18] });
});
});

View File

@@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
describe('League Settings - Stewarding', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should retrieve league stewarding configuration', async () => {
const league = await context.createLeague({
protestsEnabled: true,
appealsEnabled: false,
stewardTeam: ['steward-1'],
});
const result = await context.leagueRepository.findById(league.id);
expect(result?.protestsEnabled).toBe(true);
expect(result?.appealsEnabled).toBe(false);
expect(result?.stewardTeam).toEqual(['steward-1']);
});
it('should update league stewarding configuration', async () => {
const league = await context.createLeague({ protestsEnabled: false });
await context.leagueRepository.update(league.id, { protestsEnabled: true, stewardTeam: ['steward-2'] });
const updated = await context.leagueRepository.findById(league.id);
expect(updated?.protestsEnabled).toBe(true);
expect(updated?.stewardTeam).toEqual(['steward-2']);
});
});

View File

@@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { LeaguesTestContext } from '../LeaguesTestContext';
describe('League Settings - Structure', () => {
let context: LeaguesTestContext;
beforeEach(() => {
context = new LeaguesTestContext();
context.clear();
});
it('should retrieve league structure settings', async () => {
const league = await context.createLeague({
maxDrivers: 30,
approvalRequired: true,
lateJoinAllowed: false,
});
const result = await context.leagueRepository.findById(league.id);
expect(result?.maxDrivers).toBe(30);
expect(result?.approvalRequired).toBe(true);
expect(result?.lateJoinAllowed).toBe(false);
});
it('should update league structure settings', async () => {
const league = await context.createLeague({ maxDrivers: 20 });
await context.leagueRepository.update(league.id, { maxDrivers: 40, approvalRequired: true });
const updated = await context.leagueRepository.findById(league.id);
expect(updated?.maxDrivers).toBe(40);
expect(updated?.approvalRequired).toBe(true);
});
});

View File

@@ -1,170 +0,0 @@
# Media Integration Tests - Implementation Notes
## Overview
This document describes the implementation of integration tests for media functionality in the GridPilot project.
## Implemented Tests
### Avatar Management Integration Tests
**File:** `avatar-management.integration.test.ts`
**Tests Implemented:**
- `GetAvatarUseCase` - Success Path
- Retrieves driver avatar when avatar exists
- Returns AVATAR_NOT_FOUND when driver has no avatar
- `GetAvatarUseCase` - Error Handling
- Handles repository errors gracefully
- `UpdateAvatarUseCase` - Success Path
- Updates existing avatar for a driver
- Updates avatar when driver has no existing avatar
- `UpdateAvatarUseCase` - Error Handling
- Handles repository errors gracefully
- `RequestAvatarGenerationUseCase` - Success Path
- Requests avatar generation from photo
- Requests avatar generation with default style
- `RequestAvatarGenerationUseCase` - Validation
- Rejects generation with invalid face photo
- `SelectAvatarUseCase` - Success Path
- Selects a generated avatar
- `SelectAvatarUseCase` - Error Handling
- Rejects selection when request does not exist
- Rejects selection when request is not completed
- `GetUploadedMediaUseCase` - Success Path
- Retrieves uploaded media
- Returns null when media does not exist
- `DeleteMediaUseCase` - Success Path
- Deletes media file
- `DeleteMediaUseCase` - Error Handling
- Returns MEDIA_NOT_FOUND when media does not exist
**Use Cases Tested:**
- `GetAvatarUseCase` - Retrieves driver avatar
- `UpdateAvatarUseCase` - Updates an existing avatar for a driver
- `RequestAvatarGenerationUseCase` - Requests avatar generation from a photo
- `SelectAvatarUseCase` - Selects a generated avatar
- `GetUploadedMediaUseCase` - Retrieves uploaded media
- `DeleteMediaUseCase` - Deletes media files
**In-Memory Adapters Created:**
- `InMemoryAvatarRepository` - Stores avatar entities in memory
- `InMemoryAvatarGenerationRepository` - Stores avatar generation requests in memory
- `InMemoryMediaRepository` - Stores media entities in memory
- `InMemoryMediaStorageAdapter` - Simulates file storage in memory
- `InMemoryFaceValidationAdapter` - Simulates face validation in memory
- `InMemoryImageServiceAdapter` - Simulates image service in memory
- `InMemoryMediaEventPublisher` - Stores domain events in memory
## Placeholder Tests
The following test files remain as placeholders because they reference domains that are not part of the core/media directory:
### Category Icon Management
**File:** `category-icon-management.integration.test.ts`
**Status:** Placeholder - Not implemented
**Reason:** Category icon management would be part of the `core/categories` domain, not `core/media`. The test placeholders reference use cases like `GetCategoryIconsUseCase`, `UploadCategoryIconUseCase`, etc., which would be implemented in the categories domain.
### League Media Management
**File:** `league-media-management.integration.test.ts`
**Status:** Placeholder - Not implemented
**Reason:** League media management would be part of the `core/leagues` domain, not `core/media`. The test placeholders reference use cases like `GetLeagueMediaUseCase`, `UploadLeagueCoverUseCase`, etc., which would be implemented in the leagues domain.
### Sponsor Logo Management
**File:** `sponsor-logo-management.integration.test.ts`
**Status:** Placeholder - Not implemented
**Reason:** Sponsor logo management would be part of the `core/sponsors` domain, not `core/media`. The test placeholders reference use cases like `GetSponsorLogosUseCase`, `UploadSponsorLogoUseCase`, etc., which would be implemented in the sponsors domain.
### Team Logo Management
**File:** `team-logo-management.integration.test.ts`
**Status:** Placeholder - Not implemented
**Reason:** Team logo management would be part of the `core/teams` domain, not `core/media`. The test placeholders reference use cases like `GetTeamLogosUseCase`, `UploadTeamLogoUseCase`, etc., which would be implemented in the teams domain.
### Track Image Management
**File:** `track-image-management.integration.test.ts`
**Status:** Placeholder - Not implemented
**Reason:** Track image management would be part of the `core/tracks` domain, not `core/media`. The test placeholders reference use cases like `GetTrackImagesUseCase`, `UploadTrackImageUseCase`, etc., which would be implemented in the tracks domain.
## Architecture Compliance
### Core Layer (Business Logic)
**Compliant:** All tests focus on Core Use Cases only
- Tests use In-Memory adapters for repositories and event publishers
- Tests follow Given/When/Then pattern for business logic scenarios
- Tests verify Use Case orchestration (interaction between Use Cases and their Ports)
- Tests do NOT test HTTP endpoints, DTOs, or Presenters
### Adapters Layer (Infrastructure)
**Compliant:** In-Memory adapters created for testing
- `InMemoryAvatarRepository` implements `AvatarRepository` port
- `InMemoryMediaRepository` implements `MediaRepository` port
- `InMemoryMediaStorageAdapter` implements `MediaStoragePort` port
- `InMemoryFaceValidationAdapter` implements `FaceValidationPort` port
- `InMemoryImageServiceAdapter` implements `ImageServicePort` port
- `InMemoryMediaEventPublisher` stores domain events for verification
### Test Framework
**Compliant:** Using Vitest as specified
- All tests use Vitest's `describe`, `it`, `expect`, `beforeAll`, `beforeEach`
- Tests are asynchronous and use `async/await`
- Tests verify both success paths and error handling
## Observations
### Media Implementation Structure
The core/media directory contains:
- **Domain Layer:** Entities (Avatar, Media, AvatarGenerationRequest), Value Objects (AvatarId, MediaUrl), Repositories (AvatarRepository, MediaRepository, AvatarGenerationRepository)
- **Application Layer:** Use Cases (GetAvatarUseCase, UpdateAvatarUseCase, RequestAvatarGenerationUseCase, SelectAvatarUseCase, GetUploadedMediaUseCase, DeleteMediaUseCase), Ports (MediaStoragePort, AvatarGenerationPort, FaceValidationPort, ImageServicePort)
### Missing Use Cases
The placeholder tests reference use cases that don't exist in the core/media directory:
- `UploadAvatarUseCase` - Not found (likely part of a different domain)
- `DeleteAvatarUseCase` - Not found (likely part of a different domain)
- `GenerateAvatarFromPhotoUseCase` - Not found (replaced by `RequestAvatarGenerationUseCase` + `SelectAvatarUseCase`)
### Domain Boundaries
The media functionality is split across multiple domains:
- **core/media:** Avatar management and general media management
- **core/categories:** Category icon management (not implemented)
- **core/leagues:** League media management (not implemented)
- **core/sponsors:** Sponsor logo management (not implemented)
- **core/teams:** Team logo management (not implemented)
- **core/tracks:** Track image management (not implemented)
Each domain would have its own media-related use cases and repositories, following the same pattern as the core/media domain.
## Recommendations
1. **For categories, leagues, sponsors, teams, and tracks domains:**
- Create similar integration tests in their respective test directories
- Follow the same pattern as avatar-management.integration.test.ts
- Use In-Memory adapters for repositories and event publishers
- Test Use Case orchestration only, not HTTP endpoints
2. **For missing use cases:**
- If `UploadAvatarUseCase` and `DeleteAvatarUseCase` are needed, they should be implemented in the appropriate domain
- The current implementation uses `UpdateAvatarUseCase` and `DeleteMediaUseCase` instead
3. **For event publishing:**
- The current implementation uses `InMemoryMediaEventPublisher` for testing
- In production, a real event publisher would be used
- Events should be published for all significant state changes (avatar uploaded, avatar updated, media deleted, etc.)
## Conclusion
The integration tests for avatar management have been successfully implemented following the architecture requirements:
- ✅ Tests Core Use Cases directly
- ✅ Use In-Memory adapters for repositories and event publishers
- ✅ Test Use Case orchestration (interaction between Use Cases and their Ports)
- ✅ Follow Given/When/Then pattern for business logic scenarios
- ✅ Do NOT test HTTP endpoints, DTOs, or Presenters
The placeholder tests for category, league, sponsor, team, and track media management remain as placeholders because they belong to different domains and would need to be implemented in their respective test directories.

View File

@@ -0,0 +1,73 @@
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { InMemoryAvatarRepository } from '@adapters/media/persistence/inmemory/InMemoryAvatarRepository';
import { InMemoryAvatarGenerationRepository } from '@adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
import { InMemoryMediaRepository } from '@adapters/media/persistence/inmemory/InMemoryMediaRepository';
import { InMemoryMediaStorageAdapter } from '@adapters/media/ports/InMemoryMediaStorageAdapter';
import { InMemoryFaceValidationAdapter } from '@adapters/media/ports/InMemoryFaceValidationAdapter';
import { InMemoryAvatarGenerationAdapter } from '@adapters/media/ports/InMemoryAvatarGenerationAdapter';
import { InMemoryMediaEventPublisher } from '@adapters/media/events/InMemoryMediaEventPublisher';
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import { SelectAvatarUseCase } from '@core/media/application/use-cases/SelectAvatarUseCase';
import { GetUploadedMediaUseCase } from '@core/media/application/use-cases/GetUploadedMediaUseCase';
import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase';
import { UploadMediaUseCase } from '@core/media/application/use-cases/UploadMediaUseCase';
import { GetMediaUseCase } from '@core/media/application/use-cases/GetMediaUseCase';
export class MediaTestContext {
public readonly logger: ConsoleLogger;
public readonly avatarRepository: InMemoryAvatarRepository;
public readonly avatarGenerationRepository: InMemoryAvatarGenerationRepository;
public readonly mediaRepository: InMemoryMediaRepository;
public readonly mediaStorage: InMemoryMediaStorageAdapter;
public readonly faceValidation: InMemoryFaceValidationAdapter;
public readonly avatarGeneration: InMemoryAvatarGenerationAdapter;
public readonly eventPublisher: InMemoryMediaEventPublisher;
public readonly getAvatarUseCase: GetAvatarUseCase;
public readonly updateAvatarUseCase: UpdateAvatarUseCase;
public readonly requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase;
public readonly selectAvatarUseCase: SelectAvatarUseCase;
public readonly getUploadedMediaUseCase: GetUploadedMediaUseCase;
public readonly deleteMediaUseCase: DeleteMediaUseCase;
public readonly uploadMediaUseCase: UploadMediaUseCase;
public readonly getMediaUseCase: GetMediaUseCase;
private constructor() {
this.logger = new ConsoleLogger();
this.avatarRepository = new InMemoryAvatarRepository(this.logger);
this.avatarGenerationRepository = new InMemoryAvatarGenerationRepository(this.logger);
this.mediaRepository = new InMemoryMediaRepository(this.logger);
this.mediaStorage = new InMemoryMediaStorageAdapter(this.logger);
this.faceValidation = new InMemoryFaceValidationAdapter(this.logger);
this.avatarGeneration = new InMemoryAvatarGenerationAdapter(this.logger);
this.eventPublisher = new InMemoryMediaEventPublisher(this.logger);
this.getAvatarUseCase = new GetAvatarUseCase(this.avatarRepository, this.logger);
this.updateAvatarUseCase = new UpdateAvatarUseCase(this.avatarRepository, this.logger);
this.requestAvatarGenerationUseCase = new RequestAvatarGenerationUseCase(
this.avatarGenerationRepository,
this.faceValidation,
this.avatarGeneration,
this.logger
);
this.selectAvatarUseCase = new SelectAvatarUseCase(this.avatarGenerationRepository, this.logger);
this.getUploadedMediaUseCase = new GetUploadedMediaUseCase(this.mediaStorage);
this.deleteMediaUseCase = new DeleteMediaUseCase(this.mediaRepository, this.mediaStorage, this.logger);
this.uploadMediaUseCase = new UploadMediaUseCase(this.mediaRepository, this.mediaStorage, this.logger);
this.getMediaUseCase = new GetMediaUseCase(this.mediaRepository, this.logger);
}
public static create(): MediaTestContext {
return new MediaTestContext();
}
public reset(): void {
this.avatarRepository.clear();
this.avatarGenerationRepository.clear();
this.mediaRepository.clear();
this.mediaStorage.clear();
this.eventPublisher.clear();
}
}

View File

@@ -1,478 +0,0 @@
/**
* Integration Test: Avatar Management Use Case Orchestration
*
* Tests the orchestration logic of avatar-related Use Cases:
* - GetAvatarUseCase: Retrieves driver avatar
* - UpdateAvatarUseCase: Updates an existing avatar for a driver
* - RequestAvatarGenerationUseCase: Requests avatar generation from a photo
* - SelectAvatarUseCase: Selects a generated avatar
* - GetUploadedMediaUseCase: Retrieves uploaded media
* - DeleteMediaUseCase: Deletes media files
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { ConsoleLogger } from '@core/shared/logging/ConsoleLogger';
import { InMemoryAvatarRepository } from '@adapters/media/persistence/inmemory/InMemoryAvatarRepository';
import { InMemoryAvatarGenerationRepository } from '@adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
import { InMemoryMediaRepository } from '@adapters/media/persistence/inmemory/InMemoryMediaRepository';
import { InMemoryMediaStorageAdapter } from '@adapters/media/ports/InMemoryMediaStorageAdapter';
import { InMemoryFaceValidationAdapter } from '@adapters/media/ports/InMemoryFaceValidationAdapter';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { InMemoryMediaEventPublisher } from '@adapters/media/events/InMemoryMediaEventPublisher';
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import { SelectAvatarUseCase } from '@core/media/application/use-cases/SelectAvatarUseCase';
import { GetUploadedMediaUseCase } from '@core/media/application/use-cases/GetUploadedMediaUseCase';
import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase';
import { Avatar } from '@core/media/domain/entities/Avatar';
import { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
import { Media } from '@core/media/domain/entities/Media';
describe('Avatar Management Use Case Orchestration', () => {
let avatarRepository: InMemoryAvatarRepository;
let avatarGenerationRepository: InMemoryAvatarGenerationRepository;
let mediaRepository: InMemoryMediaRepository;
let mediaStorage: InMemoryMediaStorageAdapter;
let faceValidation: InMemoryFaceValidationAdapter;
let imageService: InMemoryImageServiceAdapter;
let eventPublisher: InMemoryMediaEventPublisher;
let logger: ConsoleLogger;
let getAvatarUseCase: GetAvatarUseCase;
let updateAvatarUseCase: UpdateAvatarUseCase;
let requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase;
let selectAvatarUseCase: SelectAvatarUseCase;
let getUploadedMediaUseCase: GetUploadedMediaUseCase;
let deleteMediaUseCase: DeleteMediaUseCase;
beforeAll(() => {
logger = new ConsoleLogger();
avatarRepository = new InMemoryAvatarRepository(logger);
avatarGenerationRepository = new InMemoryAvatarGenerationRepository(logger);
mediaRepository = new InMemoryMediaRepository(logger);
mediaStorage = new InMemoryMediaStorageAdapter(logger);
faceValidation = new InMemoryFaceValidationAdapter(logger);
imageService = new InMemoryImageServiceAdapter(logger);
eventPublisher = new InMemoryMediaEventPublisher(logger);
getAvatarUseCase = new GetAvatarUseCase(avatarRepository, logger);
updateAvatarUseCase = new UpdateAvatarUseCase(avatarRepository, logger);
requestAvatarGenerationUseCase = new RequestAvatarGenerationUseCase(
avatarGenerationRepository,
faceValidation,
imageService,
logger
);
selectAvatarUseCase = new SelectAvatarUseCase(avatarGenerationRepository, logger);
getUploadedMediaUseCase = new GetUploadedMediaUseCase(mediaStorage);
deleteMediaUseCase = new DeleteMediaUseCase(mediaRepository, mediaStorage, logger);
});
beforeEach(() => {
avatarRepository.clear();
avatarGenerationRepository.clear();
mediaRepository.clear();
mediaStorage.clear();
eventPublisher.clear();
});
describe('GetAvatarUseCase - Success Path', () => {
it('should retrieve driver avatar when avatar exists', async () => {
// Scenario: Driver with existing avatar
// Given: A driver exists with an avatar
const avatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
});
await avatarRepository.save(avatar);
// When: GetAvatarUseCase.execute() is called with driver ID
const result = await getAvatarUseCase.execute({ driverId: 'driver-1' });
// Then: The result should contain the avatar data
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.avatar.id).toBe('avatar-1');
expect(successResult.avatar.driverId).toBe('driver-1');
expect(successResult.avatar.mediaUrl).toBe('https://example.com/avatar.png');
expect(successResult.avatar.selectedAt).toBeInstanceOf(Date);
});
it('should return AVATAR_NOT_FOUND when driver has no avatar', async () => {
// Scenario: Driver without avatar
// Given: A driver exists without an avatar
// When: GetAvatarUseCase.execute() is called with driver ID
const result = await getAvatarUseCase.execute({ driverId: 'driver-1' });
// Then: Should return AVATAR_NOT_FOUND error
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.code).toBe('AVATAR_NOT_FOUND');
expect(err.details.message).toBe('Avatar not found');
});
});
describe('GetAvatarUseCase - Error Handling', () => {
it('should handle repository errors gracefully', async () => {
// Scenario: Repository error
// Given: AvatarRepository throws an error
const originalFind = avatarRepository.findActiveByDriverId;
avatarRepository.findActiveByDriverId = async () => {
throw new Error('Database connection error');
};
// When: GetAvatarUseCase.execute() is called
const result = await getAvatarUseCase.execute({ driverId: 'driver-1' });
// Then: Should return REPOSITORY_ERROR
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.code).toBe('REPOSITORY_ERROR');
expect(err.details.message).toContain('Database connection error');
// Restore original method
avatarRepository.findActiveByDriverId = originalFind;
});
});
describe('UpdateAvatarUseCase - Success Path', () => {
it('should update existing avatar for a driver', async () => {
// Scenario: Driver updates existing avatar
// Given: A driver exists with an existing avatar
const existingAvatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/old-avatar.png',
});
await avatarRepository.save(existingAvatar);
// When: UpdateAvatarUseCase.execute() is called with driver ID and new image data
const result = await updateAvatarUseCase.execute({
driverId: 'driver-1',
mediaUrl: 'https://example.com/new-avatar.png',
});
// Then: The old avatar should be deactivated and new one created
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.avatarId).toBeDefined();
expect(successResult.driverId).toBe('driver-1');
// Verify old avatar is deactivated
const oldAvatar = await avatarRepository.findById('avatar-1');
expect(oldAvatar?.isActive).toBe(false);
// Verify new avatar exists
const newAvatar = await avatarRepository.findActiveByDriverId('driver-1');
expect(newAvatar).not.toBeNull();
expect(newAvatar?.mediaUrl.value).toBe('https://example.com/new-avatar.png');
});
it('should update avatar when driver has no existing avatar', async () => {
// Scenario: Driver updates avatar when no avatar exists
// Given: A driver exists without an avatar
// When: UpdateAvatarUseCase.execute() is called
const result = await updateAvatarUseCase.execute({
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
});
// Then: A new avatar should be created
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.avatarId).toBeDefined();
expect(successResult.driverId).toBe('driver-1');
// Verify new avatar exists
const newAvatar = await avatarRepository.findActiveByDriverId('driver-1');
expect(newAvatar).not.toBeNull();
expect(newAvatar?.mediaUrl.value).toBe('https://example.com/avatar.png');
});
});
describe('UpdateAvatarUseCase - Error Handling', () => {
it('should handle repository errors gracefully', async () => {
// Scenario: Repository error
// Given: AvatarRepository throws an error
const originalSave = avatarRepository.save;
avatarRepository.save = async () => {
throw new Error('Database connection error');
};
// When: UpdateAvatarUseCase.execute() is called
const result = await updateAvatarUseCase.execute({
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
});
// Then: Should return REPOSITORY_ERROR
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.code).toBe('REPOSITORY_ERROR');
expect(err.details.message).toContain('Database connection error');
// Restore original method
avatarRepository.save = originalSave;
});
});
describe('RequestAvatarGenerationUseCase - Success Path', () => {
it('should request avatar generation from photo', async () => {
// Scenario: Driver requests avatar generation from photo
// Given: A driver exists
// And: Valid photo data is provided
// When: RequestAvatarGenerationUseCase.execute() is called with driver ID and photo data
const result = await requestAvatarGenerationUseCase.execute({
userId: 'user-1',
facePhotoData: 'https://example.com/face-photo.jpg',
suitColor: 'red',
style: 'realistic',
});
// Then: An avatar generation request should be created
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.requestId).toBeDefined();
expect(successResult.status).toBe('completed');
expect(successResult.avatarUrls).toBeDefined();
expect(successResult.avatarUrls?.length).toBeGreaterThan(0);
// Verify request was saved
const request = await avatarGenerationRepository.findById(successResult.requestId);
expect(request).not.toBeNull();
expect(request?.status).toBe('completed');
});
it('should request avatar generation with default style', async () => {
// Scenario: Driver requests avatar generation with default style
// Given: A driver exists
// When: RequestAvatarGenerationUseCase.execute() is called without style
const result = await requestAvatarGenerationUseCase.execute({
userId: 'user-1',
facePhotoData: 'https://example.com/face-photo.jpg',
suitColor: 'blue',
});
// Then: An avatar generation request should be created with default style
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.requestId).toBeDefined();
expect(successResult.status).toBe('completed');
});
});
describe('RequestAvatarGenerationUseCase - Validation', () => {
it('should reject generation with invalid face photo', async () => {
// Scenario: Invalid face photo
// Given: A driver exists
// And: Face validation fails
const originalValidate = faceValidation.validateFacePhoto;
faceValidation.validateFacePhoto = async () => ({
isValid: false,
hasFace: false,
faceCount: 0,
confidence: 0.0,
errorMessage: 'No face detected',
});
// When: RequestAvatarGenerationUseCase.execute() is called
const result = await requestAvatarGenerationUseCase.execute({
userId: 'user-1',
facePhotoData: 'https://example.com/invalid-photo.jpg',
suitColor: 'red',
});
// Then: Should return FACE_VALIDATION_FAILED error
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.code).toBe('FACE_VALIDATION_FAILED');
expect(err.details.message).toContain('No face detected');
// Restore original method
faceValidation.validateFacePhoto = originalValidate;
});
});
describe('SelectAvatarUseCase - Success Path', () => {
it('should select a generated avatar', async () => {
// Scenario: Driver selects a generated avatar
// Given: A completed avatar generation request exists
const request = AvatarGenerationRequest.create({
id: 'request-1',
userId: 'user-1',
facePhotoUrl: 'https://example.com/face-photo.jpg',
suitColor: 'red',
style: 'realistic',
});
request.completeWithAvatars([
'https://example.com/avatar-1.png',
'https://example.com/avatar-2.png',
'https://example.com/avatar-3.png',
]);
await avatarGenerationRepository.save(request);
// When: SelectAvatarUseCase.execute() is called with request ID and selected index
const result = await selectAvatarUseCase.execute({
requestId: 'request-1',
selectedIndex: 1,
});
// Then: The avatar should be selected
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.requestId).toBe('request-1');
expect(successResult.selectedAvatarUrl).toBe('https://example.com/avatar-2.png');
// Verify request was updated
const updatedRequest = await avatarGenerationRepository.findById('request-1');
expect(updatedRequest?.selectedAvatarUrl).toBe('https://example.com/avatar-2.png');
});
});
describe('SelectAvatarUseCase - Error Handling', () => {
it('should reject selection when request does not exist', async () => {
// Scenario: Request does not exist
// Given: No request exists with the given ID
// When: SelectAvatarUseCase.execute() is called
const result = await selectAvatarUseCase.execute({
requestId: 'non-existent-request',
selectedIndex: 0,
});
// Then: Should return REQUEST_NOT_FOUND error
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.code).toBe('REQUEST_NOT_FOUND');
});
it('should reject selection when request is not completed', async () => {
// Scenario: Request is not completed
// Given: An incomplete avatar generation request exists
const request = AvatarGenerationRequest.create({
id: 'request-1',
userId: 'user-1',
facePhotoUrl: 'https://example.com/face-photo.jpg',
suitColor: 'red',
style: 'realistic',
});
await avatarGenerationRepository.save(request);
// When: SelectAvatarUseCase.execute() is called
const result = await selectAvatarUseCase.execute({
requestId: 'request-1',
selectedIndex: 0,
});
// Then: Should return REQUEST_NOT_COMPLETED error
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.code).toBe('REQUEST_NOT_COMPLETED');
});
});
describe('GetUploadedMediaUseCase - Success Path', () => {
it('should retrieve uploaded media', async () => {
// Scenario: Retrieve uploaded media
// Given: Media has been uploaded
const uploadResult = await mediaStorage.uploadMedia(
Buffer.from('test media content'),
{
filename: 'test-avatar.png',
mimeType: 'image/png',
}
);
expect(uploadResult.success).toBe(true);
const storageKey = uploadResult.url!;
// When: GetUploadedMediaUseCase.execute() is called
const result = await getUploadedMediaUseCase.execute({ storageKey });
// Then: The media should be retrieved
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult).not.toBeNull();
expect(successResult?.bytes).toBeInstanceOf(Buffer);
expect(successResult?.contentType).toBe('image/png');
});
it('should return null when media does not exist', async () => {
// Scenario: Media does not exist
// Given: No media exists with the given storage key
// When: GetUploadedMediaUseCase.execute() is called
const result = await getUploadedMediaUseCase.execute({ storageKey: 'non-existent-key' });
// Then: Should return null
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult).toBeNull();
});
});
describe('DeleteMediaUseCase - Success Path', () => {
it('should delete media file', async () => {
// Scenario: Delete media file
// Given: Media has been uploaded
const uploadResult = await mediaStorage.uploadMedia(
Buffer.from('test media content'),
{
filename: 'test-avatar.png',
mimeType: 'image/png',
}
);
expect(uploadResult.success).toBe(true);
const storageKey = uploadResult.url!;
// Create media entity
const media = Media.create({
id: 'media-1',
filename: 'test-avatar.png',
originalName: 'test-avatar.png',
mimeType: 'image/png',
size: 18,
url: storageKey,
type: 'image',
uploadedBy: 'user-1',
});
await mediaRepository.save(media);
// When: DeleteMediaUseCase.execute() is called
const result = await deleteMediaUseCase.execute({ mediaId: 'media-1' });
// Then: The media should be deleted
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.mediaId).toBe('media-1');
expect(successResult.deleted).toBe(true);
// Verify media is deleted from repository
const deletedMedia = await mediaRepository.findById('media-1');
expect(deletedMedia).toBeNull();
// Verify media is deleted from storage
const storageExists = mediaStorage.has(storageKey);
expect(storageExists).toBe(false);
});
});
describe('DeleteMediaUseCase - Error Handling', () => {
it('should return MEDIA_NOT_FOUND when media does not exist', async () => {
// Scenario: Media does not exist
// Given: No media exists with the given ID
// When: DeleteMediaUseCase.execute() is called
const result = await deleteMediaUseCase.execute({ mediaId: 'non-existent-media' });
// Then: Should return MEDIA_NOT_FOUND error
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.code).toBe('MEDIA_NOT_FOUND');
});
});
});

View File

@@ -0,0 +1,114 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { MediaTestContext } from '../MediaTestContext';
import { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
describe('Avatar Management: Generation and Selection', () => {
let ctx: MediaTestContext;
beforeEach(() => {
ctx = MediaTestContext.create();
ctx.reset();
});
describe('RequestAvatarGenerationUseCase', () => {
it('should request avatar generation from photo', async () => {
const result = await ctx.requestAvatarGenerationUseCase.execute({
userId: 'user-1',
facePhotoData: 'https://example.com/face-photo.jpg',
suitColor: 'red',
style: 'realistic',
});
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.requestId).toBeDefined();
expect(successResult.status).toBe('completed');
expect(successResult.avatarUrls).toHaveLength(3);
const request = await ctx.avatarGenerationRepository.findById(successResult.requestId);
expect(request).not.toBeNull();
expect(request?.status).toBe('completed');
});
it('should reject generation with invalid face photo', async () => {
const originalValidate = ctx.faceValidation.validateFacePhoto;
ctx.faceValidation.validateFacePhoto = async () => ({
isValid: false,
hasFace: false,
faceCount: 0,
confidence: 0.0,
errorMessage: 'No face detected',
});
const result = await ctx.requestAvatarGenerationUseCase.execute({
userId: 'user-1',
facePhotoData: 'https://example.com/invalid-photo.jpg',
suitColor: 'red',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('FACE_VALIDATION_FAILED');
ctx.faceValidation.validateFacePhoto = originalValidate;
});
});
describe('SelectAvatarUseCase', () => {
it('should select a generated avatar', async () => {
const request = AvatarGenerationRequest.create({
id: 'request-1',
userId: 'user-1',
facePhotoUrl: 'https://example.com/face-photo.jpg',
suitColor: 'red',
style: 'realistic',
});
request.completeWithAvatars([
'https://example.com/avatar-1.png',
'https://example.com/avatar-2.png',
'https://example.com/avatar-3.png',
]);
await ctx.avatarGenerationRepository.save(request);
const result = await ctx.selectAvatarUseCase.execute({
requestId: 'request-1',
selectedIndex: 1,
});
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.selectedAvatarUrl).toBe('https://example.com/avatar-2.png');
const updatedRequest = await ctx.avatarGenerationRepository.findById('request-1');
expect(updatedRequest?.selectedAvatarUrl).toBe('https://example.com/avatar-2.png');
});
it('should reject selection when request does not exist', async () => {
const result = await ctx.selectAvatarUseCase.execute({
requestId: 'non-existent-request',
selectedIndex: 0,
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('REQUEST_NOT_FOUND');
});
it('should reject selection when request is not completed', async () => {
const request = AvatarGenerationRequest.create({
id: 'request-1',
userId: 'user-1',
facePhotoUrl: 'https://example.com/face-photo.jpg',
suitColor: 'red',
style: 'realistic',
});
await ctx.avatarGenerationRepository.save(request);
const result = await ctx.selectAvatarUseCase.execute({
requestId: 'request-1',
selectedIndex: 0,
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('REQUEST_NOT_COMPLETED');
});
});
});

View File

@@ -0,0 +1,89 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { MediaTestContext } from '../MediaTestContext';
import { Avatar } from '@core/media/domain/entities/Avatar';
describe('Avatar Management: Retrieval and Updates', () => {
let ctx: MediaTestContext;
beforeEach(() => {
ctx = MediaTestContext.create();
ctx.reset();
});
describe('GetAvatarUseCase', () => {
it('should retrieve driver avatar when avatar exists', async () => {
const avatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
});
await ctx.avatarRepository.save(avatar);
const result = await ctx.getAvatarUseCase.execute({ driverId: 'driver-1' });
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.avatar.id).toBe('avatar-1');
expect(successResult.avatar.driverId).toBe('driver-1');
expect(successResult.avatar.mediaUrl).toBe('https://example.com/avatar.png');
});
it('should return AVATAR_NOT_FOUND when driver has no avatar', async () => {
const result = await ctx.getAvatarUseCase.execute({ driverId: 'driver-1' });
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('AVATAR_NOT_FOUND');
});
it('should handle repository errors gracefully', async () => {
const originalFind = ctx.avatarRepository.findActiveByDriverId;
ctx.avatarRepository.findActiveByDriverId = async () => {
throw new Error('Database connection error');
};
const result = await ctx.getAvatarUseCase.execute({ driverId: 'driver-1' });
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
ctx.avatarRepository.findActiveByDriverId = originalFind;
});
});
describe('UpdateAvatarUseCase', () => {
it('should update existing avatar for a driver', async () => {
const existingAvatar = Avatar.create({
id: 'avatar-1',
driverId: 'driver-1',
mediaUrl: 'https://example.com/old-avatar.png',
});
await ctx.avatarRepository.save(existingAvatar);
const result = await ctx.updateAvatarUseCase.execute({
driverId: 'driver-1',
mediaUrl: 'https://example.com/new-avatar.png',
});
expect(result.isOk()).toBe(true);
const oldAvatar = await ctx.avatarRepository.findById('avatar-1');
expect(oldAvatar?.isActive).toBe(false);
const newAvatar = await ctx.avatarRepository.findActiveByDriverId('driver-1');
expect(newAvatar).not.toBeNull();
expect(newAvatar?.mediaUrl.value).toBe('https://example.com/new-avatar.png');
});
it('should update avatar when driver has no existing avatar', async () => {
const result = await ctx.updateAvatarUseCase.execute({
driverId: 'driver-1',
mediaUrl: 'https://example.com/avatar.png',
});
expect(result.isOk()).toBe(true);
const newAvatar = await ctx.avatarRepository.findActiveByDriverId('driver-1');
expect(newAvatar).not.toBeNull();
expect(newAvatar?.mediaUrl.value).toBe('https://example.com/avatar.png');
});
});
});

View File

@@ -0,0 +1,41 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { MediaTestContext } from '../MediaTestContext';
describe('Category Icon Management', () => {
let ctx: MediaTestContext;
beforeEach(() => {
ctx = MediaTestContext.create();
ctx.reset();
});
it('should upload and retrieve a category icon', async () => {
// When: An icon is uploaded
const uploadResult = await ctx.mediaStorage.uploadMedia(
Buffer.from('icon content'),
{ filename: 'icon.png', mimeType: 'image/png' }
);
expect(uploadResult.success).toBe(true);
const storageKey = uploadResult.url!;
// Then: The icon should be retrievable from storage
const retrieved = await ctx.getUploadedMediaUseCase.execute({ storageKey });
expect(retrieved.isOk()).toBe(true);
expect(retrieved.unwrap()?.contentType).toBe('image/png');
});
it('should handle multiple category icons', async () => {
const upload1 = await ctx.mediaStorage.uploadMedia(
Buffer.from('icon 1'),
{ filename: 'icon1.png', mimeType: 'image/png' }
);
const upload2 = await ctx.mediaStorage.uploadMedia(
Buffer.from('icon 2'),
{ filename: 'icon2.png', mimeType: 'image/png' }
);
expect(upload1.success).toBe(true);
expect(upload2.success).toBe(true);
expect(ctx.mediaStorage.size).toBe(2);
});
});

View File

@@ -1,313 +0,0 @@
/**
* Integration Test: Category Icon Management Use Case Orchestration
*
* Tests the orchestration logic of category icon-related Use Cases:
* - GetCategoryIconsUseCase: Retrieves category icons
* - UploadCategoryIconUseCase: Uploads a new category icon
* - UpdateCategoryIconUseCase: Updates an existing category icon
* - DeleteCategoryIconUseCase: Deletes a category icon
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
describe('Category Icon Management Use Case Orchestration', () => {
// TODO: Initialize In-Memory repositories and event publisher
// let categoryIconRepository: InMemoryCategoryIconRepository;
// let categoryRepository: InMemoryCategoryRepository;
// let eventPublisher: InMemoryEventPublisher;
// let getCategoryIconsUseCase: GetCategoryIconsUseCase;
// let uploadCategoryIconUseCase: UploadCategoryIconUseCase;
// let updateCategoryIconUseCase: UpdateCategoryIconUseCase;
// let deleteCategoryIconUseCase: DeleteCategoryIconUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// categoryIconRepository = new InMemoryCategoryIconRepository();
// categoryRepository = new InMemoryCategoryRepository();
// eventPublisher = new InMemoryEventPublisher();
// getCategoryIconsUseCase = new GetCategoryIconsUseCase({
// categoryIconRepository,
// categoryRepository,
// eventPublisher,
// });
// uploadCategoryIconUseCase = new UploadCategoryIconUseCase({
// categoryIconRepository,
// categoryRepository,
// eventPublisher,
// });
// updateCategoryIconUseCase = new UpdateCategoryIconUseCase({
// categoryIconRepository,
// categoryRepository,
// eventPublisher,
// });
// deleteCategoryIconUseCase = new DeleteCategoryIconUseCase({
// categoryIconRepository,
// categoryRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// categoryIconRepository.clear();
// categoryRepository.clear();
// eventPublisher.clear();
});
describe('GetCategoryIconsUseCase - Success Path', () => {
it('should retrieve all category icons', async () => {
// TODO: Implement test
// Scenario: Multiple categories with icons
// Given: Multiple categories exist with icons
// When: GetCategoryIconsUseCase.execute() is called
// Then: The result should contain all category icons
// And: Each icon should have correct metadata
// And: EventPublisher should emit CategoryIconsRetrievedEvent
});
it('should retrieve category icons for specific category type', async () => {
// TODO: Implement test
// Scenario: Filter by category type
// Given: Categories exist with different types
// When: GetCategoryIconsUseCase.execute() is called with type filter
// Then: The result should only contain icons for that type
// And: EventPublisher should emit CategoryIconsRetrievedEvent
});
it('should retrieve category icons with search query', async () => {
// TODO: Implement test
// Scenario: Search categories by name
// Given: Categories exist with various names
// When: GetCategoryIconsUseCase.execute() is called with search query
// Then: The result should only contain matching categories
// And: EventPublisher should emit CategoryIconsRetrievedEvent
});
});
describe('GetCategoryIconsUseCase - Edge Cases', () => {
it('should handle empty category list', async () => {
// TODO: Implement test
// Scenario: No categories exist
// Given: No categories exist in the system
// When: GetCategoryIconsUseCase.execute() is called
// Then: The result should be an empty list
// And: EventPublisher should emit CategoryIconsRetrievedEvent
});
it('should handle categories without icons', async () => {
// TODO: Implement test
// Scenario: Categories exist without icons
// Given: Categories exist without icons
// When: GetCategoryIconsUseCase.execute() is called
// Then: The result should show categories with default icons
// And: EventPublisher should emit CategoryIconsRetrievedEvent
});
});
describe('UploadCategoryIconUseCase - Success Path', () => {
it('should upload a new category icon', async () => {
// TODO: Implement test
// Scenario: Admin uploads new category icon
// Given: A category exists without an icon
// And: Valid icon image data is provided
// When: UploadCategoryIconUseCase.execute() is called with category ID and image data
// Then: The icon should be stored in the repository
// And: The icon should have correct metadata (file size, format, upload date)
// And: EventPublisher should emit CategoryIconUploadedEvent
});
it('should upload category icon with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin uploads icon with validation
// Given: A category exists
// And: Icon data meets validation requirements (correct format, size, dimensions)
// When: UploadCategoryIconUseCase.execute() is called
// Then: The icon should be stored successfully
// And: EventPublisher should emit CategoryIconUploadedEvent
});
it('should upload icon for new category creation', async () => {
// TODO: Implement test
// Scenario: Admin creates category with icon
// Given: No category exists
// When: UploadCategoryIconUseCase.execute() is called with new category details and icon
// Then: The category should be created
// And: The icon should be stored
// And: EventPublisher should emit CategoryCreatedEvent and CategoryIconUploadedEvent
});
});
describe('UploadCategoryIconUseCase - Validation', () => {
it('should reject upload with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A category exists
// And: Icon data has invalid format (e.g., .txt, .exe)
// When: UploadCategoryIconUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A category exists
// And: Icon data exceeds maximum file size
// When: UploadCategoryIconUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with invalid dimensions', async () => {
// TODO: Implement test
// Scenario: Invalid image dimensions
// Given: A category exists
// And: Icon data has invalid dimensions (too small or too large)
// When: UploadCategoryIconUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateCategoryIconUseCase - Success Path', () => {
it('should update existing category icon', async () => {
// TODO: Implement test
// Scenario: Admin updates category icon
// Given: A category exists with an existing icon
// And: Valid new icon image data is provided
// When: UpdateCategoryIconUseCase.execute() is called with category ID and new image data
// Then: The old icon should be replaced with the new one
// And: The new icon should have updated metadata
// And: EventPublisher should emit CategoryIconUpdatedEvent
});
it('should update icon with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin updates icon with validation
// Given: A category exists with an existing icon
// And: New icon data meets validation requirements
// When: UpdateCategoryIconUseCase.execute() is called
// Then: The icon should be updated successfully
// And: EventPublisher should emit CategoryIconUpdatedEvent
});
it('should update icon for category with multiple icons', async () => {
// TODO: Implement test
// Scenario: Category with multiple icons
// Given: A category exists with multiple icons
// When: UpdateCategoryIconUseCase.execute() is called
// Then: Only the specified icon should be updated
// And: Other icons should remain unchanged
// And: EventPublisher should emit CategoryIconUpdatedEvent
});
});
describe('UpdateCategoryIconUseCase - Validation', () => {
it('should reject update with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A category exists with an existing icon
// And: New icon data has invalid format
// When: UpdateCategoryIconUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject update with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A category exists with an existing icon
// And: New icon data exceeds maximum file size
// When: UpdateCategoryIconUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('DeleteCategoryIconUseCase - Success Path', () => {
it('should delete category icon', async () => {
// TODO: Implement test
// Scenario: Admin deletes category icon
// Given: A category exists with an existing icon
// When: DeleteCategoryIconUseCase.execute() is called with category ID
// Then: The icon should be removed from the repository
// And: The category should show a default icon
// And: EventPublisher should emit CategoryIconDeletedEvent
});
it('should delete specific icon when category has multiple icons', async () => {
// TODO: Implement test
// Scenario: Category with multiple icons
// Given: A category exists with multiple icons
// When: DeleteCategoryIconUseCase.execute() is called with specific icon ID
// Then: Only that icon should be removed
// And: Other icons should remain
// And: EventPublisher should emit CategoryIconDeletedEvent
});
});
describe('DeleteCategoryIconUseCase - Error Handling', () => {
it('should handle deletion when category has no icon', async () => {
// TODO: Implement test
// Scenario: Category without icon
// Given: A category exists without an icon
// When: DeleteCategoryIconUseCase.execute() is called with category ID
// Then: Should complete successfully (no-op)
// And: EventPublisher should emit CategoryIconDeletedEvent
});
it('should throw error when category does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent category
// Given: No category exists with the given ID
// When: DeleteCategoryIconUseCase.execute() is called with non-existent category ID
// Then: Should throw CategoryNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('Category Icon Data Orchestration', () => {
it('should correctly format category icon metadata', async () => {
// TODO: Implement test
// Scenario: Category icon metadata formatting
// Given: A category exists with an icon
// When: GetCategoryIconsUseCase.execute() is called
// Then: Icon metadata should show:
// - File size: Correctly formatted (e.g., "1.2 MB")
// - File format: Correct format (e.g., "PNG", "SVG")
// - Upload date: Correctly formatted date
});
it('should correctly handle category icon caching', async () => {
// TODO: Implement test
// Scenario: Category icon caching
// Given: Categories exist with icons
// When: GetCategoryIconsUseCase.execute() is called multiple times
// Then: Subsequent calls should return cached data
// And: EventPublisher should emit CategoryIconsRetrievedEvent for each call
});
it('should correctly handle category icon error states', async () => {
// TODO: Implement test
// Scenario: Category icon error handling
// Given: Categories exist
// And: CategoryIconRepository throws an error during retrieval
// When: GetCategoryIconsUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
it('should correctly handle bulk category icon operations', async () => {
// TODO: Implement test
// Scenario: Bulk category icon operations
// Given: Multiple categories exist
// When: Bulk upload or export operations are performed
// Then: All operations should complete successfully
// And: EventPublisher should emit appropriate events for each operation
});
});
});

View File

@@ -0,0 +1,140 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { MediaTestContext } from '../MediaTestContext';
import { Media } from '@core/media/domain/entities/Media';
import type { MulterFile } from '@core/media/application/use-cases/UploadMediaUseCase';
describe('General Media Management: Upload, Retrieval, and Deletion', () => {
let ctx: MediaTestContext;
beforeEach(() => {
ctx = MediaTestContext.create();
ctx.reset();
});
const createMockFile = (filename: string, mimeType: string, content: Buffer): MulterFile => ({
fieldname: 'file',
originalname: filename,
encoding: '7bit',
mimetype: mimeType,
size: content.length,
buffer: content,
stream: null as any,
destination: '',
filename: filename,
path: '',
});
describe('UploadMediaUseCase', () => {
it('should upload media successfully', async () => {
const content = Buffer.from('test content');
const file = createMockFile('test.png', 'image/png', content);
const result = await ctx.uploadMediaUseCase.execute({
file,
uploadedBy: 'user-1',
});
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.mediaId).toBeDefined();
expect(successResult.url).toBeDefined();
const media = await ctx.mediaRepository.findById(successResult.mediaId);
expect(media).not.toBeNull();
expect(media?.filename).toBe('test.png');
});
});
describe('GetMediaUseCase', () => {
it('should retrieve media by ID', async () => {
const media = Media.create({
id: 'media-1',
filename: 'test.png',
originalName: 'test.png',
mimeType: 'image/png',
size: 100,
url: 'https://example.com/test.png',
type: 'image',
uploadedBy: 'user-1',
});
await ctx.mediaRepository.save(media);
const result = await ctx.getMediaUseCase.execute({ mediaId: 'media-1' });
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult.media.id).toBe('media-1');
});
it('should return MEDIA_NOT_FOUND when media does not exist', async () => {
const result = await ctx.getMediaUseCase.execute({ mediaId: 'non-existent' });
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('MEDIA_NOT_FOUND');
});
});
describe('GetUploadedMediaUseCase', () => {
it('should retrieve uploaded media content', async () => {
const uploadResult = await ctx.mediaStorage.uploadMedia(
Buffer.from('test content'),
{ filename: 'test.png', mimeType: 'image/png' }
);
const storageKey = uploadResult.url!;
const result = await ctx.getUploadedMediaUseCase.execute({ storageKey });
expect(result.isOk()).toBe(true);
const successResult = result.unwrap();
expect(successResult?.bytes.toString()).toBe('test content');
expect(successResult?.contentType).toBe('image/png');
});
it('should return null when media does not exist in storage', async () => {
const result = await ctx.getUploadedMediaUseCase.execute({ storageKey: 'non-existent' });
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeNull();
});
});
describe('DeleteMediaUseCase', () => {
it('should delete media file and repository entry', async () => {
const uploadResult = await ctx.mediaStorage.uploadMedia(
Buffer.from('test content'),
{ filename: 'test.png', mimeType: 'image/png' }
);
const storageKey = uploadResult.url!;
const media = Media.create({
id: 'media-1',
filename: 'test.png',
originalName: 'test.png',
mimeType: 'image/png',
size: 12,
url: storageKey,
type: 'image',
uploadedBy: 'user-1',
});
await ctx.mediaRepository.save(media);
const result = await ctx.deleteMediaUseCase.execute({ mediaId: 'media-1' });
expect(result.isOk()).toBe(true);
expect(result.unwrap().deleted).toBe(true);
const deletedMedia = await ctx.mediaRepository.findById('media-1');
expect(deletedMedia).toBeNull();
const storageExists = ctx.mediaStorage.has(storageKey);
expect(storageExists).toBe(false);
});
it('should return MEDIA_NOT_FOUND when media does not exist', async () => {
const result = await ctx.deleteMediaUseCase.execute({ mediaId: 'non-existent' });
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('MEDIA_NOT_FOUND');
});
});
});

View File

@@ -1,530 +0,0 @@
/**
* Integration Test: League Media Management Use Case Orchestration
*
* Tests the orchestration logic of league media-related Use Cases:
* - GetLeagueMediaUseCase: Retrieves league covers and logos
* - UploadLeagueCoverUseCase: Uploads a new league cover
* - UploadLeagueLogoUseCase: Uploads a new league logo
* - UpdateLeagueCoverUseCase: Updates an existing league cover
* - UpdateLeagueLogoUseCase: Updates an existing league logo
* - DeleteLeagueCoverUseCase: Deletes a league cover
* - DeleteLeagueLogoUseCase: Deletes a league logo
* - SetLeagueMediaFeaturedUseCase: Sets league media as featured
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
describe('League Media Management Use Case Orchestration', () => {
// TODO: Initialize In-Memory repositories and event publisher
// let leagueMediaRepository: InMemoryLeagueMediaRepository;
// let leagueRepository: InMemoryLeagueRepository;
// let eventPublisher: InMemoryEventPublisher;
// let getLeagueMediaUseCase: GetLeagueMediaUseCase;
// let uploadLeagueCoverUseCase: UploadLeagueCoverUseCase;
// let uploadLeagueLogoUseCase: UploadLeagueLogoUseCase;
// let updateLeagueCoverUseCase: UpdateLeagueCoverUseCase;
// let updateLeagueLogoUseCase: UpdateLeagueLogoUseCase;
// let deleteLeagueCoverUseCase: DeleteLeagueCoverUseCase;
// let deleteLeagueLogoUseCase: DeleteLeagueLogoUseCase;
// let setLeagueMediaFeaturedUseCase: SetLeagueMediaFeaturedUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// leagueMediaRepository = new InMemoryLeagueMediaRepository();
// leagueRepository = new InMemoryLeagueRepository();
// eventPublisher = new InMemoryEventPublisher();
// getLeagueMediaUseCase = new GetLeagueMediaUseCase({
// leagueMediaRepository,
// leagueRepository,
// eventPublisher,
// });
// uploadLeagueCoverUseCase = new UploadLeagueCoverUseCase({
// leagueMediaRepository,
// leagueRepository,
// eventPublisher,
// });
// uploadLeagueLogoUseCase = new UploadLeagueLogoUseCase({
// leagueMediaRepository,
// leagueRepository,
// eventPublisher,
// });
// updateLeagueCoverUseCase = new UpdateLeagueCoverUseCase({
// leagueMediaRepository,
// leagueRepository,
// eventPublisher,
// });
// updateLeagueLogoUseCase = new UpdateLeagueLogoUseCase({
// leagueMediaRepository,
// leagueRepository,
// eventPublisher,
// });
// deleteLeagueCoverUseCase = new DeleteLeagueCoverUseCase({
// leagueMediaRepository,
// leagueRepository,
// eventPublisher,
// });
// deleteLeagueLogoUseCase = new DeleteLeagueLogoUseCase({
// leagueMediaRepository,
// leagueRepository,
// eventPublisher,
// });
// setLeagueMediaFeaturedUseCase = new SetLeagueMediaFeaturedUseCase({
// leagueMediaRepository,
// leagueRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// leagueMediaRepository.clear();
// leagueRepository.clear();
// eventPublisher.clear();
});
describe('GetLeagueMediaUseCase - Success Path', () => {
it('should retrieve league cover and logo', async () => {
// TODO: Implement test
// Scenario: League with cover and logo
// Given: A league exists with a cover and logo
// When: GetLeagueMediaUseCase.execute() is called with league ID
// Then: The result should contain both cover and logo
// And: Each media should have correct metadata
// And: EventPublisher should emit LeagueMediaRetrievedEvent
});
it('should retrieve league with only cover', async () => {
// TODO: Implement test
// Scenario: League with only cover
// Given: A league exists with only a cover
// When: GetLeagueMediaUseCase.execute() is called with league ID
// Then: The result should contain the cover
// And: Logo should be null or default
// And: EventPublisher should emit LeagueMediaRetrievedEvent
});
it('should retrieve league with only logo', async () => {
// TODO: Implement test
// Scenario: League with only logo
// Given: A league exists with only a logo
// When: GetLeagueMediaUseCase.execute() is called with league ID
// Then: The result should contain the logo
// And: Cover should be null or default
// And: EventPublisher should emit LeagueMediaRetrievedEvent
});
it('should retrieve league with multiple covers', async () => {
// TODO: Implement test
// Scenario: League with multiple covers
// Given: A league exists with multiple covers
// When: GetLeagueMediaUseCase.execute() is called with league ID
// Then: The result should contain all covers
// And: Each cover should have correct metadata
// And: EventPublisher should emit LeagueMediaRetrievedEvent
});
});
describe('GetLeagueMediaUseCase - 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: GetLeagueMediaUseCase.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: GetLeagueMediaUseCase.execute() is called with invalid league ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('UploadLeagueCoverUseCase - Success Path', () => {
it('should upload a new league cover', async () => {
// TODO: Implement test
// Scenario: Admin uploads new league cover
// Given: A league exists without a cover
// And: Valid cover image data is provided
// When: UploadLeagueCoverUseCase.execute() is called with league ID and image data
// Then: The cover should be stored in the repository
// And: The cover should have correct metadata (file size, format, upload date)
// And: EventPublisher should emit LeagueCoverUploadedEvent
});
it('should upload cover with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin uploads cover with validation
// Given: A league exists
// And: Cover data meets validation requirements (correct format, size, dimensions)
// When: UploadLeagueCoverUseCase.execute() is called
// Then: The cover should be stored successfully
// And: EventPublisher should emit LeagueCoverUploadedEvent
});
it('should upload cover for new league creation', async () => {
// TODO: Implement test
// Scenario: Admin creates league with cover
// Given: No league exists
// When: UploadLeagueCoverUseCase.execute() is called with new league details and cover
// Then: The league should be created
// And: The cover should be stored
// And: EventPublisher should emit LeagueCreatedEvent and LeagueCoverUploadedEvent
});
});
describe('UploadLeagueCoverUseCase - Validation', () => {
it('should reject upload with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A league exists
// And: Cover data has invalid format (e.g., .txt, .exe)
// When: UploadLeagueCoverUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A league exists
// And: Cover data exceeds maximum file size
// When: UploadLeagueCoverUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with invalid dimensions', async () => {
// TODO: Implement test
// Scenario: Invalid image dimensions
// Given: A league exists
// And: Cover data has invalid dimensions (too small or too large)
// When: UploadLeagueCoverUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('UploadLeagueLogoUseCase - Success Path', () => {
it('should upload a new league logo', async () => {
// TODO: Implement test
// Scenario: Admin uploads new league logo
// Given: A league exists without a logo
// And: Valid logo image data is provided
// When: UploadLeagueLogoUseCase.execute() is called with league ID and image data
// Then: The logo should be stored in the repository
// And: The logo should have correct metadata (file size, format, upload date)
// And: EventPublisher should emit LeagueLogoUploadedEvent
});
it('should upload logo with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin uploads logo with validation
// Given: A league exists
// And: Logo data meets validation requirements (correct format, size, dimensions)
// When: UploadLeagueLogoUseCase.execute() is called
// Then: The logo should be stored successfully
// And: EventPublisher should emit LeagueLogoUploadedEvent
});
});
describe('UploadLeagueLogoUseCase - Validation', () => {
it('should reject upload with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A league exists
// And: Logo data has invalid format
// When: UploadLeagueLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A league exists
// And: Logo data exceeds maximum file size
// When: UploadLeagueLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateLeagueCoverUseCase - Success Path', () => {
it('should update existing league cover', async () => {
// TODO: Implement test
// Scenario: Admin updates league cover
// Given: A league exists with an existing cover
// And: Valid new cover image data is provided
// When: UpdateLeagueCoverUseCase.execute() is called with league ID and new image data
// Then: The old cover should be replaced with the new one
// And: The new cover should have updated metadata
// And: EventPublisher should emit LeagueCoverUpdatedEvent
});
it('should update cover with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin updates cover with validation
// Given: A league exists with an existing cover
// And: New cover data meets validation requirements
// When: UpdateLeagueCoverUseCase.execute() is called
// Then: The cover should be updated successfully
// And: EventPublisher should emit LeagueCoverUpdatedEvent
});
it('should update cover for league with multiple covers', async () => {
// TODO: Implement test
// Scenario: League with multiple covers
// Given: A league exists with multiple covers
// When: UpdateLeagueCoverUseCase.execute() is called
// Then: Only the specified cover should be updated
// And: Other covers should remain unchanged
// And: EventPublisher should emit LeagueCoverUpdatedEvent
});
});
describe('UpdateLeagueCoverUseCase - Validation', () => {
it('should reject update with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A league exists with an existing cover
// And: New cover data has invalid format
// When: UpdateLeagueCoverUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject update with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A league exists with an existing cover
// And: New cover data exceeds maximum file size
// When: UpdateLeagueCoverUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateLeagueLogoUseCase - Success Path', () => {
it('should update existing league logo', async () => {
// TODO: Implement test
// Scenario: Admin updates league logo
// Given: A league exists with an existing logo
// And: Valid new logo image data is provided
// When: UpdateLeagueLogoUseCase.execute() is called with league ID and new image data
// Then: The old logo should be replaced with the new one
// And: The new logo should have updated metadata
// And: EventPublisher should emit LeagueLogoUpdatedEvent
});
it('should update logo with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin updates logo with validation
// Given: A league exists with an existing logo
// And: New logo data meets validation requirements
// When: UpdateLeagueLogoUseCase.execute() is called
// Then: The logo should be updated successfully
// And: EventPublisher should emit LeagueLogoUpdatedEvent
});
});
describe('UpdateLeagueLogoUseCase - Validation', () => {
it('should reject update with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A league exists with an existing logo
// And: New logo data has invalid format
// When: UpdateLeagueLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject update with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A league exists with an existing logo
// And: New logo data exceeds maximum file size
// When: UpdateLeagueLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('DeleteLeagueCoverUseCase - Success Path', () => {
it('should delete league cover', async () => {
// TODO: Implement test
// Scenario: Admin deletes league cover
// Given: A league exists with an existing cover
// When: DeleteLeagueCoverUseCase.execute() is called with league ID
// Then: The cover should be removed from the repository
// And: The league should show a default cover
// And: EventPublisher should emit LeagueCoverDeletedEvent
});
it('should delete specific cover when league has multiple covers', async () => {
// TODO: Implement test
// Scenario: League with multiple covers
// Given: A league exists with multiple covers
// When: DeleteLeagueCoverUseCase.execute() is called with specific cover ID
// Then: Only that cover should be removed
// And: Other covers should remain
// And: EventPublisher should emit LeagueCoverDeletedEvent
});
});
describe('DeleteLeagueCoverUseCase - Error Handling', () => {
it('should handle deletion when league has no cover', async () => {
// TODO: Implement test
// Scenario: League without cover
// Given: A league exists without a cover
// When: DeleteLeagueCoverUseCase.execute() is called with league ID
// Then: Should complete successfully (no-op)
// And: EventPublisher should emit LeagueCoverDeletedEvent
});
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: DeleteLeagueCoverUseCase.execute() is called with non-existent league ID
// Then: Should throw LeagueNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('DeleteLeagueLogoUseCase - Success Path', () => {
it('should delete league logo', async () => {
// TODO: Implement test
// Scenario: Admin deletes league logo
// Given: A league exists with an existing logo
// When: DeleteLeagueLogoUseCase.execute() is called with league ID
// Then: The logo should be removed from the repository
// And: The league should show a default logo
// And: EventPublisher should emit LeagueLogoDeletedEvent
});
});
describe('DeleteLeagueLogoUseCase - Error Handling', () => {
it('should handle deletion when league has no logo', async () => {
// TODO: Implement test
// Scenario: League without logo
// Given: A league exists without a logo
// When: DeleteLeagueLogoUseCase.execute() is called with league ID
// Then: Should complete successfully (no-op)
// And: EventPublisher should emit LeagueLogoDeletedEvent
});
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: DeleteLeagueLogoUseCase.execute() is called with non-existent league ID
// Then: Should throw LeagueNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('SetLeagueMediaFeaturedUseCase - Success Path', () => {
it('should set league cover as featured', async () => {
// TODO: Implement test
// Scenario: Admin sets cover as featured
// Given: A league exists with multiple covers
// When: SetLeagueMediaFeaturedUseCase.execute() is called with cover ID
// Then: The cover should be marked as featured
// And: Other covers should not be featured
// And: EventPublisher should emit LeagueMediaFeaturedEvent
});
it('should set league logo as featured', async () => {
// TODO: Implement test
// Scenario: Admin sets logo as featured
// Given: A league exists with multiple logos
// When: SetLeagueMediaFeaturedUseCase.execute() is called with logo ID
// Then: The logo should be marked as featured
// And: Other logos should not be featured
// And: EventPublisher should emit LeagueMediaFeaturedEvent
});
it('should update featured media when new one is set', async () => {
// TODO: Implement test
// Scenario: Update featured media
// Given: A league exists with a featured cover
// When: SetLeagueMediaFeaturedUseCase.execute() is called with a different cover
// Then: The new cover should be featured
// And: The old cover should not be featured
// And: EventPublisher should emit LeagueMediaFeaturedEvent
});
});
describe('SetLeagueMediaFeaturedUseCase - Error Handling', () => {
it('should throw error when media does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent media
// Given: A league exists
// And: No media exists with the given ID
// When: SetLeagueMediaFeaturedUseCase.execute() is called with non-existent media ID
// Then: Should throw MediaNotFoundError
// 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: No league exists with the given ID
// When: SetLeagueMediaFeaturedUseCase.execute() is called with non-existent league ID
// Then: Should throw LeagueNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('League Media Data Orchestration', () => {
it('should correctly format league media metadata', async () => {
// TODO: Implement test
// Scenario: League media metadata formatting
// Given: A league exists with cover and logo
// When: GetLeagueMediaUseCase.execute() is called
// Then: Media metadata should show:
// - File size: Correctly formatted (e.g., "3.2 MB")
// - File format: Correct format (e.g., "PNG", "JPEG")
// - Upload date: Correctly formatted date
// - Featured status: Correctly indicated
});
it('should correctly handle league media caching', async () => {
// TODO: Implement test
// Scenario: League media caching
// Given: A league exists with media
// When: GetLeagueMediaUseCase.execute() is called multiple times
// Then: Subsequent calls should return cached data
// And: EventPublisher should emit LeagueMediaRetrievedEvent for each call
});
it('should correctly handle league media error states', async () => {
// TODO: Implement test
// Scenario: League media error handling
// Given: A league exists
// And: LeagueMediaRepository throws an error during retrieval
// When: GetLeagueMediaUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
it('should correctly handle multiple media files per league', async () => {
// TODO: Implement test
// Scenario: Multiple media files per league
// Given: A league exists with multiple covers and logos
// When: GetLeagueMediaUseCase.execute() is called
// Then: All media files should be returned
// And: Each media file should have correct metadata
// And: EventPublisher should emit LeagueMediaRetrievedEvent
});
});
});

View File

@@ -0,0 +1,60 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { MediaTestContext } from '../MediaTestContext';
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
import { League } from '@core/racing/domain/entities/League';
import { MediaReference } from '@core/domain/media/MediaReference';
describe('League Media Management', () => {
let ctx: MediaTestContext;
let leagueRepository: InMemoryLeagueRepository;
beforeEach(() => {
ctx = MediaTestContext.create();
ctx.reset();
leagueRepository = new InMemoryLeagueRepository(ctx.logger);
});
it('should upload and set a league logo', async () => {
// Given: A league exists
const league = League.create({
id: 'league-1',
name: 'Test League',
description: 'Test Description',
ownerId: 'owner-1',
});
await leagueRepository.create(league);
// When: A logo is uploaded
const uploadResult = await ctx.mediaStorage.uploadMedia(
Buffer.from('logo content'),
{ filename: 'logo.png', mimeType: 'image/png' }
);
expect(uploadResult.success).toBe(true);
const mediaId = 'media-1';
// And: The league is updated with the new logo reference
const updatedLeague = league.update({
logoRef: MediaReference.createUploaded(mediaId)
});
await leagueRepository.update(updatedLeague);
// Then: The league should have the correct logo reference
const savedLeague = await leagueRepository.findById('league-1');
expect(savedLeague?.logoRef.type).toBe('uploaded');
expect(savedLeague?.logoRef.mediaId).toBe(mediaId);
});
it('should retrieve league media (simulated via repository)', async () => {
const league = League.create({
id: 'league-1',
name: 'Test League',
description: 'Test Description',
ownerId: 'owner-1',
});
await leagueRepository.create(league);
const found = await leagueRepository.findById('league-1');
expect(found).not.toBeNull();
expect(found?.logoRef).toBeDefined();
});
});

View File

@@ -1,380 +0,0 @@
/**
* Integration Test: Sponsor Logo Management Use Case Orchestration
*
* Tests the orchestration logic of sponsor logo-related Use Cases:
* - GetSponsorLogosUseCase: Retrieves sponsor logos
* - UploadSponsorLogoUseCase: Uploads a new sponsor logo
* - UpdateSponsorLogoUseCase: Updates an existing sponsor logo
* - DeleteSponsorLogoUseCase: Deletes a sponsor logo
* - SetSponsorFeaturedUseCase: Sets sponsor as featured
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
describe('Sponsor Logo Management Use Case Orchestration', () => {
// TODO: Initialize In-Memory repositories and event publisher
// let sponsorLogoRepository: InMemorySponsorLogoRepository;
// let sponsorRepository: InMemorySponsorRepository;
// let eventPublisher: InMemoryEventPublisher;
// let getSponsorLogosUseCase: GetSponsorLogosUseCase;
// let uploadSponsorLogoUseCase: UploadSponsorLogoUseCase;
// let updateSponsorLogoUseCase: UpdateSponsorLogoUseCase;
// let deleteSponsorLogoUseCase: DeleteSponsorLogoUseCase;
// let setSponsorFeaturedUseCase: SetSponsorFeaturedUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// sponsorLogoRepository = new InMemorySponsorLogoRepository();
// sponsorRepository = new InMemorySponsorRepository();
// eventPublisher = new InMemoryEventPublisher();
// getSponsorLogosUseCase = new GetSponsorLogosUseCase({
// sponsorLogoRepository,
// sponsorRepository,
// eventPublisher,
// });
// uploadSponsorLogoUseCase = new UploadSponsorLogoUseCase({
// sponsorLogoRepository,
// sponsorRepository,
// eventPublisher,
// });
// updateSponsorLogoUseCase = new UpdateSponsorLogoUseCase({
// sponsorLogoRepository,
// sponsorRepository,
// eventPublisher,
// });
// deleteSponsorLogoUseCase = new DeleteSponsorLogoUseCase({
// sponsorLogoRepository,
// sponsorRepository,
// eventPublisher,
// });
// setSponsorFeaturedUseCase = new SetSponsorFeaturedUseCase({
// sponsorLogoRepository,
// sponsorRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// sponsorLogoRepository.clear();
// sponsorRepository.clear();
// eventPublisher.clear();
});
describe('GetSponsorLogosUseCase - Success Path', () => {
it('should retrieve all sponsor logos', async () => {
// TODO: Implement test
// Scenario: Multiple sponsors with logos
// Given: Multiple sponsors exist with logos
// When: GetSponsorLogosUseCase.execute() is called
// Then: The result should contain all sponsor logos
// And: Each logo should have correct metadata
// And: EventPublisher should emit SponsorLogosRetrievedEvent
});
it('should retrieve sponsor logos for specific tier', async () => {
// TODO: Implement test
// Scenario: Filter by sponsor tier
// Given: Sponsors exist with different tiers
// When: GetSponsorLogosUseCase.execute() is called with tier filter
// Then: The result should only contain logos for that tier
// And: EventPublisher should emit SponsorLogosRetrievedEvent
});
it('should retrieve sponsor logos with search query', async () => {
// TODO: Implement test
// Scenario: Search sponsors by name
// Given: Sponsors exist with various names
// When: GetSponsorLogosUseCase.execute() is called with search query
// Then: The result should only contain matching sponsors
// And: EventPublisher should emit SponsorLogosRetrievedEvent
});
it('should retrieve featured sponsor logos', async () => {
// TODO: Implement test
// Scenario: Filter by featured status
// Given: Sponsors exist with featured and non-featured logos
// When: GetSponsorLogosUseCase.execute() is called with featured filter
// Then: The result should only contain featured logos
// And: EventPublisher should emit SponsorLogosRetrievedEvent
});
});
describe('GetSponsorLogosUseCase - Edge Cases', () => {
it('should handle empty sponsor list', async () => {
// TODO: Implement test
// Scenario: No sponsors exist
// Given: No sponsors exist in the system
// When: GetSponsorLogosUseCase.execute() is called
// Then: The result should be an empty list
// And: EventPublisher should emit SponsorLogosRetrievedEvent
});
it('should handle sponsors without logos', async () => {
// TODO: Implement test
// Scenario: Sponsors exist without logos
// Given: Sponsors exist without logos
// When: GetSponsorLogosUseCase.execute() is called
// Then: The result should show sponsors with default logos
// And: EventPublisher should emit SponsorLogosRetrievedEvent
});
});
describe('UploadSponsorLogoUseCase - Success Path', () => {
it('should upload a new sponsor logo', async () => {
// TODO: Implement test
// Scenario: Admin uploads new sponsor logo
// Given: A sponsor exists without a logo
// And: Valid logo image data is provided
// When: UploadSponsorLogoUseCase.execute() is called with sponsor ID and image data
// Then: The logo should be stored in the repository
// And: The logo should have correct metadata (file size, format, upload date)
// And: EventPublisher should emit SponsorLogoUploadedEvent
});
it('should upload logo with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin uploads logo with validation
// Given: A sponsor exists
// And: Logo data meets validation requirements (correct format, size, dimensions)
// When: UploadSponsorLogoUseCase.execute() is called
// Then: The logo should be stored successfully
// And: EventPublisher should emit SponsorLogoUploadedEvent
});
it('should upload logo for new sponsor creation', async () => {
// TODO: Implement test
// Scenario: Admin creates sponsor with logo
// Given: No sponsor exists
// When: UploadSponsorLogoUseCase.execute() is called with new sponsor details and logo
// Then: The sponsor should be created
// And: The logo should be stored
// And: EventPublisher should emit SponsorCreatedEvent and SponsorLogoUploadedEvent
});
});
describe('UploadSponsorLogoUseCase - Validation', () => {
it('should reject upload with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A sponsor exists
// And: Logo data has invalid format (e.g., .txt, .exe)
// When: UploadSponsorLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A sponsor exists
// And: Logo data exceeds maximum file size
// When: UploadSponsorLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with invalid dimensions', async () => {
// TODO: Implement test
// Scenario: Invalid image dimensions
// Given: A sponsor exists
// And: Logo data has invalid dimensions (too small or too large)
// When: UploadSponsorLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateSponsorLogoUseCase - Success Path', () => {
it('should update existing sponsor logo', async () => {
// TODO: Implement test
// Scenario: Admin updates sponsor logo
// Given: A sponsor exists with an existing logo
// And: Valid new logo image data is provided
// When: UpdateSponsorLogoUseCase.execute() is called with sponsor ID and new image data
// Then: The old logo should be replaced with the new one
// And: The new logo should have updated metadata
// And: EventPublisher should emit SponsorLogoUpdatedEvent
});
it('should update logo with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin updates logo with validation
// Given: A sponsor exists with an existing logo
// And: New logo data meets validation requirements
// When: UpdateSponsorLogoUseCase.execute() is called
// Then: The logo should be updated successfully
// And: EventPublisher should emit SponsorLogoUpdatedEvent
});
it('should update logo for sponsor with multiple logos', async () => {
// TODO: Implement test
// Scenario: Sponsor with multiple logos
// Given: A sponsor exists with multiple logos
// When: UpdateSponsorLogoUseCase.execute() is called
// Then: Only the specified logo should be updated
// And: Other logos should remain unchanged
// And: EventPublisher should emit SponsorLogoUpdatedEvent
});
});
describe('UpdateSponsorLogoUseCase - Validation', () => {
it('should reject update with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A sponsor exists with an existing logo
// And: New logo data has invalid format
// When: UpdateSponsorLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject update with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A sponsor exists with an existing logo
// And: New logo data exceeds maximum file size
// When: UpdateSponsorLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('DeleteSponsorLogoUseCase - Success Path', () => {
it('should delete sponsor logo', async () => {
// TODO: Implement test
// Scenario: Admin deletes sponsor logo
// Given: A sponsor exists with an existing logo
// When: DeleteSponsorLogoUseCase.execute() is called with sponsor ID
// Then: The logo should be removed from the repository
// And: The sponsor should show a default logo
// And: EventPublisher should emit SponsorLogoDeletedEvent
});
it('should delete specific logo when sponsor has multiple logos', async () => {
// TODO: Implement test
// Scenario: Sponsor with multiple logos
// Given: A sponsor exists with multiple logos
// When: DeleteSponsorLogoUseCase.execute() is called with specific logo ID
// Then: Only that logo should be removed
// And: Other logos should remain
// And: EventPublisher should emit SponsorLogoDeletedEvent
});
});
describe('DeleteSponsorLogoUseCase - Error Handling', () => {
it('should handle deletion when sponsor has no logo', async () => {
// TODO: Implement test
// Scenario: Sponsor without logo
// Given: A sponsor exists without a logo
// When: DeleteSponsorLogoUseCase.execute() is called with sponsor ID
// Then: Should complete successfully (no-op)
// And: EventPublisher should emit SponsorLogoDeletedEvent
});
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: DeleteSponsorLogoUseCase.execute() is called with non-existent sponsor ID
// Then: Should throw SponsorNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('SetSponsorFeaturedUseCase - Success Path', () => {
it('should set sponsor as featured', async () => {
// TODO: Implement test
// Scenario: Admin sets sponsor as featured
// Given: A sponsor exists
// When: SetSponsorFeaturedUseCase.execute() is called with sponsor ID
// Then: The sponsor should be marked as featured
// And: EventPublisher should emit SponsorFeaturedEvent
});
it('should update featured sponsor when new one is set', async () => {
// TODO: Implement test
// Scenario: Update featured sponsor
// Given: A sponsor exists as featured
// When: SetSponsorFeaturedUseCase.execute() is called with a different sponsor
// Then: The new sponsor should be featured
// And: The old sponsor should not be featured
// And: EventPublisher should emit SponsorFeaturedEvent
});
it('should set sponsor as featured with specific tier', async () => {
// TODO: Implement test
// Scenario: Set sponsor as featured by tier
// Given: Sponsors exist with different tiers
// When: SetSponsorFeaturedUseCase.execute() is called with tier filter
// Then: The sponsor from that tier should be featured
// And: EventPublisher should emit SponsorFeaturedEvent
});
});
describe('SetSponsorFeaturedUseCase - 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: SetSponsorFeaturedUseCase.execute() is called with non-existent sponsor ID
// Then: Should throw SponsorNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('Sponsor Logo Data Orchestration', () => {
it('should correctly format sponsor logo metadata', async () => {
// TODO: Implement test
// Scenario: Sponsor logo metadata formatting
// Given: A sponsor exists with a logo
// When: GetSponsorLogosUseCase.execute() is called
// Then: Logo metadata should show:
// - File size: Correctly formatted (e.g., "1.5 MB")
// - File format: Correct format (e.g., "PNG", "SVG")
// - Upload date: Correctly formatted date
// - Featured status: Correctly indicated
});
it('should correctly handle sponsor logo caching', async () => {
// TODO: Implement test
// Scenario: Sponsor logo caching
// Given: Sponsors exist with logos
// When: GetSponsorLogosUseCase.execute() is called multiple times
// Then: Subsequent calls should return cached data
// And: EventPublisher should emit SponsorLogosRetrievedEvent for each call
});
it('should correctly handle sponsor logo error states', async () => {
// TODO: Implement test
// Scenario: Sponsor logo error handling
// Given: Sponsors exist
// And: SponsorLogoRepository throws an error during retrieval
// When: GetSponsorLogosUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
it('should correctly handle sponsor tier filtering', async () => {
// TODO: Implement test
// Scenario: Sponsor tier filtering
// Given: Sponsors exist with different tiers (Gold, Silver, Bronze)
// When: GetSponsorLogosUseCase.execute() is called with tier filter
// Then: Only sponsors from the specified tier should be returned
// And: EventPublisher should emit SponsorLogosRetrievedEvent
});
it('should correctly handle bulk sponsor logo operations', async () => {
// TODO: Implement test
// Scenario: Bulk sponsor logo operations
// Given: Multiple sponsors exist
// When: Bulk upload or export operations are performed
// Then: All operations should complete successfully
// And: EventPublisher should emit appropriate events for each operation
});
});
});

View File

@@ -0,0 +1,57 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { MediaTestContext } from '../MediaTestContext';
import { InMemorySponsorRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorRepository';
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
describe('Sponsor Logo Management', () => {
let ctx: MediaTestContext;
let sponsorRepository: InMemorySponsorRepository;
beforeEach(() => {
ctx = MediaTestContext.create();
ctx.reset();
sponsorRepository = new InMemorySponsorRepository(ctx.logger);
});
it('should upload and set a sponsor logo', async () => {
// Given: A sponsor exists
const sponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
// When: A logo is uploaded
const uploadResult = await ctx.mediaStorage.uploadMedia(
Buffer.from('logo content'),
{ filename: 'logo.png', mimeType: 'image/png' }
);
expect(uploadResult.success).toBe(true);
const logoUrl = `https://example.com${uploadResult.url!}`;
// And: The sponsor is updated with the new logo URL
const updatedSponsor = sponsor.update({
logoUrl: logoUrl
});
await sponsorRepository.update(updatedSponsor);
// Then: The sponsor should have the correct logo URL
const savedSponsor = await sponsorRepository.findById('sponsor-1');
expect(savedSponsor?.logoUrl?.value).toBe(logoUrl);
});
it('should retrieve sponsor logos (simulated via repository)', async () => {
const sponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
logoUrl: 'https://example.com/logo.png'
});
await sponsorRepository.create(sponsor);
const found = await sponsorRepository.findById('sponsor-1');
expect(found).not.toBeNull();
expect(found?.logoUrl?.value).toBe('https://example.com/logo.png');
});
});

View File

@@ -1,390 +0,0 @@
/**
* Integration Test: Team Logo Management Use Case Orchestration
*
* Tests the orchestration logic of team logo-related Use Cases:
* - GetTeamLogosUseCase: Retrieves team logos
* - UploadTeamLogoUseCase: Uploads a new team logo
* - UpdateTeamLogoUseCase: Updates an existing team logo
* - DeleteTeamLogoUseCase: Deletes a team logo
* - SetTeamFeaturedUseCase: Sets team as featured
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
describe('Team Logo Management Use Case Orchestration', () => {
// TODO: Initialize In-Memory repositories and event publisher
// let teamLogoRepository: InMemoryTeamLogoRepository;
// let teamRepository: InMemoryTeamRepository;
// let eventPublisher: InMemoryEventPublisher;
// let getTeamLogosUseCase: GetTeamLogosUseCase;
// let uploadTeamLogoUseCase: UploadTeamLogoUseCase;
// let updateTeamLogoUseCase: UpdateTeamLogoUseCase;
// let deleteTeamLogoUseCase: DeleteTeamLogoUseCase;
// let setTeamFeaturedUseCase: SetTeamFeaturedUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// teamLogoRepository = new InMemoryTeamLogoRepository();
// teamRepository = new InMemoryTeamRepository();
// eventPublisher = new InMemoryEventPublisher();
// getTeamLogosUseCase = new GetTeamLogosUseCase({
// teamLogoRepository,
// teamRepository,
// eventPublisher,
// });
// uploadTeamLogoUseCase = new UploadTeamLogoUseCase({
// teamLogoRepository,
// teamRepository,
// eventPublisher,
// });
// updateTeamLogoUseCase = new UpdateTeamLogoUseCase({
// teamLogoRepository,
// teamRepository,
// eventPublisher,
// });
// deleteTeamLogoUseCase = new DeleteTeamLogoUseCase({
// teamLogoRepository,
// teamRepository,
// eventPublisher,
// });
// setTeamFeaturedUseCase = new SetTeamFeaturedUseCase({
// teamLogoRepository,
// teamRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// teamLogoRepository.clear();
// teamRepository.clear();
// eventPublisher.clear();
});
describe('GetTeamLogosUseCase - Success Path', () => {
it('should retrieve all team logos', async () => {
// TODO: Implement test
// Scenario: Multiple teams with logos
// Given: Multiple teams exist with logos
// When: GetTeamLogosUseCase.execute() is called
// Then: The result should contain all team logos
// And: Each logo should have correct metadata
// And: EventPublisher should emit TeamLogosRetrievedEvent
});
it('should retrieve team logos for specific league', async () => {
// TODO: Implement test
// Scenario: Filter by league
// Given: Teams exist in different leagues
// When: GetTeamLogosUseCase.execute() is called with league filter
// Then: The result should only contain logos for that league
// And: EventPublisher should emit TeamLogosRetrievedEvent
});
it('should retrieve team logos with search query', async () => {
// TODO: Implement test
// Scenario: Search teams by name
// Given: Teams exist with various names
// When: GetTeamLogosUseCase.execute() is called with search query
// Then: The result should only contain matching teams
// And: EventPublisher should emit TeamLogosRetrievedEvent
});
it('should retrieve featured team logos', async () => {
// TODO: Implement test
// Scenario: Filter by featured status
// Given: Teams exist with featured and non-featured logos
// When: GetTeamLogosUseCase.execute() is called with featured filter
// Then: The result should only contain featured logos
// And: EventPublisher should emit TeamLogosRetrievedEvent
});
});
describe('GetTeamLogosUseCase - Edge Cases', () => {
it('should handle empty team list', async () => {
// TODO: Implement test
// Scenario: No teams exist
// Given: No teams exist in the system
// When: GetTeamLogosUseCase.execute() is called
// Then: The result should be an empty list
// And: EventPublisher should emit TeamLogosRetrievedEvent
});
it('should handle teams without logos', async () => {
// TODO: Implement test
// Scenario: Teams exist without logos
// Given: Teams exist without logos
// When: GetTeamLogosUseCase.execute() is called
// Then: The result should show teams with default logos
// And: EventPublisher should emit TeamLogosRetrievedEvent
});
});
describe('UploadTeamLogoUseCase - Success Path', () => {
it('should upload a new team logo', async () => {
// TODO: Implement test
// Scenario: Admin uploads new team logo
// Given: A team exists without a logo
// And: Valid logo image data is provided
// When: UploadTeamLogoUseCase.execute() is called with team ID and image data
// Then: The logo should be stored in the repository
// And: The logo should have correct metadata (file size, format, upload date)
// And: EventPublisher should emit TeamLogoUploadedEvent
});
it('should upload logo with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin uploads logo with validation
// Given: A team exists
// And: Logo data meets validation requirements (correct format, size, dimensions)
// When: UploadTeamLogoUseCase.execute() is called
// Then: The logo should be stored successfully
// And: EventPublisher should emit TeamLogoUploadedEvent
});
it('should upload logo for new team creation', async () => {
// TODO: Implement test
// Scenario: Admin creates team with logo
// Given: No team exists
// When: UploadTeamLogoUseCase.execute() is called with new team details and logo
// Then: The team should be created
// And: The logo should be stored
// And: EventPublisher should emit TeamCreatedEvent and TeamLogoUploadedEvent
});
});
describe('UploadTeamLogoUseCase - Validation', () => {
it('should reject upload with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A team exists
// And: Logo data has invalid format (e.g., .txt, .exe)
// When: UploadTeamLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A team exists
// And: Logo data exceeds maximum file size
// When: UploadTeamLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with invalid dimensions', async () => {
// TODO: Implement test
// Scenario: Invalid image dimensions
// Given: A team exists
// And: Logo data has invalid dimensions (too small or too large)
// When: UploadTeamLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateTeamLogoUseCase - Success Path', () => {
it('should update existing team logo', async () => {
// TODO: Implement test
// Scenario: Admin updates team logo
// Given: A team exists with an existing logo
// And: Valid new logo image data is provided
// When: UpdateTeamLogoUseCase.execute() is called with team ID and new image data
// Then: The old logo should be replaced with the new one
// And: The new logo should have updated metadata
// And: EventPublisher should emit TeamLogoUpdatedEvent
});
it('should update logo with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin updates logo with validation
// Given: A team exists with an existing logo
// And: New logo data meets validation requirements
// When: UpdateTeamLogoUseCase.execute() is called
// Then: The logo should be updated successfully
// And: EventPublisher should emit TeamLogoUpdatedEvent
});
it('should update logo for team with multiple logos', async () => {
// TODO: Implement test
// Scenario: Team with multiple logos
// Given: A team exists with multiple logos
// When: UpdateTeamLogoUseCase.execute() is called
// Then: Only the specified logo should be updated
// And: Other logos should remain unchanged
// And: EventPublisher should emit TeamLogoUpdatedEvent
});
});
describe('UpdateTeamLogoUseCase - Validation', () => {
it('should reject update with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A team exists with an existing logo
// And: New logo data has invalid format
// When: UpdateTeamLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject update with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A team exists with an existing logo
// And: New logo data exceeds maximum file size
// When: UpdateTeamLogoUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('DeleteTeamLogoUseCase - Success Path', () => {
it('should delete team logo', async () => {
// TODO: Implement test
// Scenario: Admin deletes team logo
// Given: A team exists with an existing logo
// When: DeleteTeamLogoUseCase.execute() is called with team ID
// Then: The logo should be removed from the repository
// And: The team should show a default logo
// And: EventPublisher should emit TeamLogoDeletedEvent
});
it('should delete specific logo when team has multiple logos', async () => {
// TODO: Implement test
// Scenario: Team with multiple logos
// Given: A team exists with multiple logos
// When: DeleteTeamLogoUseCase.execute() is called with specific logo ID
// Then: Only that logo should be removed
// And: Other logos should remain
// And: EventPublisher should emit TeamLogoDeletedEvent
});
});
describe('DeleteTeamLogoUseCase - Error Handling', () => {
it('should handle deletion when team has no logo', async () => {
// TODO: Implement test
// Scenario: Team without logo
// Given: A team exists without a logo
// When: DeleteTeamLogoUseCase.execute() is called with team ID
// Then: Should complete successfully (no-op)
// And: EventPublisher should emit TeamLogoDeletedEvent
});
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: DeleteTeamLogoUseCase.execute() is called with non-existent team ID
// Then: Should throw TeamNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('SetTeamFeaturedUseCase - Success Path', () => {
it('should set team as featured', async () => {
// TODO: Implement test
// Scenario: Admin sets team as featured
// Given: A team exists
// When: SetTeamFeaturedUseCase.execute() is called with team ID
// Then: The team should be marked as featured
// And: EventPublisher should emit TeamFeaturedEvent
});
it('should update featured team when new one is set', async () => {
// TODO: Implement test
// Scenario: Update featured team
// Given: A team exists as featured
// When: SetTeamFeaturedUseCase.execute() is called with a different team
// Then: The new team should be featured
// And: The old team should not be featured
// And: EventPublisher should emit TeamFeaturedEvent
});
it('should set team as featured with specific league', async () => {
// TODO: Implement test
// Scenario: Set team as featured by league
// Given: Teams exist in different leagues
// When: SetTeamFeaturedUseCase.execute() is called with league filter
// Then: The team from that league should be featured
// And: EventPublisher should emit TeamFeaturedEvent
});
});
describe('SetTeamFeaturedUseCase - 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: SetTeamFeaturedUseCase.execute() is called with non-existent team ID
// Then: Should throw TeamNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('Team Logo Data Orchestration', () => {
it('should correctly format team logo metadata', async () => {
// TODO: Implement test
// Scenario: Team logo metadata formatting
// Given: A team exists with a logo
// When: GetTeamLogosUseCase.execute() is called
// Then: Logo metadata should show:
// - File size: Correctly formatted (e.g., "1.8 MB")
// - File format: Correct format (e.g., "PNG", "SVG")
// - Upload date: Correctly formatted date
// - Featured status: Correctly indicated
});
it('should correctly handle team logo caching', async () => {
// TODO: Implement test
// Scenario: Team logo caching
// Given: Teams exist with logos
// When: GetTeamLogosUseCase.execute() is called multiple times
// Then: Subsequent calls should return cached data
// And: EventPublisher should emit TeamLogosRetrievedEvent for each call
});
it('should correctly handle team logo error states', async () => {
// TODO: Implement test
// Scenario: Team logo error handling
// Given: Teams exist
// And: TeamLogoRepository throws an error during retrieval
// When: GetTeamLogosUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
it('should correctly handle team league filtering', async () => {
// TODO: Implement test
// Scenario: Team league filtering
// Given: Teams exist in different leagues
// When: GetTeamLogosUseCase.execute() is called with league filter
// Then: Only teams from the specified league should be returned
// And: EventPublisher should emit TeamLogosRetrievedEvent
});
it('should correctly handle team roster with logos', async () => {
// TODO: Implement test
// Scenario: Team roster with logos
// Given: A team exists with members and logo
// When: GetTeamLogosUseCase.execute() is called
// Then: The result should show team logo
// And: Team roster should be accessible
// And: EventPublisher should emit TeamLogosRetrievedEvent
});
it('should correctly handle bulk team logo operations', async () => {
// TODO: Implement test
// Scenario: Bulk team logo operations
// Given: Multiple teams exist
// When: Bulk upload or export operations are performed
// Then: All operations should complete successfully
// And: EventPublisher should emit appropriate events for each operation
});
});
});

View File

@@ -0,0 +1,75 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { MediaTestContext } from '../MediaTestContext';
import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository';
import { InMemoryTeamMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
import { Team } from '@core/racing/domain/entities/Team';
import { MediaReference } from '@core/domain/media/MediaReference';
describe('Team Logo Management', () => {
let ctx: MediaTestContext;
let teamRepository: InMemoryTeamRepository;
let membershipRepository: InMemoryTeamMembershipRepository;
beforeEach(() => {
ctx = MediaTestContext.create();
ctx.reset();
teamRepository = new InMemoryTeamRepository(ctx.logger);
membershipRepository = new InMemoryTeamMembershipRepository(ctx.logger);
});
it('should upload and set a team logo', async () => {
// Given: A team exists
const team = Team.create({
id: 'team-1',
name: 'Test Team',
tag: 'TST',
description: 'Test Description',
ownerId: 'owner-1',
leagues: [],
});
await teamRepository.create(team);
// When: A logo is uploaded
const uploadResult = await ctx.mediaStorage.uploadMedia(
Buffer.from('logo content'),
{ filename: 'logo.png', mimeType: 'image/png' }
);
expect(uploadResult.success).toBe(true);
const mediaId = 'media-1'; // In real use case, this comes from repository save
// And: The team is updated with the new logo reference
const updatedTeam = team.update({
logoRef: MediaReference.createUploaded(mediaId)
});
await teamRepository.update(updatedTeam);
// Then: The team should have the correct logo reference
const savedTeam = await teamRepository.findById('team-1');
expect(savedTeam?.logoRef.type).toBe('uploaded');
expect(savedTeam?.logoRef.mediaId).toBe(mediaId);
});
it('should retrieve team logos (simulated via repository)', async () => {
const team1 = Team.create({
id: 'team-1',
name: 'Team 1',
tag: 'T1',
description: 'Desc 1',
ownerId: 'owner-1',
leagues: ['league-1'],
});
const team2 = Team.create({
id: 'team-2',
name: 'Team 2',
tag: 'T2',
description: 'Desc 2',
ownerId: 'owner-2',
leagues: ['league-1'],
});
await teamRepository.create(team1);
await teamRepository.create(team2);
const leagueTeams = await teamRepository.findByLeagueId('league-1');
expect(leagueTeams).toHaveLength(2);
});
});

View File

@@ -1,390 +0,0 @@
/**
* Integration Test: Track Image Management Use Case Orchestration
*
* Tests the orchestration logic of track image-related Use Cases:
* - GetTrackImagesUseCase: Retrieves track images
* - UploadTrackImageUseCase: Uploads a new track image
* - UpdateTrackImageUseCase: Updates an existing track image
* - DeleteTrackImageUseCase: Deletes a track image
* - SetTrackFeaturedUseCase: Sets track as featured
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
describe('Track Image Management Use Case Orchestration', () => {
// TODO: Initialize In-Memory repositories and event publisher
// let trackImageRepository: InMemoryTrackImageRepository;
// let trackRepository: InMemoryTrackRepository;
// let eventPublisher: InMemoryEventPublisher;
// let getTrackImagesUseCase: GetTrackImagesUseCase;
// let uploadTrackImageUseCase: UploadTrackImageUseCase;
// let updateTrackImageUseCase: UpdateTrackImageUseCase;
// let deleteTrackImageUseCase: DeleteTrackImageUseCase;
// let setTrackFeaturedUseCase: SetTrackFeaturedUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// trackImageRepository = new InMemoryTrackImageRepository();
// trackRepository = new InMemoryTrackRepository();
// eventPublisher = new InMemoryEventPublisher();
// getTrackImagesUseCase = new GetTrackImagesUseCase({
// trackImageRepository,
// trackRepository,
// eventPublisher,
// });
// uploadTrackImageUseCase = new UploadTrackImageUseCase({
// trackImageRepository,
// trackRepository,
// eventPublisher,
// });
// updateTrackImageUseCase = new UpdateTrackImageUseCase({
// trackImageRepository,
// trackRepository,
// eventPublisher,
// });
// deleteTrackImageUseCase = new DeleteTrackImageUseCase({
// trackImageRepository,
// trackRepository,
// eventPublisher,
// });
// setTrackFeaturedUseCase = new SetTrackFeaturedUseCase({
// trackImageRepository,
// trackRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// trackImageRepository.clear();
// trackRepository.clear();
// eventPublisher.clear();
});
describe('GetTrackImagesUseCase - Success Path', () => {
it('should retrieve all track images', async () => {
// TODO: Implement test
// Scenario: Multiple tracks with images
// Given: Multiple tracks exist with images
// When: GetTrackImagesUseCase.execute() is called
// Then: The result should contain all track images
// And: Each image should have correct metadata
// And: EventPublisher should emit TrackImagesRetrievedEvent
});
it('should retrieve track images for specific location', async () => {
// TODO: Implement test
// Scenario: Filter by location
// Given: Tracks exist in different locations
// When: GetTrackImagesUseCase.execute() is called with location filter
// Then: The result should only contain images for that location
// And: EventPublisher should emit TrackImagesRetrievedEvent
});
it('should retrieve track images with search query', async () => {
// TODO: Implement test
// Scenario: Search tracks by name
// Given: Tracks exist with various names
// When: GetTrackImagesUseCase.execute() is called with search query
// Then: The result should only contain matching tracks
// And: EventPublisher should emit TrackImagesRetrievedEvent
});
it('should retrieve featured track images', async () => {
// TODO: Implement test
// Scenario: Filter by featured status
// Given: Tracks exist with featured and non-featured images
// When: GetTrackImagesUseCase.execute() is called with featured filter
// Then: The result should only contain featured images
// And: EventPublisher should emit TrackImagesRetrievedEvent
});
});
describe('GetTrackImagesUseCase - Edge Cases', () => {
it('should handle empty track list', async () => {
// TODO: Implement test
// Scenario: No tracks exist
// Given: No tracks exist in the system
// When: GetTrackImagesUseCase.execute() is called
// Then: The result should be an empty list
// And: EventPublisher should emit TrackImagesRetrievedEvent
});
it('should handle tracks without images', async () => {
// TODO: Implement test
// Scenario: Tracks exist without images
// Given: Tracks exist without images
// When: GetTrackImagesUseCase.execute() is called
// Then: The result should show tracks with default images
// And: EventPublisher should emit TrackImagesRetrievedEvent
});
});
describe('UploadTrackImageUseCase - Success Path', () => {
it('should upload a new track image', async () => {
// TODO: Implement test
// Scenario: Admin uploads new track image
// Given: A track exists without an image
// And: Valid image data is provided
// When: UploadTrackImageUseCase.execute() is called with track ID and image data
// Then: The image should be stored in the repository
// And: The image should have correct metadata (file size, format, upload date)
// And: EventPublisher should emit TrackImageUploadedEvent
});
it('should upload image with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin uploads image with validation
// Given: A track exists
// And: Image data meets validation requirements (correct format, size, dimensions)
// When: UploadTrackImageUseCase.execute() is called
// Then: The image should be stored successfully
// And: EventPublisher should emit TrackImageUploadedEvent
});
it('should upload image for new track creation', async () => {
// TODO: Implement test
// Scenario: Admin creates track with image
// Given: No track exists
// When: UploadTrackImageUseCase.execute() is called with new track details and image
// Then: The track should be created
// And: The image should be stored
// And: EventPublisher should emit TrackCreatedEvent and TrackImageUploadedEvent
});
});
describe('UploadTrackImageUseCase - Validation', () => {
it('should reject upload with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A track exists
// And: Image data has invalid format (e.g., .txt, .exe)
// When: UploadTrackImageUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A track exists
// And: Image data exceeds maximum file size
// When: UploadTrackImageUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject upload with invalid dimensions', async () => {
// TODO: Implement test
// Scenario: Invalid image dimensions
// Given: A track exists
// And: Image data has invalid dimensions (too small or too large)
// When: UploadTrackImageUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateTrackImageUseCase - Success Path', () => {
it('should update existing track image', async () => {
// TODO: Implement test
// Scenario: Admin updates track image
// Given: A track exists with an existing image
// And: Valid new image data is provided
// When: UpdateTrackImageUseCase.execute() is called with track ID and new image data
// Then: The old image should be replaced with the new one
// And: The new image should have updated metadata
// And: EventPublisher should emit TrackImageUpdatedEvent
});
it('should update image with validation requirements', async () => {
// TODO: Implement test
// Scenario: Admin updates image with validation
// Given: A track exists with an existing image
// And: New image data meets validation requirements
// When: UpdateTrackImageUseCase.execute() is called
// Then: The image should be updated successfully
// And: EventPublisher should emit TrackImageUpdatedEvent
});
it('should update image for track with multiple images', async () => {
// TODO: Implement test
// Scenario: Track with multiple images
// Given: A track exists with multiple images
// When: UpdateTrackImageUseCase.execute() is called
// Then: Only the specified image should be updated
// And: Other images should remain unchanged
// And: EventPublisher should emit TrackImageUpdatedEvent
});
});
describe('UpdateTrackImageUseCase - Validation', () => {
it('should reject update with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A track exists with an existing image
// And: New image data has invalid format
// When: UpdateTrackImageUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should reject update with oversized file', async () => {
// TODO: Implement test
// Scenario: File exceeds size limit
// Given: A track exists with an existing image
// And: New image data exceeds maximum file size
// When: UpdateTrackImageUseCase.execute() is called
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('DeleteTrackImageUseCase - Success Path', () => {
it('should delete track image', async () => {
// TODO: Implement test
// Scenario: Admin deletes track image
// Given: A track exists with an existing image
// When: DeleteTrackImageUseCase.execute() is called with track ID
// Then: The image should be removed from the repository
// And: The track should show a default image
// And: EventPublisher should emit TrackImageDeletedEvent
});
it('should delete specific image when track has multiple images', async () => {
// TODO: Implement test
// Scenario: Track with multiple images
// Given: A track exists with multiple images
// When: DeleteTrackImageUseCase.execute() is called with specific image ID
// Then: Only that image should be removed
// And: Other images should remain
// And: EventPublisher should emit TrackImageDeletedEvent
});
});
describe('DeleteTrackImageUseCase - Error Handling', () => {
it('should handle deletion when track has no image', async () => {
// TODO: Implement test
// Scenario: Track without image
// Given: A track exists without an image
// When: DeleteTrackImageUseCase.execute() is called with track ID
// Then: Should complete successfully (no-op)
// And: EventPublisher should emit TrackImageDeletedEvent
});
it('should throw error when track does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent track
// Given: No track exists with the given ID
// When: DeleteTrackImageUseCase.execute() is called with non-existent track ID
// Then: Should throw TrackNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('SetTrackFeaturedUseCase - Success Path', () => {
it('should set track as featured', async () => {
// TODO: Implement test
// Scenario: Admin sets track as featured
// Given: A track exists
// When: SetTrackFeaturedUseCase.execute() is called with track ID
// Then: The track should be marked as featured
// And: EventPublisher should emit TrackFeaturedEvent
});
it('should update featured track when new one is set', async () => {
// TODO: Implement test
// Scenario: Update featured track
// Given: A track exists as featured
// When: SetTrackFeaturedUseCase.execute() is called with a different track
// Then: The new track should be featured
// And: The old track should not be featured
// And: EventPublisher should emit TrackFeaturedEvent
});
it('should set track as featured with specific location', async () => {
// TODO: Implement test
// Scenario: Set track as featured by location
// Given: Tracks exist in different locations
// When: SetTrackFeaturedUseCase.execute() is called with location filter
// Then: The track from that location should be featured
// And: EventPublisher should emit TrackFeaturedEvent
});
});
describe('SetTrackFeaturedUseCase - Error Handling', () => {
it('should throw error when track does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent track
// Given: No track exists with the given ID
// When: SetTrackFeaturedUseCase.execute() is called with non-existent track ID
// Then: Should throw TrackNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('Track Image Data Orchestration', () => {
it('should correctly format track image metadata', async () => {
// TODO: Implement test
// Scenario: Track image metadata formatting
// Given: A track exists with an image
// When: GetTrackImagesUseCase.execute() is called
// Then: Image metadata should show:
// - File size: Correctly formatted (e.g., "2.1 MB")
// - File format: Correct format (e.g., "PNG", "JPEG")
// - Upload date: Correctly formatted date
// - Featured status: Correctly indicated
});
it('should correctly handle track image caching', async () => {
// TODO: Implement test
// Scenario: Track image caching
// Given: Tracks exist with images
// When: GetTrackImagesUseCase.execute() is called multiple times
// Then: Subsequent calls should return cached data
// And: EventPublisher should emit TrackImagesRetrievedEvent for each call
});
it('should correctly handle track image error states', async () => {
// TODO: Implement test
// Scenario: Track image error handling
// Given: Tracks exist
// And: TrackImageRepository throws an error during retrieval
// When: GetTrackImagesUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
it('should correctly handle track location filtering', async () => {
// TODO: Implement test
// Scenario: Track location filtering
// Given: Tracks exist in different locations
// When: GetTrackImagesUseCase.execute() is called with location filter
// Then: Only tracks from the specified location should be returned
// And: EventPublisher should emit TrackImagesRetrievedEvent
});
it('should correctly handle track layout with images', async () => {
// TODO: Implement test
// Scenario: Track layout with images
// Given: A track exists with layout information and image
// When: GetTrackImagesUseCase.execute() is called
// Then: The result should show track image
// And: Track layout should be accessible
// And: EventPublisher should emit TrackImagesRetrievedEvent
});
it('should correctly handle bulk track image operations', async () => {
// TODO: Implement test
// Scenario: Bulk track image operations
// Given: Multiple tracks exist
// When: Bulk upload or export operations are performed
// Then: All operations should complete successfully
// And: EventPublisher should emit appropriate events for each operation
});
});
});

View File

@@ -0,0 +1,65 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { MediaTestContext } from '../MediaTestContext';
import { InMemoryTrackRepository } from '@adapters/racing/persistence/inmemory/InMemoryTrackRepository';
import { Track } from '@core/racing/domain/entities/Track';
describe('Track Image Management', () => {
let ctx: MediaTestContext;
let trackRepository: InMemoryTrackRepository;
beforeEach(() => {
ctx = MediaTestContext.create();
ctx.reset();
trackRepository = new InMemoryTrackRepository(ctx.logger);
});
it('should upload and set a track image', async () => {
// Given: A track exists
const track = Track.create({
id: 'track-1',
name: 'Test Track',
shortName: 'TST',
location: 'Test Location',
country: 'Test Country',
gameId: 'game-1',
category: 'road',
});
await trackRepository.create(track);
// When: An image is uploaded
const uploadResult = await ctx.mediaStorage.uploadMedia(
Buffer.from('image content'),
{ filename: 'track.png', mimeType: 'image/png' }
);
expect(uploadResult.success).toBe(true);
const imageUrl = uploadResult.url!;
// And: The track is updated with the new image URL
const updatedTrack = track.update({
imageUrl: imageUrl
});
await trackRepository.update(updatedTrack);
// Then: The track should have the correct image URL
const savedTrack = await trackRepository.findById('track-1');
expect(savedTrack?.imageUrl?.value).toBe(imageUrl);
});
it('should retrieve track images (simulated via repository)', async () => {
const track = Track.create({
id: 'track-1',
name: 'Test Track',
shortName: 'TST',
location: 'Test Location',
country: 'Test Country',
gameId: 'game-1',
category: 'road',
imageUrl: 'https://example.com/track.png'
});
await trackRepository.create(track);
const found = await trackRepository.findById('track-1');
expect(found).not.toBeNull();
expect(found?.imageUrl?.value).toBe('https://example.com/track.png');
});
});

View File

@@ -0,0 +1,32 @@
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { CompleteDriverOnboardingUseCase } from '../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { Logger } from '../../../core/shared/domain/Logger';
export class OnboardingTestContext {
public readonly driverRepository: InMemoryDriverRepository;
public readonly completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase;
public readonly mockLogger: Logger;
constructor() {
this.mockLogger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
this.driverRepository = new InMemoryDriverRepository(this.mockLogger);
this.completeDriverOnboardingUseCase = new CompleteDriverOnboardingUseCase(
this.driverRepository,
this.mockLogger
);
}
async clear() {
await this.driverRepository.clear();
}
static create() {
return new OnboardingTestContext();
}
}

View File

@@ -0,0 +1,5 @@
import { describe, it } from 'vitest';
describe('Onboarding Avatar Use Case Orchestration', () => {
it.todo('should test onboarding-specific avatar orchestration when implemented');
});

View File

@@ -0,0 +1,83 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { OnboardingTestContext } from '../OnboardingTestContext';
describe('CompleteDriverOnboardingUseCase - Success Path', () => {
let context: OnboardingTestContext;
beforeEach(async () => {
context = OnboardingTestContext.create();
await context.clear();
});
it('should complete onboarding with valid personal info', async () => {
// Scenario: Complete onboarding successfully
// Given: A new user ID
const userId = 'user-123';
const input = {
userId,
firstName: 'John',
lastName: 'Doe',
displayName: 'RacerJohn',
country: 'US',
bio: 'New racer on the grid',
};
// When: CompleteDriverOnboardingUseCase.execute() is called
const result = await context.completeDriverOnboardingUseCase.execute(input);
// Then: Driver should be created
expect(result.isOk()).toBe(true);
const { driver } = result.unwrap();
expect(driver.id).toBe(userId);
expect(driver.name.toString()).toBe('RacerJohn');
expect(driver.country.toString()).toBe('US');
expect(driver.bio?.toString()).toBe('New racer on the grid');
// And: Repository should contain the driver
const savedDriver = await context.driverRepository.findById(userId);
expect(savedDriver).not.toBeNull();
expect(savedDriver?.id).toBe(userId);
});
it('should complete onboarding with minimal required data', async () => {
// Scenario: Complete onboarding with minimal data
// Given: A new user ID
const userId = 'user-456';
const input = {
userId,
firstName: 'Jane',
lastName: 'Smith',
displayName: 'JaneS',
country: 'UK',
};
// When: CompleteDriverOnboardingUseCase.execute() is called
const result = await context.completeDriverOnboardingUseCase.execute(input);
// Then: Driver should be created successfully
expect(result.isOk()).toBe(true);
const { driver } = result.unwrap();
expect(driver.id).toBe(userId);
expect(driver.bio).toBeUndefined();
});
it('should handle bio as optional personal information', async () => {
// Scenario: Optional bio field
// Given: Personal info with bio
const input = {
userId: 'user-bio',
firstName: 'Bob',
lastName: 'Builder',
displayName: 'BobBuilds',
country: 'AU',
bio: 'I build fast cars',
};
// When: CompleteDriverOnboardingUseCase.execute() is called
const result = await context.completeDriverOnboardingUseCase.execute(input);
// Then: Bio should be saved
expect(result.isOk()).toBe(true);
expect(result.unwrap().driver.bio?.toString()).toBe('I build fast cars');
});
});

View File

@@ -0,0 +1,67 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { OnboardingTestContext } from '../OnboardingTestContext';
describe('CompleteDriverOnboardingUseCase - Validation & Errors', () => {
let context: OnboardingTestContext;
beforeEach(async () => {
context = OnboardingTestContext.create();
await context.clear();
});
it('should reject onboarding if driver already exists', async () => {
// Scenario: Already onboarded user
// Given: A driver already exists for the user
const userId = 'existing-user';
const existingInput = {
userId,
firstName: 'Old',
lastName: 'Name',
displayName: 'OldRacer',
country: 'DE',
};
await context.completeDriverOnboardingUseCase.execute(existingInput);
// When: CompleteDriverOnboardingUseCase.execute() is called again for same user
const result = await context.completeDriverOnboardingUseCase.execute({
userId,
firstName: 'New',
lastName: 'Name',
displayName: 'NewRacer',
country: 'FR',
});
// Then: Should return DRIVER_ALREADY_EXISTS error
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('DRIVER_ALREADY_EXISTS');
});
it('should handle repository errors gracefully', async () => {
// Scenario: Repository error
// Given: Repository throws an error
const userId = 'error-user';
const originalCreate = context.driverRepository.create.bind(context.driverRepository);
context.driverRepository.create = async () => {
throw new Error('Database failure');
};
// When: CompleteDriverOnboardingUseCase.execute() is called
const result = await context.completeDriverOnboardingUseCase.execute({
userId,
firstName: 'John',
lastName: 'Doe',
displayName: 'RacerJohn',
country: 'US',
});
// Then: Should return REPOSITORY_ERROR
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details.message).toBe('Database failure');
// Restore
context.driverRepository.create = originalCreate;
});
});

View File

@@ -1,17 +0,0 @@
/**
* Integration Test: Onboarding Avatar Use Case Orchestration
*
* Tests the orchestration logic of avatar-related Use Cases.
*
* NOTE: Currently, avatar generation is handled in core/media domain.
* This file remains as a placeholder for future onboarding-specific avatar orchestration
* if it moves out of the general media domain.
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it } from 'vitest';
describe('Onboarding Avatar Use Case Orchestration', () => {
it.todo('should test onboarding-specific avatar orchestration when implemented');
});

View File

@@ -1,84 +0,0 @@
/**
* Integration Test: Onboarding Personal Information Use Case Orchestration
*
* Tests the orchestration logic of personal information-related Use Cases:
* - CompleteDriverOnboardingUseCase: Handles the initial driver profile creation
*
* 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, beforeEach } from 'vitest';
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { CompleteDriverOnboardingUseCase } from '../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { Logger } from '../../../core/shared/domain/Logger';
describe('Onboarding Personal Information Use Case Orchestration', () => {
let driverRepository: InMemoryDriverRepository;
let completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase;
let mockLogger: Logger;
beforeAll(() => {
mockLogger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
driverRepository = new InMemoryDriverRepository(mockLogger);
completeDriverOnboardingUseCase = new CompleteDriverOnboardingUseCase(
driverRepository,
mockLogger
);
});
beforeEach(async () => {
await driverRepository.clear();
});
describe('CompleteDriverOnboardingUseCase - Personal Info Scenarios', () => {
it('should create driver with valid personal information', async () => {
// Scenario: Valid personal info
// Given: A new user
const input = {
userId: 'user-789',
firstName: 'Alice',
lastName: 'Wonderland',
displayName: 'AliceRacer',
country: 'UK',
};
// When: CompleteDriverOnboardingUseCase.execute() is called
const result = await completeDriverOnboardingUseCase.execute(input);
// Then: Validation should pass and driver be created
expect(result.isOk()).toBe(true);
const { driver } = result.unwrap();
expect(driver.name.toString()).toBe('AliceRacer');
expect(driver.country.toString()).toBe('UK');
});
it('should handle bio as optional personal information', async () => {
// Scenario: Optional bio field
// Given: Personal info with bio
const input = {
userId: 'user-bio',
firstName: 'Bob',
lastName: 'Builder',
displayName: 'BobBuilds',
country: 'AU',
bio: 'I build fast cars',
};
// When: CompleteDriverOnboardingUseCase.execute() is called
const result = await completeDriverOnboardingUseCase.execute(input);
// Then: Bio should be saved
expect(result.isOk()).toBe(true);
expect(result.unwrap().driver.bio?.toString()).toBe('I build fast cars');
});
});
});

View File

@@ -1,69 +0,0 @@
/**
* Integration Test: Onboarding Validation Use Case Orchestration
*
* Tests the orchestration logic of validation-related Use Cases:
* - CompleteDriverOnboardingUseCase: Validates driver data before creation
*
* 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, beforeEach } from 'vitest';
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { CompleteDriverOnboardingUseCase } from '../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { Logger } from '../../../core/shared/domain/Logger';
describe('Onboarding Validation Use Case Orchestration', () => {
let driverRepository: InMemoryDriverRepository;
let completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase;
let mockLogger: Logger;
beforeAll(() => {
mockLogger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
driverRepository = new InMemoryDriverRepository(mockLogger);
completeDriverOnboardingUseCase = new CompleteDriverOnboardingUseCase(
driverRepository,
mockLogger
);
});
beforeEach(async () => {
await driverRepository.clear();
});
describe('CompleteDriverOnboardingUseCase - Validation Scenarios', () => {
it('should validate that driver does not already exist', async () => {
// Scenario: Duplicate driver validation
// Given: A driver already exists
const userId = 'duplicate-user';
await completeDriverOnboardingUseCase.execute({
userId,
firstName: 'First',
lastName: 'Last',
displayName: 'FirstLast',
country: 'US',
});
// When: Attempting to onboard again
const result = await completeDriverOnboardingUseCase.execute({
userId,
firstName: 'Second',
lastName: 'Attempt',
displayName: 'SecondAttempt',
country: 'US',
});
// Then: Validation should fail
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('DRIVER_ALREADY_EXISTS');
});
});
});

View File

@@ -1,153 +0,0 @@
/**
* Integration Test: Onboarding Wizard Use Case Orchestration
*
* Tests the orchestration logic of onboarding wizard-related Use Cases:
* - CompleteDriverOnboardingUseCase: Orchestrates the driver creation flow
*
* 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, beforeEach } from 'vitest';
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { CompleteDriverOnboardingUseCase } from '../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { Logger } from '../../../core/shared/domain/Logger';
describe('Onboarding Wizard Use Case Orchestration', () => {
let driverRepository: InMemoryDriverRepository;
let completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase;
let mockLogger: Logger;
beforeAll(() => {
mockLogger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
driverRepository = new InMemoryDriverRepository(mockLogger);
completeDriverOnboardingUseCase = new CompleteDriverOnboardingUseCase(
driverRepository,
mockLogger
);
});
beforeEach(async () => {
await driverRepository.clear();
});
describe('CompleteDriverOnboardingUseCase - Success Path', () => {
it('should complete onboarding with valid personal info', async () => {
// Scenario: Complete onboarding successfully
// Given: A new user ID
const userId = 'user-123';
const input = {
userId,
firstName: 'John',
lastName: 'Doe',
displayName: 'RacerJohn',
country: 'US',
bio: 'New racer on the grid',
};
// When: CompleteDriverOnboardingUseCase.execute() is called
const result = await completeDriverOnboardingUseCase.execute(input);
// Then: Driver should be created
expect(result.isOk()).toBe(true);
const { driver } = result.unwrap();
expect(driver.id).toBe(userId);
expect(driver.name.toString()).toBe('RacerJohn');
expect(driver.country.toString()).toBe('US');
expect(driver.bio?.toString()).toBe('New racer on the grid');
// And: Repository should contain the driver
const savedDriver = await driverRepository.findById(userId);
expect(savedDriver).not.toBeNull();
expect(savedDriver?.id).toBe(userId);
});
it('should complete onboarding with minimal required data', async () => {
// Scenario: Complete onboarding with minimal data
// Given: A new user ID
const userId = 'user-456';
const input = {
userId,
firstName: 'Jane',
lastName: 'Smith',
displayName: 'JaneS',
country: 'UK',
};
// When: CompleteDriverOnboardingUseCase.execute() is called
const result = await completeDriverOnboardingUseCase.execute(input);
// Then: Driver should be created successfully
expect(result.isOk()).toBe(true);
const { driver } = result.unwrap();
expect(driver.id).toBe(userId);
expect(driver.bio).toBeUndefined();
});
});
describe('CompleteDriverOnboardingUseCase - Validation & Errors', () => {
it('should reject onboarding if driver already exists', async () => {
// Scenario: Already onboarded user
// Given: A driver already exists for the user
const userId = 'existing-user';
const existingInput = {
userId,
firstName: 'Old',
lastName: 'Name',
displayName: 'OldRacer',
country: 'DE',
};
await completeDriverOnboardingUseCase.execute(existingInput);
// When: CompleteDriverOnboardingUseCase.execute() is called again for same user
const result = await completeDriverOnboardingUseCase.execute({
userId,
firstName: 'New',
lastName: 'Name',
displayName: 'NewRacer',
country: 'FR',
});
// Then: Should return DRIVER_ALREADY_EXISTS error
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('DRIVER_ALREADY_EXISTS');
});
it('should handle repository errors gracefully', async () => {
// Scenario: Repository error
// Given: Repository throws an error
const userId = 'error-user';
const originalCreate = driverRepository.create.bind(driverRepository);
driverRepository.create = async () => {
throw new Error('Database failure');
};
// When: CompleteDriverOnboardingUseCase.execute() is called
const result = await completeDriverOnboardingUseCase.execute({
userId,
firstName: 'John',
lastName: 'Doe',
displayName: 'RacerJohn',
country: 'US',
});
// Then: Should return REPOSITORY_ERROR
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details.message).toBe('Database failure');
// Restore
driverRepository.create = originalCreate;
});
});
});

View File

@@ -0,0 +1,78 @@
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 { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { InMemoryResultRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryResultRepository';
import { InMemoryStandingRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { Logger } from '../../../core/shared/domain/Logger';
export class ProfileTestContext {
public readonly driverRepository: InMemoryDriverRepository;
public readonly teamRepository: InMemoryTeamRepository;
public readonly teamMembershipRepository: InMemoryTeamMembershipRepository;
public readonly socialRepository: InMemorySocialGraphRepository;
public readonly driverExtendedProfileProvider: InMemoryDriverExtendedProfileProvider;
public readonly driverStatsRepository: InMemoryDriverStatsRepository;
public readonly liveryRepository: InMemoryLiveryRepository;
public readonly leagueRepository: InMemoryLeagueRepository;
public readonly leagueMembershipRepository: InMemoryLeagueMembershipRepository;
public readonly sponsorshipRequestRepository: InMemorySponsorshipRequestRepository;
public readonly sponsorRepository: InMemorySponsorRepository;
public readonly eventPublisher: InMemoryEventPublisher;
public readonly resultRepository: InMemoryResultRepository;
public readonly standingRepository: InMemoryStandingRepository;
public readonly raceRepository: InMemoryRaceRepository;
public readonly logger: Logger;
constructor() {
this.logger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
this.driverRepository = new InMemoryDriverRepository(this.logger);
this.teamRepository = new InMemoryTeamRepository(this.logger);
this.teamMembershipRepository = new InMemoryTeamMembershipRepository(this.logger);
this.socialRepository = new InMemorySocialGraphRepository(this.logger);
this.driverExtendedProfileProvider = new InMemoryDriverExtendedProfileProvider(this.logger);
this.driverStatsRepository = new InMemoryDriverStatsRepository(this.logger);
this.liveryRepository = new InMemoryLiveryRepository(this.logger);
this.leagueRepository = new InMemoryLeagueRepository(this.logger);
this.leagueMembershipRepository = new InMemoryLeagueMembershipRepository(this.logger);
this.sponsorshipRequestRepository = new InMemorySponsorshipRequestRepository(this.logger);
this.sponsorRepository = new InMemorySponsorRepository(this.logger);
this.eventPublisher = new InMemoryEventPublisher();
this.raceRepository = new InMemoryRaceRepository(this.logger);
this.resultRepository = new InMemoryResultRepository(this.logger, this.raceRepository);
this.standingRepository = new InMemoryStandingRepository(this.logger, {}, this.resultRepository, this.raceRepository);
}
public async clear(): Promise<void> {
await this.driverRepository.clear();
await this.teamRepository.clear();
await this.teamMembershipRepository.clear();
await this.socialRepository.clear();
await this.driverExtendedProfileProvider.clear();
await this.driverStatsRepository.clear();
await this.liveryRepository.clear();
await this.leagueRepository.clear();
await this.leagueMembershipRepository.clear();
await this.sponsorshipRequestRepository.clear();
await this.sponsorRepository.clear();
this.eventPublisher.clear();
await this.raceRepository.clear();
await this.resultRepository.clear();
await this.standingRepository.clear();
}
}

View File

@@ -1,556 +0,0 @@
/**
* Integration Test: Profile Leagues Use Case Orchestration
*
* Tests the orchestration logic of profile leagues-related Use Cases:
* - GetProfileLeaguesUseCase: Retrieves driver's league memberships
* - LeaveLeagueUseCase: Allows driver to leave a league from profile
* - GetLeagueDetailsUseCase: Retrieves league details from profile
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryLeagueRepository } from '../../../adapters/leagues/persistence/inmemory/InMemoryLeagueRepository';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { GetProfileLeaguesUseCase } from '../../../core/profile/use-cases/GetProfileLeaguesUseCase';
import { LeaveLeagueUseCase } from '../../../core/leagues/use-cases/LeaveLeagueUseCase';
import { GetLeagueDetailsUseCase } from '../../../core/leagues/use-cases/GetLeagueDetailsUseCase';
import { ProfileLeaguesQuery } from '../../../core/profile/ports/ProfileLeaguesQuery';
import { LeaveLeagueCommand } from '../../../core/leagues/ports/LeaveLeagueCommand';
import { LeagueDetailsQuery } from '../../../core/leagues/ports/LeagueDetailsQuery';
describe('Profile Leagues Use Case Orchestration', () => {
let driverRepository: InMemoryDriverRepository;
let leagueRepository: InMemoryLeagueRepository;
let eventPublisher: InMemoryEventPublisher;
let getProfileLeaguesUseCase: GetProfileLeaguesUseCase;
let leaveLeagueUseCase: LeaveLeagueUseCase;
let getLeagueDetailsUseCase: GetLeagueDetailsUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// driverRepository = new InMemoryDriverRepository();
// leagueRepository = new InMemoryLeagueRepository();
// eventPublisher = new InMemoryEventPublisher();
// getProfileLeaguesUseCase = new GetProfileLeaguesUseCase({
// driverRepository,
// leagueRepository,
// eventPublisher,
// });
// leaveLeagueUseCase = new LeaveLeagueUseCase({
// driverRepository,
// leagueRepository,
// eventPublisher,
// });
// getLeagueDetailsUseCase = new GetLeagueDetailsUseCase({
// leagueRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// driverRepository.clear();
// leagueRepository.clear();
// eventPublisher.clear();
});
describe('GetProfileLeaguesUseCase - Success Path', () => {
it('should retrieve complete list of league memberships', async () => {
// TODO: Implement test
// Scenario: Driver with multiple league memberships
// Given: A driver exists
// And: The driver is a member of 3 leagues
// And: Each league has different status (Active/Inactive)
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should contain all league memberships
// And: Each league should display name, status, and upcoming races
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with minimal data', async () => {
// TODO: Implement test
// Scenario: Driver with minimal league memberships
// Given: A driver exists
// And: The driver is a member of 1 league
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should contain the league membership
// And: The league should display basic information
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with upcoming races', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having upcoming races
// Given: A driver exists
// And: The driver is a member of a league with upcoming races
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show upcoming races for the league
// And: Each race should display track name, date, and time
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league status', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having different statuses
// Given: A driver exists
// And: The driver is a member of an active league
// And: The driver is a member of an inactive league
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show status for each league
// And: Active leagues should be clearly marked
// And: Inactive leagues should be clearly marked
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with member count', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having member counts
// Given: A driver exists
// And: The driver is a member of a league with 50 members
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show member count for the league
// And: The count should be accurate
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with driver role', async () => {
// TODO: Implement test
// Scenario: Driver with different roles in leagues
// Given: A driver exists
// And: The driver is a member of a league as "Member"
// And: The driver is an admin of another league
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show role for each league
// And: The role should be clearly indicated
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league category tags', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having category tags
// Given: A driver exists
// And: The driver is a member of a league with category tags
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show category tags for the league
// And: Tags should include game type, skill level, etc.
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league rating', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having ratings
// Given: A driver exists
// And: The driver is a member of a league with average rating
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show rating for the league
// And: The rating should be displayed as stars or numeric value
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league prize pool', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having prize pools
// Given: A driver exists
// And: The driver is a member of a league with prize pool
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show prize pool for the league
// And: The prize pool should be displayed as currency amount
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league sponsor count', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having sponsors
// Given: A driver exists
// And: The driver is a member of a league with sponsors
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show sponsor count for the league
// And: The count should be accurate
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league race count', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having races
// Given: A driver exists
// And: The driver is a member of a league with races
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show race count for the league
// And: The count should be accurate
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league championship count', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having championships
// Given: A driver exists
// And: The driver is a member of a league with championships
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show championship count for the league
// And: The count should be accurate
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league visibility', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having different visibility
// Given: A driver exists
// And: The driver is a member of a public league
// And: The driver is a member of a private league
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show visibility for each league
// And: The visibility should be clearly indicated
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league creation date', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having creation dates
// Given: A driver exists
// And: The driver is a member of a league created on a specific date
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show creation date for the league
// And: The date should be formatted correctly
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should retrieve league memberships with league owner information', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having owners
// Given: A driver exists
// And: The driver is a member of a league with an owner
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should show owner name for the league
// And: The owner name should be clickable to view profile
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
});
describe('GetProfileLeaguesUseCase - Edge Cases', () => {
it('should handle driver with no league memberships', async () => {
// TODO: Implement test
// Scenario: Driver without league memberships
// Given: A driver exists without league memberships
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should contain empty list
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should handle driver with only active leagues', async () => {
// TODO: Implement test
// Scenario: Driver with only active leagues
// Given: A driver exists
// And: The driver is a member of only active leagues
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should contain only active leagues
// And: All leagues should show Active status
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should handle driver with only inactive leagues', async () => {
// TODO: Implement test
// Scenario: Driver with only inactive leagues
// Given: A driver exists
// And: The driver is a member of only inactive leagues
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should contain only inactive leagues
// And: All leagues should show Inactive status
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should handle driver with leagues having no upcoming races', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having no upcoming races
// Given: A driver exists
// And: The driver is a member of leagues with no upcoming races
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should contain league memberships
// And: Upcoming races section should be empty
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
it('should handle driver with leagues having no sponsors', async () => {
// TODO: Implement test
// Scenario: Driver with leagues having no sponsors
// Given: A driver exists
// And: The driver is a member of leagues with no sponsors
// When: GetProfileLeaguesUseCase.execute() is called with driver ID
// Then: The result should contain league memberships
// And: Sponsor count should be zero
// And: EventPublisher should emit ProfileLeaguesAccessedEvent
});
});
describe('GetProfileLeaguesUseCase - 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
// When: GetProfileLeaguesUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: GetProfileLeaguesUseCase.execute() is called with invalid driver 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 driver exists
// And: DriverRepository throws an error during query
// When: GetProfileLeaguesUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('LeaveLeagueUseCase - Success Path', () => {
it('should allow driver to leave a league', async () => {
// TODO: Implement test
// Scenario: Driver leaves a league
// Given: A driver exists
// And: The driver is a member of a league
// When: LeaveLeagueUseCase.execute() is called with driver ID and league ID
// Then: The driver should be removed from the league roster
// And: EventPublisher should emit LeagueLeftEvent
});
it('should allow driver to leave multiple leagues', async () => {
// TODO: Implement test
// Scenario: Driver leaves multiple leagues
// Given: A driver exists
// And: The driver is a member of 3 leagues
// When: LeaveLeagueUseCase.execute() is called for each league
// Then: The driver should be removed from all league rosters
// And: EventPublisher should emit LeagueLeftEvent for each league
});
it('should allow admin to leave league', async () => {
// TODO: Implement test
// Scenario: Admin leaves a league
// Given: A driver exists as admin of a league
// When: LeaveLeagueUseCase.execute() is called with admin driver ID and league ID
// Then: The admin should be removed from the league roster
// And: EventPublisher should emit LeagueLeftEvent
});
it('should allow owner to leave league', async () => {
// TODO: Implement test
// Scenario: Owner leaves a league
// Given: A driver exists as owner of a league
// When: LeaveLeagueUseCase.execute() is called with owner driver ID and league ID
// Then: The owner should be removed from the league roster
// And: EventPublisher should emit LeagueLeftEvent
});
});
describe('LeaveLeagueUseCase - Validation', () => {
it('should reject leaving league when driver is not a member', async () => {
// TODO: Implement test
// Scenario: Driver not a member of league
// Given: A driver exists
// And: The driver is not a member of a league
// When: LeaveLeagueUseCase.execute() is called with driver ID and league ID
// Then: Should throw NotMemberError
// And: EventPublisher should NOT emit any events
});
it('should reject leaving league with invalid league ID', async () => {
// TODO: Implement test
// Scenario: Invalid league ID
// Given: A driver exists
// When: LeaveLeagueUseCase.execute() is called with invalid league ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('LeaveLeagueUseCase - 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
// When: LeaveLeagueUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// 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 driver exists
// And: No league exists with the given ID
// When: LeaveLeagueUseCase.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 driver exists
// And: LeagueRepository throws an error during update
// When: LeaveLeagueUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('GetLeagueDetailsUseCase - Success Path', () => {
it('should retrieve complete league details', async () => {
// TODO: Implement test
// Scenario: League with complete details
// Given: A league exists with complete information
// And: The league has name, status, members, races, championships
// When: GetLeagueDetailsUseCase.execute() is called with league ID
// Then: The result should contain all league details
// And: EventPublisher should emit LeagueDetailsAccessedEvent
});
it('should retrieve league details with minimal information', async () => {
// TODO: Implement test
// Scenario: League with minimal details
// Given: A league exists with minimal information
// And: The league has only name and status
// When: GetLeagueDetailsUseCase.execute() is called with league ID
// Then: The result should contain basic league details
// And: EventPublisher should emit LeagueDetailsAccessedEvent
});
it('should retrieve league details with upcoming races', async () => {
// TODO: Implement test
// Scenario: League with upcoming races
// Given: A league exists with upcoming races
// When: GetLeagueDetailsUseCase.execute() is called with league ID
// Then: The result should show upcoming races
// And: Each race should display track name, date, and time
// And: EventPublisher should emit LeagueDetailsAccessedEvent
});
it('should retrieve league details with member list', async () => {
// TODO: Implement test
// Scenario: League with member list
// Given: A league exists with members
// When: GetLeagueDetailsUseCase.execute() is called with league ID
// Then: The result should show member list
// And: Each member should display name and role
// And: EventPublisher should emit LeagueDetailsAccessedEvent
});
});
describe('GetLeagueDetailsUseCase - 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: GetLeagueDetailsUseCase.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: GetLeagueDetailsUseCase.execute() is called with invalid league ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('Profile Leagues Data Orchestration', () => {
it('should correctly format league status with visual cues', async () => {
// TODO: Implement test
// Scenario: League status formatting
// Given: A driver exists
// And: The driver is a member of an active league
// And: The driver is a member of an inactive league
// When: GetProfileLeaguesUseCase.execute() is called
// Then: Active leagues should show "Active" status with green indicator
// And: Inactive leagues should show "Inactive" status with gray indicator
});
it('should correctly format upcoming races with proper details', async () => {
// TODO: Implement test
// Scenario: Upcoming races formatting
// Given: A driver exists
// And: The driver is a member of a league with upcoming races
// When: GetProfileLeaguesUseCase.execute() is called
// Then: Upcoming races should show:
// - Track name
// - Race date and time (formatted correctly)
// - Race type (if available)
});
it('should correctly format league rating with stars or numeric value', async () => {
// TODO: Implement test
// Scenario: League rating formatting
// Given: A driver exists
// And: The driver is a member of a league with rating 4.5
// When: GetProfileLeaguesUseCase.execute() is called
// Then: League rating should show as stars (4.5/5) or numeric value (4.5)
});
it('should correctly format league prize pool as currency', async () => {
// TODO: Implement test
// Scenario: League prize pool formatting
// Given: A driver exists
// And: The driver is a member of a league with prize pool $1000
// When: GetProfileLeaguesUseCase.execute() is called
// Then: League prize pool should show as "$1,000" or "1000 USD"
});
it('should correctly format league creation date', async () => {
// TODO: Implement test
// Scenario: League creation date formatting
// Given: A driver exists
// And: The driver is a member of a league created on 2024-01-15
// When: GetProfileLeaguesUseCase.execute() is called
// Then: League creation date should show as "January 15, 2024" or similar format
});
it('should correctly identify driver role in each league', async () => {
// TODO: Implement test
// Scenario: Driver role identification
// Given: A driver exists
// And: The driver is a member of League A as "Member"
// And: The driver is an admin of League B
// And: The driver is the owner of League C
// When: GetProfileLeaguesUseCase.execute() is called
// Then: League A should show role "Member"
// And: League B should show role "Admin"
// And: League C should show role "Owner"
});
it('should correctly filter leagues by status', async () => {
// TODO: Implement test
// Scenario: League filtering by status
// Given: A driver exists
// And: The driver is a member of 2 active leagues and 1 inactive league
// When: GetProfileLeaguesUseCase.execute() is called with status filter "Active"
// Then: The result should show only the 2 active leagues
// And: The inactive league should be hidden
});
it('should correctly search leagues by name', async () => {
// TODO: Implement test
// Scenario: League search by name
// Given: A driver exists
// And: The driver is a member of "European GT League" and "Formula League"
// When: GetProfileLeaguesUseCase.execute() is called with search term "European"
// Then: The result should show only "European GT League"
// And: "Formula League" should be hidden
});
});
});

View File

@@ -1,518 +0,0 @@
/**
* Integration Test: Profile Liveries Use Case Orchestration
*
* Tests the orchestration logic of profile liveries-related Use Cases:
* - GetProfileLiveriesUseCase: Retrieves driver's uploaded liveries
* - GetLiveryDetailsUseCase: Retrieves livery details
* - DeleteLiveryUseCase: Deletes a livery
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryLiveryRepository } from '../../../adapters/media/persistence/inmemory/InMemoryLiveryRepository';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { GetProfileLiveriesUseCase } from '../../../core/profile/use-cases/GetProfileLiveriesUseCase';
import { GetLiveryDetailsUseCase } from '../../../core/media/use-cases/GetLiveryDetailsUseCase';
import { DeleteLiveryUseCase } from '../../../core/media/use-cases/DeleteLiveryUseCase';
import { ProfileLiveriesQuery } from '../../../core/profile/ports/ProfileLiveriesQuery';
import { LiveryDetailsQuery } from '../../../core/media/ports/LiveryDetailsQuery';
import { DeleteLiveryCommand } from '../../../core/media/ports/DeleteLiveryCommand';
describe('Profile Liveries Use Case Orchestration', () => {
let driverRepository: InMemoryDriverRepository;
let liveryRepository: InMemoryLiveryRepository;
let eventPublisher: InMemoryEventPublisher;
let getProfileLiveriesUseCase: GetProfileLiveriesUseCase;
let getLiveryDetailsUseCase: GetLiveryDetailsUseCase;
let deleteLiveryUseCase: DeleteLiveryUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// driverRepository = new InMemoryDriverRepository();
// liveryRepository = new InMemoryLiveryRepository();
// eventPublisher = new InMemoryEventPublisher();
// getProfileLiveriesUseCase = new GetProfileLiveriesUseCase({
// driverRepository,
// liveryRepository,
// eventPublisher,
// });
// getLiveryDetailsUseCase = new GetLiveryDetailsUseCase({
// liveryRepository,
// eventPublisher,
// });
// deleteLiveryUseCase = new DeleteLiveryUseCase({
// driverRepository,
// liveryRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// driverRepository.clear();
// liveryRepository.clear();
// eventPublisher.clear();
});
describe('GetProfileLiveriesUseCase - Success Path', () => {
it('should retrieve complete list of uploaded liveries', async () => {
// TODO: Implement test
// Scenario: Driver with multiple liveries
// Given: A driver exists
// And: The driver has uploaded 3 liveries
// And: Each livery has different validation status (Validated/Pending)
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should contain all liveries
// And: Each livery should display car name, thumbnail, and validation status
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with minimal data', async () => {
// TODO: Implement test
// Scenario: Driver with minimal liveries
// Given: A driver exists
// And: The driver has uploaded 1 livery
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should contain the livery
// And: The livery should display basic information
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with validation status', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having different validation statuses
// Given: A driver exists
// And: The driver has a validated livery
// And: The driver has a pending livery
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should show validation status for each livery
// And: Validated liveries should be clearly marked
// And: Pending liveries should be clearly marked
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with upload date', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having upload dates
// Given: A driver exists
// And: The driver has liveries uploaded on different dates
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should show upload date for each livery
// And: The date should be formatted correctly
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with car name', async () => {
// TODO: Implement test
// Scenario: Driver with liveries for different cars
// Given: A driver exists
// And: The driver has liveries for Porsche 911 GT3, Ferrari 488, etc.
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should show car name for each livery
// And: The car name should be accurate
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with car ID', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having car IDs
// Given: A driver exists
// And: The driver has liveries with car IDs
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should show car ID for each livery
// And: The car ID should be accurate
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with livery preview', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having previews
// Given: A driver exists
// And: The driver has liveries with preview images
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should show preview image for each livery
// And: The preview should be accessible
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with file metadata', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having file metadata
// Given: A driver exists
// And: The driver has liveries with file size, format, etc.
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should show file metadata for each livery
// And: Metadata should include file size, format, and upload date
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with file size', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having file sizes
// Given: A driver exists
// And: The driver has liveries with different file sizes
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should show file size for each livery
// And: The file size should be formatted correctly (e.g., MB, KB)
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with file format', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having different file formats
// Given: A driver exists
// And: The driver has liveries in PNG, DDS, etc. formats
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should show file format for each livery
// And: The format should be clearly indicated
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should retrieve liveries with error state', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having error state
// Given: A driver exists
// And: The driver has a livery that failed to load
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should show error state for the livery
// And: The livery should show error placeholder
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
});
describe('GetProfileLiveriesUseCase - Edge Cases', () => {
it('should handle driver with no liveries', async () => {
// TODO: Implement test
// Scenario: Driver without liveries
// Given: A driver exists without liveries
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should contain empty list
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should handle driver with only validated liveries', async () => {
// TODO: Implement test
// Scenario: Driver with only validated liveries
// Given: A driver exists
// And: The driver has only validated liveries
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should contain only validated liveries
// And: All liveries should show Validated status
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should handle driver with only pending liveries', async () => {
// TODO: Implement test
// Scenario: Driver with only pending liveries
// Given: A driver exists
// And: The driver has only pending liveries
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should contain only pending liveries
// And: All liveries should show Pending status
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should handle driver with liveries having no preview', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having no preview
// Given: A driver exists
// And: The driver has liveries without preview images
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should contain liveries
// And: Preview section should show placeholder
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
it('should handle driver with liveries having no metadata', async () => {
// TODO: Implement test
// Scenario: Driver with liveries having no metadata
// Given: A driver exists
// And: The driver has liveries without file metadata
// When: GetProfileLiveriesUseCase.execute() is called with driver ID
// Then: The result should contain liveries
// And: Metadata section should be empty
// And: EventPublisher should emit ProfileLiveriesAccessedEvent
});
});
describe('GetProfileLiveriesUseCase - 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
// When: GetProfileLiveriesUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: GetProfileLiveriesUseCase.execute() is called with invalid driver 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 driver exists
// And: DriverRepository throws an error during query
// When: GetProfileLiveriesUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('GetLiveryDetailsUseCase - Success Path', () => {
it('should retrieve complete livery details', async () => {
// TODO: Implement test
// Scenario: Livery with complete details
// Given: A livery exists with complete information
// And: The livery has car name, car ID, validation status, upload date
// And: The livery has file size, format, preview
// When: GetLiveryDetailsUseCase.execute() is called with livery ID
// Then: The result should contain all livery details
// And: EventPublisher should emit LiveryDetailsAccessedEvent
});
it('should retrieve livery details with minimal information', async () => {
// TODO: Implement test
// Scenario: Livery with minimal details
// Given: A livery exists with minimal information
// And: The livery has only car name and validation status
// When: GetLiveryDetailsUseCase.execute() is called with livery ID
// Then: The result should contain basic livery details
// And: EventPublisher should emit LiveryDetailsAccessedEvent
});
it('should retrieve livery details with validation status', async () => {
// TODO: Implement test
// Scenario: Livery with validation status
// Given: A livery exists with validation status
// When: GetLiveryDetailsUseCase.execute() is called with livery ID
// Then: The result should show validation status
// And: The status should be clearly indicated
// And: EventPublisher should emit LiveryDetailsAccessedEvent
});
it('should retrieve livery details with file metadata', async () => {
// TODO: Implement test
// Scenario: Livery with file metadata
// Given: A livery exists with file metadata
// When: GetLiveryDetailsUseCase.execute() is called with livery ID
// Then: The result should show file metadata
// And: Metadata should include file size, format, and upload date
// And: EventPublisher should emit LiveryDetailsAccessedEvent
});
it('should retrieve livery details with preview', async () => {
// TODO: Implement test
// Scenario: Livery with preview
// Given: A livery exists with preview image
// When: GetLiveryDetailsUseCase.execute() is called with livery ID
// Then: The result should show preview image
// And: The preview should be accessible
// And: EventPublisher should emit LiveryDetailsAccessedEvent
});
});
describe('GetLiveryDetailsUseCase - Error Handling', () => {
it('should throw error when livery does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent livery
// Given: No livery exists with the given ID
// When: GetLiveryDetailsUseCase.execute() is called with non-existent livery ID
// Then: Should throw LiveryNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when livery ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid livery ID
// Given: An invalid livery ID (e.g., empty string, null, undefined)
// When: GetLiveryDetailsUseCase.execute() is called with invalid livery ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('DeleteLiveryUseCase - Success Path', () => {
it('should allow driver to delete a livery', async () => {
// TODO: Implement test
// Scenario: Driver deletes a livery
// Given: A driver exists
// And: The driver has uploaded a livery
// When: DeleteLiveryUseCase.execute() is called with driver ID and livery ID
// Then: The livery should be removed from the driver's list
// And: EventPublisher should emit LiveryDeletedEvent
});
it('should allow driver to delete multiple liveries', async () => {
// TODO: Implement test
// Scenario: Driver deletes multiple liveries
// Given: A driver exists
// And: The driver has uploaded 3 liveries
// When: DeleteLiveryUseCase.execute() is called for each livery
// Then: All liveries should be removed from the driver's list
// And: EventPublisher should emit LiveryDeletedEvent for each livery
});
it('should allow driver to delete validated livery', async () => {
// TODO: Implement test
// Scenario: Driver deletes validated livery
// Given: A driver exists
// And: The driver has a validated livery
// When: DeleteLiveryUseCase.execute() is called with driver ID and livery ID
// Then: The validated livery should be removed
// And: EventPublisher should emit LiveryDeletedEvent
});
it('should allow driver to delete pending livery', async () => {
// TODO: Implement test
// Scenario: Driver deletes pending livery
// Given: A driver exists
// And: The driver has a pending livery
// When: DeleteLiveryUseCase.execute() is called with driver ID and livery ID
// Then: The pending livery should be removed
// And: EventPublisher should emit LiveryDeletedEvent
});
});
describe('DeleteLiveryUseCase - Validation', () => {
it('should reject deleting livery when driver is not owner', async () => {
// TODO: Implement test
// Scenario: Driver not owner of livery
// Given: A driver exists
// And: The driver is not the owner of a livery
// When: DeleteLiveryUseCase.execute() is called with driver ID and livery ID
// Then: Should throw NotOwnerError
// And: EventPublisher should NOT emit any events
});
it('should reject deleting livery with invalid livery ID', async () => {
// TODO: Implement test
// Scenario: Invalid livery ID
// Given: A driver exists
// When: DeleteLiveryUseCase.execute() is called with invalid livery ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('DeleteLiveryUseCase - 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
// When: DeleteLiveryUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when livery does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent livery
// Given: A driver exists
// And: No livery exists with the given ID
// When: DeleteLiveryUseCase.execute() is called with non-existent livery ID
// Then: Should throw LiveryNotFoundError
// 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: LiveryRepository throws an error during delete
// When: DeleteLiveryUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('Profile Liveries Data Orchestration', () => {
it('should correctly format validation status with visual cues', async () => {
// TODO: Implement test
// Scenario: Livery validation status formatting
// Given: A driver exists
// And: The driver has a validated livery
// And: The driver has a pending livery
// When: GetProfileLiveriesUseCase.execute() is called
// Then: Validated liveries should show "Validated" status with green indicator
// And: Pending liveries should show "Pending" status with yellow indicator
});
it('should correctly format upload date', async () => {
// TODO: Implement test
// Scenario: Livery upload date formatting
// Given: A driver exists
// And: The driver has a livery uploaded on 2024-01-15
// When: GetProfileLiveriesUseCase.execute() is called
// Then: Upload date should show as "January 15, 2024" or similar format
});
it('should correctly format file size', async () => {
// TODO: Implement test
// Scenario: Livery file size formatting
// Given: A driver exists
// And: The driver has a livery with file size 5242880 bytes (5 MB)
// When: GetProfileLiveriesUseCase.execute() is called
// Then: File size should show as "5 MB" or "5.0 MB"
});
it('should correctly format file format', async () => {
// TODO: Implement test
// Scenario: Livery file format formatting
// Given: A driver exists
// And: The driver has liveries in PNG and DDS formats
// When: GetProfileLiveriesUseCase.execute() is called
// Then: File format should show as "PNG" or "DDS"
});
it('should correctly filter liveries by validation status', async () => {
// TODO: Implement test
// Scenario: Livery filtering by validation status
// Given: A driver exists
// And: The driver has 2 validated liveries and 1 pending livery
// When: GetProfileLiveriesUseCase.execute() is called with status filter "Validated"
// Then: The result should show only the 2 validated liveries
// And: The pending livery should be hidden
});
it('should correctly search liveries by car name', async () => {
// TODO: Implement test
// Scenario: Livery search by car name
// Given: A driver exists
// And: The driver has liveries for "Porsche 911 GT3" and "Ferrari 488"
// When: GetProfileLiveriesUseCase.execute() is called with search term "Porsche"
// Then: The result should show only "Porsche 911 GT3" livery
// And: "Ferrari 488" livery should be hidden
});
it('should correctly identify livery owner', async () => {
// TODO: Implement test
// Scenario: Livery owner identification
// Given: A driver exists
// And: The driver has uploaded a livery
// When: GetProfileLiveriesUseCase.execute() is called
// Then: The livery should be associated with the driver
// And: The driver should be able to delete the livery
});
it('should correctly handle livery error state', async () => {
// TODO: Implement test
// Scenario: Livery error state handling
// Given: A driver exists
// And: The driver has a livery that failed to load
// When: GetProfileLiveriesUseCase.execute() is called
// Then: The livery should show error state
// And: The livery should show retry option
});
});
});

View File

@@ -1,654 +0,0 @@
/**
* Integration Test: Profile Main Use Case Orchestration
*
* Tests the orchestration logic of profile-related Use Cases:
* - GetProfileUseCase: Retrieves driver's profile information
* - GetProfileStatisticsUseCase: Retrieves driver's statistics and achievements
* - GetProfileCompletionUseCase: Calculates profile completion percentage
* - UpdateProfileUseCase: Updates driver's profile information
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { GetProfileUseCase } from '../../../core/profile/use-cases/GetProfileUseCase';
import { GetProfileStatisticsUseCase } from '../../../core/profile/use-cases/GetProfileStatisticsUseCase';
import { GetProfileCompletionUseCase } from '../../../core/profile/use-cases/GetProfileCompletionUseCase';
import { UpdateProfileUseCase } from '../../../core/profile/use-cases/UpdateProfileUseCase';
import { ProfileQuery } from '../../../core/profile/ports/ProfileQuery';
import { ProfileStatisticsQuery } from '../../../core/profile/ports/ProfileStatisticsQuery';
import { ProfileCompletionQuery } from '../../../core/profile/ports/ProfileCompletionQuery';
import { UpdateProfileCommand } from '../../../core/profile/ports/UpdateProfileCommand';
describe('Profile Main Use Case Orchestration', () => {
let driverRepository: InMemoryDriverRepository;
let eventPublisher: InMemoryEventPublisher;
let getProfileUseCase: GetProfileUseCase;
let getProfileStatisticsUseCase: GetProfileStatisticsUseCase;
let getProfileCompletionUseCase: GetProfileCompletionUseCase;
let updateProfileUseCase: UpdateProfileUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// driverRepository = new InMemoryDriverRepository();
// eventPublisher = new InMemoryEventPublisher();
// getProfileUseCase = new GetProfileUseCase({
// driverRepository,
// eventPublisher,
// });
// getProfileStatisticsUseCase = new GetProfileStatisticsUseCase({
// driverRepository,
// eventPublisher,
// });
// getProfileCompletionUseCase = new GetProfileCompletionUseCase({
// driverRepository,
// eventPublisher,
// });
// updateProfileUseCase = new UpdateProfileUseCase({
// driverRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// driverRepository.clear();
// eventPublisher.clear();
});
describe('GetProfileUseCase - Success Path', () => {
it('should retrieve complete driver profile with all personal information', async () => {
// TODO: Implement test
// Scenario: Driver with complete profile
// Given: A driver exists with complete personal information
// And: The driver has name, email, avatar, bio, location
// And: The driver has social links configured
// And: The driver has team affiliation
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain all driver information
// And: The result should display name, email, avatar, bio, location
// And: The result should display social links
// And: The result should display team affiliation
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should retrieve driver profile with minimal information', async () => {
// TODO: Implement test
// Scenario: Driver with minimal profile
// Given: A driver exists with minimal information
// And: The driver has only name and email
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain basic driver information
// And: The result should display name and email
// And: The result should show empty values for optional fields
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should retrieve driver profile with avatar', async () => {
// TODO: Implement test
// Scenario: Driver with avatar
// Given: A driver exists with an avatar
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain avatar URL
// And: The avatar should be accessible
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should retrieve driver profile with social links', async () => {
// TODO: Implement test
// Scenario: Driver with social links
// Given: A driver exists with social links
// And: The driver has Discord, Twitter, iRacing links
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain social links
// And: Each link should have correct URL format
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should retrieve driver profile with team affiliation', async () => {
// TODO: Implement test
// Scenario: Driver with team affiliation
// Given: A driver exists with team affiliation
// And: The driver is affiliated with Team XYZ
// And: The driver has role "Driver"
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain team information
// And: The result should show team name and logo
// And: The result should show driver role
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should retrieve driver profile with bio', async () => {
// TODO: Implement test
// Scenario: Driver with bio
// Given: A driver exists with a bio
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain bio text
// And: The bio should be displayed correctly
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should retrieve driver profile with location', async () => {
// TODO: Implement test
// Scenario: Driver with location
// Given: A driver exists with location
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain location
// And: The location should be displayed correctly
// And: EventPublisher should emit ProfileAccessedEvent
});
});
describe('GetProfileUseCase - Edge Cases', () => {
it('should handle driver with no avatar', async () => {
// TODO: Implement test
// Scenario: Driver without avatar
// Given: A driver exists without avatar
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain driver information
// And: The result should show default avatar or placeholder
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should handle driver with no social links', async () => {
// TODO: Implement test
// Scenario: Driver without social links
// Given: A driver exists without social links
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain driver information
// And: The result should show empty social links section
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should handle driver with no team affiliation', async () => {
// TODO: Implement test
// Scenario: Driver without team affiliation
// Given: A driver exists without team affiliation
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain driver information
// And: The result should show empty team section
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should handle driver with no bio', async () => {
// TODO: Implement test
// Scenario: Driver without bio
// Given: A driver exists without bio
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain driver information
// And: The result should show empty bio section
// And: EventPublisher should emit ProfileAccessedEvent
});
it('should handle driver with no location', async () => {
// TODO: Implement test
// Scenario: Driver without location
// Given: A driver exists without location
// When: GetProfileUseCase.execute() is called with driver ID
// Then: The result should contain driver information
// And: The result should show empty location section
// And: EventPublisher should emit ProfileAccessedEvent
});
});
describe('GetProfileUseCase - 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
// When: GetProfileUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: GetProfileUseCase.execute() is called with invalid driver 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 driver exists
// And: DriverRepository throws an error during query
// When: GetProfileUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('GetProfileStatisticsUseCase - Success Path', () => {
it('should retrieve complete driver statistics', async () => {
// TODO: Implement test
// Scenario: Driver with complete statistics
// Given: A driver exists with complete statistics
// And: The driver has rating, rank, starts, wins, podiums
// And: The driver has win percentage
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should contain all statistics
// And: The result should display rating, rank, starts, wins, podiums
// And: The result should display win percentage
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
it('should retrieve driver statistics with minimal data', async () => {
// TODO: Implement test
// Scenario: Driver with minimal statistics
// Given: A driver exists with minimal statistics
// And: The driver has only rating and rank
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should contain basic statistics
// And: The result should display rating and rank
// And: The result should show zero values for other statistics
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
it('should retrieve driver statistics with win percentage calculation', async () => {
// TODO: Implement test
// Scenario: Driver with win percentage
// Given: A driver exists with 10 starts and 3 wins
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should show win percentage as 30%
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
it('should retrieve driver statistics with podium rate calculation', async () => {
// TODO: Implement test
// Scenario: Driver with podium rate
// Given: A driver exists with 10 starts and 5 podiums
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should show podium rate as 50%
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
it('should retrieve driver statistics with rating trend', async () => {
// TODO: Implement test
// Scenario: Driver with rating trend
// Given: A driver exists with rating trend data
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should show rating trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
it('should retrieve driver statistics with rank trend', async () => {
// TODO: Implement test
// Scenario: Driver with rank trend
// Given: A driver exists with rank trend data
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should show rank trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
it('should retrieve driver statistics with points trend', async () => {
// TODO: Implement test
// Scenario: Driver with points trend
// Given: A driver exists with points trend data
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should show points trend
// And: The trend should show improvement or decline
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
});
describe('GetProfileStatisticsUseCase - Edge Cases', () => {
it('should handle driver with no statistics', async () => {
// TODO: Implement test
// Scenario: Driver without statistics
// Given: A driver exists without statistics
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should contain default statistics
// And: All values should be zero or default
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
it('should handle driver with no race history', async () => {
// TODO: Implement test
// Scenario: Driver without race history
// Given: A driver exists without race history
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should contain statistics with zero values
// And: Win percentage should be 0%
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
it('should handle driver with no trend data', async () => {
// TODO: Implement test
// Scenario: Driver without trend data
// Given: A driver exists without trend data
// When: GetProfileStatisticsUseCase.execute() is called with driver ID
// Then: The result should contain statistics
// And: Trend sections should be empty
// And: EventPublisher should emit ProfileStatisticsAccessedEvent
});
});
describe('GetProfileStatisticsUseCase - 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
// When: GetProfileStatisticsUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: GetProfileStatisticsUseCase.execute() is called with invalid driver ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('GetProfileCompletionUseCase - Success Path', () => {
it('should calculate profile completion for complete profile', async () => {
// TODO: Implement test
// Scenario: Complete profile
// Given: A driver exists with complete profile
// And: The driver has all required fields filled
// And: The driver has avatar, bio, location, social links
// When: GetProfileCompletionUseCase.execute() is called with driver ID
// Then: The result should show 100% completion
// And: The result should show no incomplete sections
// And: EventPublisher should emit ProfileCompletionCalculatedEvent
});
it('should calculate profile completion for partial profile', async () => {
// TODO: Implement test
// Scenario: Partial profile
// Given: A driver exists with partial profile
// And: The driver has name and email only
// And: The driver is missing avatar, bio, location, social links
// When: GetProfileCompletionUseCase.execute() is called with driver ID
// Then: The result should show less than 100% completion
// And: The result should show incomplete sections
// And: EventPublisher should emit ProfileCompletionCalculatedEvent
});
it('should calculate profile completion for minimal profile', async () => {
// TODO: Implement test
// Scenario: Minimal profile
// Given: A driver exists with minimal profile
// And: The driver has only name and email
// When: GetProfileCompletionUseCase.execute() is called with driver ID
// Then: The result should show low completion percentage
// And: The result should show many incomplete sections
// And: EventPublisher should emit ProfileCompletionCalculatedEvent
});
it('should calculate profile completion with suggestions', async () => {
// TODO: Implement test
// Scenario: Profile with suggestions
// Given: A driver exists with partial profile
// When: GetProfileCompletionUseCase.execute() is called with driver ID
// Then: The result should show completion percentage
// And: The result should show suggestions for completion
// And: The result should show which sections are incomplete
// And: EventPublisher should emit ProfileCompletionCalculatedEvent
});
});
describe('GetProfileCompletionUseCase - Edge Cases', () => {
it('should handle driver with no profile data', async () => {
// TODO: Implement test
// Scenario: Driver without profile data
// Given: A driver exists without profile data
// When: GetProfileCompletionUseCase.execute() is called with driver ID
// Then: The result should show 0% completion
// And: The result should show all sections as incomplete
// And: EventPublisher should emit ProfileCompletionCalculatedEvent
});
it('should handle driver with only required fields', async () => {
// TODO: Implement test
// Scenario: Driver with only required fields
// Given: A driver exists with only required fields
// And: The driver has name and email only
// When: GetProfileCompletionUseCase.execute() is called with driver ID
// Then: The result should show partial completion
// And: The result should show required fields as complete
// And: The result should show optional fields as incomplete
// And: EventPublisher should emit ProfileCompletionCalculatedEvent
});
});
describe('GetProfileCompletionUseCase - 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
// When: GetProfileCompletionUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: GetProfileCompletionUseCase.execute() is called with invalid driver ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateProfileUseCase - Success Path', () => {
it('should update driver name', async () => {
// TODO: Implement test
// Scenario: Update driver name
// Given: A driver exists with name "John Doe"
// When: UpdateProfileUseCase.execute() is called with new name "Jane Doe"
// Then: The driver's name should be updated to "Jane Doe"
// And: EventPublisher should emit ProfileUpdatedEvent
});
it('should update driver email', async () => {
// TODO: Implement test
// Scenario: Update driver email
// Given: A driver exists with email "john@example.com"
// When: UpdateProfileUseCase.execute() is called with new email "jane@example.com"
// Then: The driver's email should be updated to "jane@example.com"
// And: EventPublisher should emit ProfileUpdatedEvent
});
it('should update driver bio', async () => {
// TODO: Implement test
// Scenario: Update driver bio
// Given: A driver exists with bio "Original bio"
// When: UpdateProfileUseCase.execute() is called with new bio "Updated bio"
// Then: The driver's bio should be updated to "Updated bio"
// And: EventPublisher should emit ProfileUpdatedEvent
});
it('should update driver location', async () => {
// TODO: Implement test
// Scenario: Update driver location
// Given: A driver exists with location "USA"
// When: UpdateProfileUseCase.execute() is called with new location "Germany"
// Then: The driver's location should be updated to "Germany"
// And: EventPublisher should emit ProfileUpdatedEvent
});
it('should update driver avatar', async () => {
// TODO: Implement test
// Scenario: Update driver avatar
// Given: A driver exists with avatar "avatar1.jpg"
// When: UpdateProfileUseCase.execute() is called with new avatar "avatar2.jpg"
// Then: The driver's avatar should be updated to "avatar2.jpg"
// And: EventPublisher should emit ProfileUpdatedEvent
});
it('should update driver social links', async () => {
// TODO: Implement test
// Scenario: Update driver social links
// Given: A driver exists with social links
// When: UpdateProfileUseCase.execute() is called with new social links
// Then: The driver's social links should be updated
// And: EventPublisher should emit ProfileUpdatedEvent
});
it('should update driver team affiliation', async () => {
// TODO: Implement test
// Scenario: Update driver team affiliation
// Given: A driver exists with team affiliation "Team A"
// When: UpdateProfileUseCase.execute() is called with new team affiliation "Team B"
// Then: The driver's team affiliation should be updated to "Team B"
// And: EventPublisher should emit ProfileUpdatedEvent
});
it('should update multiple profile fields at once', async () => {
// TODO: Implement test
// Scenario: Update multiple fields
// Given: A driver exists with name "John Doe" and email "john@example.com"
// When: UpdateProfileUseCase.execute() is called with new name "Jane Doe" and new email "jane@example.com"
// Then: The driver's name should be updated to "Jane Doe"
// And: The driver's email should be updated to "jane@example.com"
// And: EventPublisher should emit ProfileUpdatedEvent
});
});
describe('UpdateProfileUseCase - Validation', () => {
it('should reject update with invalid email format', async () => {
// TODO: Implement test
// Scenario: Invalid email format
// Given: A driver exists
// When: UpdateProfileUseCase.execute() is called with invalid email "invalid-email"
// Then: Should throw ValidationError
// And: The driver's email should NOT be updated
// And: EventPublisher should NOT emit any events
});
it('should reject update with empty required fields', async () => {
// TODO: Implement test
// Scenario: Empty required fields
// Given: A driver exists
// When: UpdateProfileUseCase.execute() is called with empty name
// Then: Should throw ValidationError
// And: The driver's name should NOT be updated
// And: EventPublisher should NOT emit any events
});
it('should reject update with invalid avatar file', async () => {
// TODO: Implement test
// Scenario: Invalid avatar file
// Given: A driver exists
// When: UpdateProfileUseCase.execute() is called with invalid avatar file
// Then: Should throw ValidationError
// And: The driver's avatar should NOT be updated
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateProfileUseCase - 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
// When: UpdateProfileUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: UpdateProfileUseCase.execute() is called with invalid driver 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 driver exists
// And: DriverRepository throws an error during update
// When: UpdateProfileUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('Profile Data Orchestration', () => {
it('should correctly calculate win percentage from race results', async () => {
// TODO: Implement test
// Scenario: Win percentage calculation
// Given: A driver exists
// And: The driver has 10 race starts
// And: The driver has 3 wins
// When: GetProfileStatisticsUseCase.execute() is called
// Then: The result should show win percentage as 30%
});
it('should correctly calculate podium rate from race results', async () => {
// TODO: Implement test
// Scenario: Podium rate calculation
// Given: A driver exists
// And: The driver has 10 race starts
// And: The driver has 5 podiums
// When: GetProfileStatisticsUseCase.execute() is called
// Then: The result should show podium rate as 50%
});
it('should correctly format social links with proper URLs', async () => {
// TODO: Implement test
// Scenario: Social links formatting
// Given: A driver exists
// And: The driver has social links (Discord, Twitter, iRacing)
// When: GetProfileUseCase.execute() is called
// Then: Social links should show:
// - Discord: https://discord.gg/username
// - Twitter: https://twitter.com/username
// - iRacing: https://members.iracing.com/membersite/member/profile?username=username
});
it('should correctly format team affiliation with role', async () => {
// TODO: Implement test
// Scenario: Team affiliation formatting
// Given: A driver exists
// And: The driver is affiliated with Team XYZ
// And: The driver's role is "Driver"
// When: GetProfileUseCase.execute() is called
// Then: Team affiliation should show:
// - Team name: Team XYZ
// - Team logo: (if available)
// - Driver role: Driver
});
it('should correctly calculate profile completion percentage', async () => {
// TODO: Implement test
// Scenario: Profile completion calculation
// Given: A driver exists
// And: The driver has name, email, avatar, bio, location, social links
// When: GetProfileCompletionUseCase.execute() is called
// Then: The result should show 100% completion
// And: The result should show no incomplete sections
});
it('should correctly identify incomplete profile sections', async () => {
// TODO: Implement test
// Scenario: Incomplete profile sections
// Given: A driver exists
// And: The driver has name and email only
// When: GetProfileCompletionUseCase.execute() is called
// Then: The result should show incomplete sections:
// - Avatar
// - Bio
// - Location
// - Social links
// - Team affiliation
});
});
});

View File

@@ -1,968 +0,0 @@
/**
* Integration Test: Profile Overview Use Case Orchestration
*
* Tests the orchestration logic of profile overview-related Use Cases:
* - GetProfileOverviewUseCase: Retrieves driver's profile overview with stats, team memberships, and social summary
* - UpdateDriverProfileUseCase: Updates driver's profile information
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { 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 { InMemoryDriverStatsRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository';
import { InMemoryDriverExtendedProfileProvider } from '../../../adapters/racing/ports/InMemoryDriverExtendedProfileProvider';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { InMemoryResultRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryResultRepository';
import { InMemoryStandingRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
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 { TeamMembership } from '../../../core/racing/domain/types/TeamMembership';
import { DriverStats } from '../../../core/racing/application/use-cases/DriverStatsUseCase';
import { DriverRanking } from '../../../core/racing/application/use-cases/RankingUseCase';
import { Logger } from '../../../core/shared/domain/Logger';
// Mock logger for testing
class MockLogger implements Logger {
debug(message: string, ...args: any[]): void {}
info(message: string, ...args: any[]): void {}
warn(message: string, ...args: any[]): void {}
error(message: string, ...args: any[]): void {}
}
describe('Profile Overview Use Case Orchestration', () => {
let driverRepository: InMemoryDriverRepository;
let teamRepository: InMemoryTeamRepository;
let teamMembershipRepository: InMemoryTeamMembershipRepository;
let socialRepository: InMemorySocialGraphRepository;
let driverStatsRepository: InMemoryDriverStatsRepository;
let driverExtendedProfileProvider: InMemoryDriverExtendedProfileProvider;
let eventPublisher: InMemoryEventPublisher;
let resultRepository: InMemoryResultRepository;
let standingRepository: InMemoryStandingRepository;
let raceRepository: InMemoryRaceRepository;
let driverStatsUseCase: DriverStatsUseCase;
let rankingUseCase: RankingUseCase;
let getProfileOverviewUseCase: GetProfileOverviewUseCase;
let updateDriverProfileUseCase: UpdateDriverProfileUseCase;
let logger: MockLogger;
beforeAll(() => {
logger = new MockLogger();
driverRepository = new InMemoryDriverRepository(logger);
teamRepository = new InMemoryTeamRepository(logger);
teamMembershipRepository = new InMemoryTeamMembershipRepository(logger);
socialRepository = new InMemorySocialGraphRepository(logger);
driverStatsRepository = new InMemoryDriverStatsRepository(logger);
driverExtendedProfileProvider = new InMemoryDriverExtendedProfileProvider(logger);
eventPublisher = new InMemoryEventPublisher();
resultRepository = new InMemoryResultRepository(logger, raceRepository);
standingRepository = new InMemoryStandingRepository(logger, {}, resultRepository, raceRepository);
raceRepository = new InMemoryRaceRepository(logger);
driverStatsUseCase = new DriverStatsUseCase(resultRepository, standingRepository, driverStatsRepository, logger);
rankingUseCase = new RankingUseCase(standingRepository, driverRepository, driverStatsRepository, logger);
getProfileOverviewUseCase = new GetProfileOverviewUseCase(
driverRepository,
teamRepository,
teamMembershipRepository,
socialRepository,
driverExtendedProfileProvider,
driverStatsUseCase,
rankingUseCase
);
updateDriverProfileUseCase = new UpdateDriverProfileUseCase(driverRepository, logger);
});
beforeEach(async () => {
await driverRepository.clear();
await teamRepository.clear();
await teamMembershipRepository.clear();
await socialRepository.clear();
await driverStatsRepository.clear();
eventPublisher.clear();
});
describe('GetProfileOverviewUseCase - Success Path', () => {
it('should retrieve complete profile overview for driver with all data', async () => {
// Scenario: Driver with complete profile data
// Given: A driver exists with complete personal information
const driverId = 'driver-123';
const driver = Driver.create({
id: driverId,
iracingId: '12345',
name: 'John Doe',
country: 'US',
bio: 'Professional racing driver with 10 years experience',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver has complete statistics
const stats: DriverStats = {
totalRaces: 50,
wins: 15,
podiums: 25,
dnfs: 5,
avgFinish: 8.5,
bestFinish: 1,
worstFinish: 20,
finishRate: 90,
winRate: 30,
podiumRate: 50,
percentile: 85,
rating: 1850,
consistency: 92,
overallRank: 42,
};
await driverStatsRepository.saveDriverStats(driverId, stats);
// And: The driver is a member of a team
const team = Team.create({
id: 'team-1',
name: 'Racing Team',
tag: 'RT',
description: 'Professional racing team',
ownerId: 'owner-1',
isRecruiting: true,
});
await teamRepository.create(team);
const membership: TeamMembership = {
teamId: 'team-1',
driverId: driverId,
role: 'Driver',
status: 'active',
joinedAt: new Date('2024-01-01'),
};
await teamMembershipRepository.saveMembership(membership);
// And: The driver has friends
const friendDriver = Driver.create({
id: 'friend-1',
iracingId: '67890',
name: 'Jane Smith',
country: 'UK',
avatarRef: undefined,
});
await driverRepository.create(friendDriver);
await socialRepository.seed({
drivers: [driver, friendDriver],
friendships: [{ driverId: driverId, friendId: 'friend-1' }],
feedEvents: [],
});
// When: GetProfileOverviewUseCase.execute() is called with driver ID
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should contain all profile sections
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
// And: Driver info should be complete
expect(profile.driverInfo.driver.id).toBe(driverId);
expect(profile.driverInfo.driver.name.toString()).toBe('John Doe');
expect(profile.driverInfo.driver.country.toString()).toBe('US');
expect(profile.driverInfo.driver.bio?.toString()).toBe('Professional racing driver with 10 years experience');
expect(profile.driverInfo.totalDrivers).toBeGreaterThan(0);
expect(profile.driverInfo.globalRank).toBe(42);
expect(profile.driverInfo.consistency).toBe(92);
expect(profile.driverInfo.rating).toBe(1850);
// And: Stats should be complete
expect(profile.stats).not.toBeNull();
expect(profile.stats!.totalRaces).toBe(50);
expect(profile.stats!.wins).toBe(15);
expect(profile.stats!.podiums).toBe(25);
expect(profile.stats!.dnfs).toBe(5);
expect(profile.stats!.avgFinish).toBe(8.5);
expect(profile.stats!.bestFinish).toBe(1);
expect(profile.stats!.worstFinish).toBe(20);
expect(profile.stats!.finishRate).toBe(90);
expect(profile.stats!.winRate).toBe(30);
expect(profile.stats!.podiumRate).toBe(50);
expect(profile.stats!.percentile).toBe(85);
expect(profile.stats!.rating).toBe(1850);
expect(profile.stats!.consistency).toBe(92);
expect(profile.stats!.overallRank).toBe(42);
// And: Finish distribution should be calculated
expect(profile.finishDistribution).not.toBeNull();
expect(profile.finishDistribution!.totalRaces).toBe(50);
expect(profile.finishDistribution!.wins).toBe(15);
expect(profile.finishDistribution!.podiums).toBe(25);
expect(profile.finishDistribution!.dnfs).toBe(5);
expect(profile.finishDistribution!.topTen).toBeGreaterThan(0);
expect(profile.finishDistribution!.other).toBeGreaterThan(0);
// And: Team memberships should be present
expect(profile.teamMemberships).toHaveLength(1);
expect(profile.teamMemberships[0].team.id).toBe('team-1');
expect(profile.teamMemberships[0].team.name.toString()).toBe('Racing Team');
expect(profile.teamMemberships[0].membership.role).toBe('Driver');
expect(profile.teamMemberships[0].membership.status).toBe('active');
// And: Social summary should show friends
expect(profile.socialSummary.friendsCount).toBe(1);
expect(profile.socialSummary.friends).toHaveLength(1);
expect(profile.socialSummary.friends[0].id).toBe('friend-1');
expect(profile.socialSummary.friends[0].name.toString()).toBe('Jane Smith');
// And: Extended profile should be present (generated by provider)
expect(profile.extendedProfile).not.toBeNull();
expect(profile.extendedProfile!.socialHandles).toBeInstanceOf(Array);
expect(profile.extendedProfile!.achievements).toBeInstanceOf(Array);
});
it('should retrieve profile overview for driver with minimal data', async () => {
// Scenario: Driver with minimal profile data
// Given: A driver exists with minimal information
const driverId = 'driver-456';
const driver = Driver.create({
id: driverId,
iracingId: '78901',
name: 'New Driver',
country: 'DE',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver has no statistics
// And: The driver is not a member of any team
// And: The driver has no friends
// When: GetProfileOverviewUseCase.execute() is called with driver ID
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should contain basic driver info
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
// And: Driver info should be present
expect(profile.driverInfo.driver.id).toBe(driverId);
expect(profile.driverInfo.driver.name.toString()).toBe('New Driver');
expect(profile.driverInfo.driver.country.toString()).toBe('DE');
expect(profile.driverInfo.totalDrivers).toBeGreaterThan(0);
// And: Stats should be null (no data)
expect(profile.stats).toBeNull();
// And: Finish distribution should be null
expect(profile.finishDistribution).toBeNull();
// And: Team memberships should be empty
expect(profile.teamMemberships).toHaveLength(0);
// And: Social summary should show no friends
expect(profile.socialSummary.friendsCount).toBe(0);
expect(profile.socialSummary.friends).toHaveLength(0);
// And: Extended profile should be present (generated by provider)
expect(profile.extendedProfile).not.toBeNull();
});
it('should retrieve profile overview with multiple team memberships', async () => {
// Scenario: Driver with multiple team memberships
// Given: A driver exists
const driverId = 'driver-789';
const driver = Driver.create({
id: driverId,
iracingId: '11111',
name: 'Multi Team Driver',
country: 'FR',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver is a member of multiple teams
const team1 = Team.create({
id: 'team-1',
name: 'Team A',
tag: 'TA',
description: 'Team A',
ownerId: 'owner-1',
isRecruiting: true,
});
await teamRepository.create(team1);
const team2 = Team.create({
id: 'team-2',
name: 'Team B',
tag: 'TB',
description: 'Team B',
ownerId: 'owner-2',
isRecruiting: false,
});
await teamRepository.create(team2);
const membership1: TeamMembership = {
teamId: 'team-1',
driverId: driverId,
role: 'Driver',
status: 'active',
joinedAt: new Date('2024-01-01'),
};
await teamMembershipRepository.saveMembership(membership1);
const membership2: TeamMembership = {
teamId: 'team-2',
driverId: driverId,
role: 'Admin',
status: 'active',
joinedAt: new Date('2024-02-01'),
};
await teamMembershipRepository.saveMembership(membership2);
// When: GetProfileOverviewUseCase.execute() is called with driver ID
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should contain all team memberships
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
// And: Team memberships should include both teams
expect(profile.teamMemberships).toHaveLength(2);
expect(profile.teamMemberships[0].team.id).toBe('team-1');
expect(profile.teamMemberships[0].membership.role).toBe('Driver');
expect(profile.teamMemberships[1].team.id).toBe('team-2');
expect(profile.teamMemberships[1].membership.role).toBe('Admin');
// And: Team memberships should be sorted by joined date
expect(profile.teamMemberships[0].membership.joinedAt.getTime()).toBeLessThan(
profile.teamMemberships[1].membership.joinedAt.getTime()
);
});
it('should retrieve profile overview with multiple friends', async () => {
// Scenario: Driver with multiple friends
// Given: A driver exists
const driverId = 'driver-friends';
const driver = Driver.create({
id: driverId,
iracingId: '22222',
name: 'Social Driver',
country: 'US',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver has multiple friends
const friend1 = Driver.create({
id: 'friend-1',
iracingId: '33333',
name: 'Friend 1',
country: 'US',
avatarRef: undefined,
});
await driverRepository.create(friend1);
const friend2 = Driver.create({
id: 'friend-2',
iracingId: '44444',
name: 'Friend 2',
country: 'UK',
avatarRef: undefined,
});
await driverRepository.create(friend2);
const friend3 = Driver.create({
id: 'friend-3',
iracingId: '55555',
name: 'Friend 3',
country: 'DE',
avatarRef: undefined,
});
await driverRepository.create(friend3);
await socialRepository.seed({
drivers: [driver, friend1, friend2, friend3],
friendships: [
{ driverId: driverId, friendId: 'friend-1' },
{ driverId: driverId, friendId: 'friend-2' },
{ driverId: driverId, friendId: 'friend-3' },
],
feedEvents: [],
});
// When: GetProfileOverviewUseCase.execute() is called with driver ID
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should contain all friends
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
// And: Social summary should show 3 friends
expect(profile.socialSummary.friendsCount).toBe(3);
expect(profile.socialSummary.friends).toHaveLength(3);
// And: All friends should be present
const friendIds = profile.socialSummary.friends.map(f => f.id);
expect(friendIds).toContain('friend-1');
expect(friendIds).toContain('friend-2');
expect(friendIds).toContain('friend-3');
});
});
describe('GetProfileOverviewUseCase - Edge Cases', () => {
it('should handle driver with no statistics', async () => {
// Scenario: Driver without statistics
// Given: A driver exists
const driverId = 'driver-no-stats';
const driver = Driver.create({
id: driverId,
iracingId: '66666',
name: 'No Stats Driver',
country: 'CA',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver has no statistics
// When: GetProfileOverviewUseCase.execute() is called with driver ID
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should contain driver info with null stats
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
expect(profile.driverInfo.driver.id).toBe(driverId);
expect(profile.stats).toBeNull();
expect(profile.finishDistribution).toBeNull();
});
it('should handle driver with no team memberships', async () => {
// Scenario: Driver without team memberships
// Given: A driver exists
const driverId = 'driver-no-teams';
const driver = Driver.create({
id: driverId,
iracingId: '77777',
name: 'Solo Driver',
country: 'IT',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver is not a member of any team
// When: GetProfileOverviewUseCase.execute() is called with driver ID
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should contain driver info with empty team memberships
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
expect(profile.driverInfo.driver.id).toBe(driverId);
expect(profile.teamMemberships).toHaveLength(0);
});
it('should handle driver with no friends', async () => {
// Scenario: Driver without friends
// Given: A driver exists
const driverId = 'driver-no-friends';
const driver = Driver.create({
id: driverId,
iracingId: '88888',
name: 'Lonely Driver',
country: 'ES',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver has no friends
// When: GetProfileOverviewUseCase.execute() is called with driver ID
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should contain driver info with empty social summary
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
expect(profile.driverInfo.driver.id).toBe(driverId);
expect(profile.socialSummary.friendsCount).toBe(0);
expect(profile.socialSummary.friends).toHaveLength(0);
});
});
describe('GetProfileOverviewUseCase - 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: GetProfileOverviewUseCase.execute() is called with non-existent driver ID
const result = await getProfileOverviewUseCase.execute({ driverId: nonExistentDriverId });
// Then: Should return error
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error.code).toBe('DRIVER_NOT_FOUND');
expect(error.details.message).toBe('Driver not found');
});
it('should return error when driver ID is invalid', async () => {
// Scenario: Invalid driver ID
// Given: An invalid driver ID (empty string)
const invalidDriverId = '';
// When: GetProfileOverviewUseCase.execute() is called with invalid driver ID
const result = await getProfileOverviewUseCase.execute({ driverId: invalidDriverId });
// Then: Should return error
expect(result.isErr()).toBe(true);
const error = result.getError();
expect(error.code).toBe('DRIVER_NOT_FOUND');
expect(error.details.message).toBe('Driver not found');
});
});
describe('UpdateDriverProfileUseCase - Success Path', () => {
it('should update driver bio', async () => {
// Scenario: Update driver bio
// Given: A driver exists with bio
const driverId = 'driver-update-bio';
const driver = Driver.create({
id: driverId,
iracingId: '99999',
name: 'Update Driver',
country: 'US',
bio: 'Original bio',
avatarRef: undefined,
});
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 = 'driver-update-country';
const driver = Driver.create({
id: driverId,
iracingId: '10101',
name: 'Country Driver',
country: 'US',
avatarRef: undefined,
});
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 = 'driver-update-multiple';
const driver = Driver.create({
id: driverId,
iracingId: '11111',
name: 'Multi Update Driver',
country: 'US',
bio: 'Original bio',
avatarRef: undefined,
});
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 = 'driver-empty-bio';
const driver = Driver.create({
id: driverId,
iracingId: '12121',
name: 'Empty Bio Driver',
country: 'US',
avatarRef: undefined,
});
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.getError();
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 = 'driver-empty-country';
const driver = Driver.create({
id: driverId,
iracingId: '13131',
name: 'Empty Country Driver',
country: 'US',
avatarRef: undefined,
});
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.getError();
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.getError();
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.getError();
expect(error.code).toBe('DRIVER_NOT_FOUND');
expect(error.details.message).toContain('Driver with id');
});
});
describe('Profile Data Orchestration', () => {
it('should correctly calculate win percentage from race results', async () => {
// Scenario: Win percentage calculation
// Given: A driver exists
const driverId = 'driver-win-percentage';
const driver = Driver.create({
id: driverId,
iracingId: '14141',
name: 'Win Driver',
country: 'US',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver has 10 race starts and 3 wins
const stats: DriverStats = {
totalRaces: 10,
wins: 3,
podiums: 5,
dnfs: 0,
avgFinish: 5.0,
bestFinish: 1,
worstFinish: 10,
finishRate: 100,
winRate: 30,
podiumRate: 50,
percentile: 70,
rating: 1600,
consistency: 85,
overallRank: 100,
};
await driverStatsRepository.saveDriverStats(driverId, stats);
// When: GetProfileOverviewUseCase.execute() is called
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should show win percentage as 30%
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
expect(profile.stats!.winRate).toBe(30);
});
it('should correctly calculate podium rate from race results', async () => {
// Scenario: Podium rate calculation
// Given: A driver exists
const driverId = 'driver-podium-rate';
const driver = Driver.create({
id: driverId,
iracingId: '15151',
name: 'Podium Driver',
country: 'US',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver has 10 race starts and 5 podiums
const stats: DriverStats = {
totalRaces: 10,
wins: 2,
podiums: 5,
dnfs: 0,
avgFinish: 4.0,
bestFinish: 1,
worstFinish: 8,
finishRate: 100,
winRate: 20,
podiumRate: 50,
percentile: 60,
rating: 1550,
consistency: 80,
overallRank: 150,
};
await driverStatsRepository.saveDriverStats(driverId, stats);
// When: GetProfileOverviewUseCase.execute() is called
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should show podium rate as 50%
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
expect(profile.stats!.podiumRate).toBe(50);
});
it('should correctly calculate finish distribution', async () => {
// Scenario: Finish distribution calculation
// Given: A driver exists
const driverId = 'driver-finish-dist';
const driver = Driver.create({
id: driverId,
iracingId: '16161',
name: 'Finish Driver',
country: 'US',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver has 20 race starts with various finishes
const stats: DriverStats = {
totalRaces: 20,
wins: 5,
podiums: 8,
dnfs: 2,
avgFinish: 6.5,
bestFinish: 1,
worstFinish: 15,
finishRate: 90,
winRate: 25,
podiumRate: 40,
percentile: 75,
rating: 1700,
consistency: 88,
overallRank: 75,
};
await driverStatsRepository.saveDriverStats(driverId, stats);
// When: GetProfileOverviewUseCase.execute() is called
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: The result should show correct finish distribution
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
expect(profile.finishDistribution!.totalRaces).toBe(20);
expect(profile.finishDistribution!.wins).toBe(5);
expect(profile.finishDistribution!.podiums).toBe(8);
expect(profile.finishDistribution!.dnfs).toBe(2);
expect(profile.finishDistribution!.topTen).toBeGreaterThan(0);
expect(profile.finishDistribution!.other).toBeGreaterThan(0);
});
it('should correctly format team affiliation with role', async () => {
// Scenario: Team affiliation formatting
// Given: A driver exists
const driverId = 'driver-team-affiliation';
const driver = Driver.create({
id: driverId,
iracingId: '17171',
name: 'Team Driver',
country: 'US',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver is affiliated with a team
const team = Team.create({
id: 'team-affiliation',
name: 'Affiliation Team',
tag: 'AT',
description: 'Team for testing',
ownerId: 'owner-1',
isRecruiting: true,
});
await teamRepository.create(team);
const membership: TeamMembership = {
teamId: 'team-affiliation',
driverId: driverId,
role: 'Driver',
status: 'active',
joinedAt: new Date('2024-01-01'),
};
await teamMembershipRepository.saveMembership(membership);
// When: GetProfileOverviewUseCase.execute() is called
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: Team affiliation should show team name and role
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
expect(profile.teamMemberships).toHaveLength(1);
expect(profile.teamMemberships[0].team.name.toString()).toBe('Affiliation Team');
expect(profile.teamMemberships[0].membership.role).toBe('Driver');
});
it('should correctly identify driver role in each team', async () => {
// Scenario: Driver role identification
// Given: A driver exists
const driverId = 'driver-roles';
const driver = Driver.create({
id: driverId,
iracingId: '18181',
name: 'Role Driver',
country: 'US',
avatarRef: undefined,
});
await driverRepository.create(driver);
// And: The driver has different roles in different teams
const team1 = Team.create({
id: 'team-role-1',
name: 'Team A',
tag: 'TA',
description: 'Team A',
ownerId: 'owner-1',
isRecruiting: true,
});
await teamRepository.create(team1);
const team2 = Team.create({
id: 'team-role-2',
name: 'Team B',
tag: 'TB',
description: 'Team B',
ownerId: 'owner-2',
isRecruiting: false,
});
await teamRepository.create(team2);
const team3 = Team.create({
id: 'team-role-3',
name: 'Team C',
tag: 'TC',
description: 'Team C',
ownerId: driverId,
isRecruiting: true,
});
await teamRepository.create(team3);
const membership1: TeamMembership = {
teamId: 'team-role-1',
driverId: driverId,
role: 'Driver',
status: 'active',
joinedAt: new Date('2024-01-01'),
};
await teamMembershipRepository.saveMembership(membership1);
const membership2: TeamMembership = {
teamId: 'team-role-2',
driverId: driverId,
role: 'Admin',
status: 'active',
joinedAt: new Date('2024-02-01'),
};
await teamMembershipRepository.saveMembership(membership2);
const membership3: TeamMembership = {
teamId: 'team-role-3',
driverId: driverId,
role: 'Owner',
status: 'active',
joinedAt: new Date('2024-03-01'),
};
await teamMembershipRepository.saveMembership(membership3);
// When: GetProfileOverviewUseCase.execute() is called
const result = await getProfileOverviewUseCase.execute({ driverId });
// Then: Each team should show the correct role
expect(result.isOk()).toBe(true);
const profile = result.unwrap();
expect(profile.teamMemberships).toHaveLength(3);
const teamARole = profile.teamMemberships.find(m => m.team.id === 'team-role-1')?.membership.role;
const teamBRole = profile.teamMemberships.find(m => m.team.id === 'team-role-2')?.membership.role;
const teamCRole = profile.teamMemberships.find(m => m.team.id === 'team-role-3')?.membership.role;
expect(teamARole).toBe('Driver');
expect(teamBRole).toBe('Admin');
expect(teamCRole).toBe('Owner');
});
});
});

View File

@@ -1,668 +0,0 @@
/**
* Integration Test: Profile Settings Use Case Orchestration
*
* Tests the orchestration logic of profile settings-related Use Cases:
* - GetProfileSettingsUseCase: Retrieves driver's current profile settings
* - UpdateProfileSettingsUseCase: Updates driver's profile settings
* - UpdateAvatarUseCase: Updates driver's avatar
* - ClearAvatarUseCase: Clears driver's avatar
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { GetProfileSettingsUseCase } from '../../../core/profile/use-cases/GetProfileSettingsUseCase';
import { UpdateProfileSettingsUseCase } from '../../../core/profile/use-cases/UpdateProfileSettingsUseCase';
import { UpdateAvatarUseCase } from '../../../core/media/use-cases/UpdateAvatarUseCase';
import { ClearAvatarUseCase } from '../../../core/media/use-cases/ClearAvatarUseCase';
import { ProfileSettingsQuery } from '../../../core/profile/ports/ProfileSettingsQuery';
import { UpdateProfileSettingsCommand } from '../../../core/profile/ports/UpdateProfileSettingsCommand';
import { UpdateAvatarCommand } from '../../../core/media/ports/UpdateAvatarCommand';
import { ClearAvatarCommand } from '../../../core/media/ports/ClearAvatarCommand';
describe('Profile Settings Use Case Orchestration', () => {
let driverRepository: InMemoryDriverRepository;
let eventPublisher: InMemoryEventPublisher;
let getProfileSettingsUseCase: GetProfileSettingsUseCase;
let updateProfileSettingsUseCase: UpdateProfileSettingsUseCase;
let updateAvatarUseCase: UpdateAvatarUseCase;
let clearAvatarUseCase: ClearAvatarUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// driverRepository = new InMemoryDriverRepository();
// eventPublisher = new InMemoryEventPublisher();
// getProfileSettingsUseCase = new GetProfileSettingsUseCase({
// driverRepository,
// eventPublisher,
// });
// updateProfileSettingsUseCase = new UpdateProfileSettingsUseCase({
// driverRepository,
// eventPublisher,
// });
// updateAvatarUseCase = new UpdateAvatarUseCase({
// driverRepository,
// eventPublisher,
// });
// clearAvatarUseCase = new ClearAvatarUseCase({
// driverRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// driverRepository.clear();
// eventPublisher.clear();
});
describe('GetProfileSettingsUseCase - Success Path', () => {
it('should retrieve complete driver profile settings', async () => {
// TODO: Implement test
// Scenario: Driver with complete profile settings
// Given: A driver exists with complete profile settings
// And: The driver has name, email, avatar, bio, location
// And: The driver has social links configured
// And: The driver has team affiliation
// And: The driver has notification preferences
// And: The driver has privacy settings
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain all profile settings
// And: The result should display name, email, avatar, bio, location
// And: The result should display social links
// And: The result should display team affiliation
// And: The result should display notification preferences
// And: The result should display privacy settings
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should retrieve driver profile settings with minimal information', async () => {
// TODO: Implement test
// Scenario: Driver with minimal profile settings
// Given: A driver exists with minimal information
// And: The driver has only name and email
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain basic profile settings
// And: The result should display name and email
// And: The result should show empty values for optional fields
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should retrieve driver profile settings with avatar', async () => {
// TODO: Implement test
// Scenario: Driver with avatar
// Given: A driver exists with an avatar
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain avatar URL
// And: The avatar should be accessible
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should retrieve driver profile settings with social links', async () => {
// TODO: Implement test
// Scenario: Driver with social links
// Given: A driver exists with social links
// And: The driver has Discord, Twitter, iRacing links
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain social links
// And: Each link should have correct URL format
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should retrieve driver profile settings with team affiliation', async () => {
// TODO: Implement test
// Scenario: Driver with team affiliation
// Given: A driver exists with team affiliation
// And: The driver is affiliated with Team XYZ
// And: The driver has role "Driver"
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain team information
// And: The result should show team name and logo
// And: The result should show driver role
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should retrieve driver profile settings with notification preferences', async () => {
// TODO: Implement test
// Scenario: Driver with notification preferences
// Given: A driver exists with notification preferences
// And: The driver has email notifications enabled
// And: The driver has push notifications disabled
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain notification preferences
// And: The result should show email notification status
// And: The result should show push notification status
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should retrieve driver profile settings with privacy settings', async () => {
// TODO: Implement test
// Scenario: Driver with privacy settings
// Given: A driver exists with privacy settings
// And: The driver has profile visibility set to "Public"
// And: The driver has race results visibility set to "Friends Only"
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain privacy settings
// And: The result should show profile visibility
// And: The result should show race results visibility
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should retrieve driver profile settings with bio', async () => {
// TODO: Implement test
// Scenario: Driver with bio
// Given: A driver exists with a bio
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain bio text
// And: The bio should be displayed correctly
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should retrieve driver profile settings with location', async () => {
// TODO: Implement test
// Scenario: Driver with location
// Given: A driver exists with location
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain location
// And: The location should be displayed correctly
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
});
describe('GetProfileSettingsUseCase - Edge Cases', () => {
it('should handle driver with no avatar', async () => {
// TODO: Implement test
// Scenario: Driver without avatar
// Given: A driver exists without avatar
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain profile settings
// And: The result should show default avatar or placeholder
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should handle driver with no social links', async () => {
// TODO: Implement test
// Scenario: Driver without social links
// Given: A driver exists without social links
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain profile settings
// And: The result should show empty social links section
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should handle driver with no team affiliation', async () => {
// TODO: Implement test
// Scenario: Driver without team affiliation
// Given: A driver exists without team affiliation
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain profile settings
// And: The result should show empty team section
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should handle driver with no bio', async () => {
// TODO: Implement test
// Scenario: Driver without bio
// Given: A driver exists without bio
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain profile settings
// And: The result should show empty bio section
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should handle driver with no location', async () => {
// TODO: Implement test
// Scenario: Driver without location
// Given: A driver exists without location
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain profile settings
// And: The result should show empty location section
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should handle driver with no notification preferences', async () => {
// TODO: Implement test
// Scenario: Driver without notification preferences
// Given: A driver exists without notification preferences
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain profile settings
// And: The result should show default notification preferences
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
it('should handle driver with no privacy settings', async () => {
// TODO: Implement test
// Scenario: Driver without privacy settings
// Given: A driver exists without privacy settings
// When: GetProfileSettingsUseCase.execute() is called with driver ID
// Then: The result should contain profile settings
// And: The result should show default privacy settings
// And: EventPublisher should emit ProfileSettingsAccessedEvent
});
});
describe('GetProfileSettingsUseCase - 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
// When: GetProfileSettingsUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: GetProfileSettingsUseCase.execute() is called with invalid driver 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 driver exists
// And: DriverRepository throws an error during query
// When: GetProfileSettingsUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateProfileSettingsUseCase - Success Path', () => {
it('should update driver name', async () => {
// TODO: Implement test
// Scenario: Update driver name
// Given: A driver exists with name "John Doe"
// When: UpdateProfileSettingsUseCase.execute() is called with new name "Jane Doe"
// Then: The driver's name should be updated to "Jane Doe"
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
it('should update driver email', async () => {
// TODO: Implement test
// Scenario: Update driver email
// Given: A driver exists with email "john@example.com"
// When: UpdateProfileSettingsUseCase.execute() is called with new email "jane@example.com"
// Then: The driver's email should be updated to "jane@example.com"
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
it('should update driver bio', async () => {
// TODO: Implement test
// Scenario: Update driver bio
// Given: A driver exists with bio "Original bio"
// When: UpdateProfileSettingsUseCase.execute() is called with new bio "Updated bio"
// Then: The driver's bio should be updated to "Updated bio"
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
it('should update driver location', async () => {
// TODO: Implement test
// Scenario: Update driver location
// Given: A driver exists with location "USA"
// When: UpdateProfileSettingsUseCase.execute() is called with new location "Germany"
// Then: The driver's location should be updated to "Germany"
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
it('should update driver social links', async () => {
// TODO: Implement test
// Scenario: Update driver social links
// Given: A driver exists with social links
// When: UpdateProfileSettingsUseCase.execute() is called with new social links
// Then: The driver's social links should be updated
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
it('should update driver team affiliation', async () => {
// TODO: Implement test
// Scenario: Update driver team affiliation
// Given: A driver exists with team affiliation "Team A"
// When: UpdateProfileSettingsUseCase.execute() is called with new team affiliation "Team B"
// Then: The driver's team affiliation should be updated to "Team B"
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
it('should update driver notification preferences', async () => {
// TODO: Implement test
// Scenario: Update driver notification preferences
// Given: A driver exists with notification preferences
// When: UpdateProfileSettingsUseCase.execute() is called with new notification preferences
// Then: The driver's notification preferences should be updated
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
it('should update driver privacy settings', async () => {
// TODO: Implement test
// Scenario: Update driver privacy settings
// Given: A driver exists with privacy settings
// When: UpdateProfileSettingsUseCase.execute() is called with new privacy settings
// Then: The driver's privacy settings should be updated
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
it('should update multiple profile settings at once', async () => {
// TODO: Implement test
// Scenario: Update multiple settings
// Given: A driver exists with name "John Doe" and email "john@example.com"
// When: UpdateProfileSettingsUseCase.execute() is called with new name "Jane Doe" and new email "jane@example.com"
// Then: The driver's name should be updated to "Jane Doe"
// And: The driver's email should be updated to "jane@example.com"
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
});
describe('UpdateProfileSettingsUseCase - Validation', () => {
it('should reject update with invalid email format', async () => {
// TODO: Implement test
// Scenario: Invalid email format
// Given: A driver exists
// When: UpdateProfileSettingsUseCase.execute() is called with invalid email "invalid-email"
// Then: Should throw ValidationError
// And: The driver's email should NOT be updated
// And: EventPublisher should NOT emit any events
});
it('should reject update with empty required fields', async () => {
// TODO: Implement test
// Scenario: Empty required fields
// Given: A driver exists
// When: UpdateProfileSettingsUseCase.execute() is called with empty name
// Then: Should throw ValidationError
// And: The driver's name should NOT be updated
// And: EventPublisher should NOT emit any events
});
it('should reject update with invalid social link URL', async () => {
// TODO: Implement test
// Scenario: Invalid social link URL
// Given: A driver exists
// When: UpdateProfileSettingsUseCase.execute() is called with invalid social link URL
// Then: Should throw ValidationError
// And: The driver's social links should NOT be updated
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateProfileSettingsUseCase - 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
// When: UpdateProfileSettingsUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: UpdateProfileSettingsUseCase.execute() is called with invalid driver 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 driver exists
// And: DriverRepository throws an error during update
// When: UpdateProfileSettingsUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateAvatarUseCase - Success Path', () => {
it('should update driver avatar', async () => {
// TODO: Implement test
// Scenario: Update driver avatar
// Given: A driver exists with avatar "avatar1.jpg"
// When: UpdateAvatarUseCase.execute() is called with new avatar "avatar2.jpg"
// Then: The driver's avatar should be updated to "avatar2.jpg"
// And: EventPublisher should emit AvatarUpdatedEvent
});
it('should update driver avatar with validation', async () => {
// TODO: Implement test
// Scenario: Update driver avatar with validation
// Given: A driver exists
// When: UpdateAvatarUseCase.execute() is called with valid avatar file
// Then: The driver's avatar should be updated
// And: The avatar should be validated
// And: EventPublisher should emit AvatarUpdatedEvent
});
});
describe('UpdateAvatarUseCase - Validation', () => {
it('should reject update with invalid avatar file', async () => {
// TODO: Implement test
// Scenario: Invalid avatar file
// Given: A driver exists
// When: UpdateAvatarUseCase.execute() is called with invalid avatar file
// Then: Should throw ValidationError
// And: The driver's avatar should NOT be updated
// And: EventPublisher should NOT emit any events
});
it('should reject update with invalid file format', async () => {
// TODO: Implement test
// Scenario: Invalid file format
// Given: A driver exists
// When: UpdateAvatarUseCase.execute() is called with invalid file format
// Then: Should throw ValidationError
// And: The driver's avatar should NOT be updated
// And: EventPublisher should NOT emit any events
});
it('should reject update with file exceeding size limit', async () => {
// TODO: Implement test
// Scenario: File exceeding size limit
// Given: A driver exists
// When: UpdateAvatarUseCase.execute() is called with file exceeding size limit
// Then: Should throw ValidationError
// And: The driver's avatar should NOT be updated
// And: EventPublisher should NOT emit any events
});
});
describe('UpdateAvatarUseCase - 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
// When: UpdateAvatarUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: UpdateAvatarUseCase.execute() is called with invalid driver 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 driver exists
// And: DriverRepository throws an error during update
// When: UpdateAvatarUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('ClearAvatarUseCase - Success Path', () => {
it('should clear driver avatar', async () => {
// TODO: Implement test
// Scenario: Clear driver avatar
// Given: A driver exists with avatar "avatar.jpg"
// When: ClearAvatarUseCase.execute() is called with driver ID
// Then: The driver's avatar should be cleared
// And: The driver should have default avatar or placeholder
// And: EventPublisher should emit AvatarClearedEvent
});
it('should clear driver avatar when no avatar exists', async () => {
// TODO: Implement test
// Scenario: Clear avatar when no avatar exists
// Given: A driver exists without avatar
// When: ClearAvatarUseCase.execute() is called with driver ID
// Then: The operation should succeed
// And: EventPublisher should emit AvatarClearedEvent
});
});
describe('ClearAvatarUseCase - 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
// When: ClearAvatarUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: ClearAvatarUseCase.execute() is called with invalid driver 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 driver exists
// And: DriverRepository throws an error during update
// When: ClearAvatarUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('Profile Settings Data Orchestration', () => {
it('should correctly format social links with proper URLs', async () => {
// TODO: Implement test
// Scenario: Social links formatting
// Given: A driver exists
// And: The driver has social links (Discord, Twitter, iRacing)
// When: GetProfileSettingsUseCase.execute() is called
// Then: Social links should show:
// - Discord: https://discord.gg/username
// - Twitter: https://twitter.com/username
// - iRacing: https://members.iracing.com/membersite/member/profile?username=username
});
it('should correctly format team affiliation with role', async () => {
// TODO: Implement test
// Scenario: Team affiliation formatting
// Given: A driver exists
// And: The driver is affiliated with Team XYZ
// And: The driver's role is "Driver"
// When: GetProfileSettingsUseCase.execute() is called
// Then: Team affiliation should show:
// - Team name: Team XYZ
// - Team logo: (if available)
// - Driver role: Driver
});
it('should correctly format notification preferences', async () => {
// TODO: Implement test
// Scenario: Notification preferences formatting
// Given: A driver exists
// And: The driver has email notifications enabled
// And: The driver has push notifications disabled
// When: GetProfileSettingsUseCase.execute() is called
// Then: Notification preferences should show:
// - Email notifications: Enabled
// - Push notifications: Disabled
});
it('should correctly format privacy settings', async () => {
// TODO: Implement test
// Scenario: Privacy settings formatting
// Given: A driver exists
// And: The driver has profile visibility set to "Public"
// And: The driver has race results visibility set to "Friends Only"
// When: GetProfileSettingsUseCase.execute() is called
// Then: Privacy settings should show:
// - Profile visibility: Public
// - Race results visibility: Friends Only
});
it('should correctly validate email format', async () => {
// TODO: Implement test
// Scenario: Email validation
// Given: A driver exists
// When: UpdateProfileSettingsUseCase.execute() is called with valid email "test@example.com"
// Then: The email should be accepted
// And: EventPublisher should emit ProfileSettingsUpdatedEvent
});
it('should correctly reject invalid email format', async () => {
// TODO: Implement test
// Scenario: Invalid email format
// Given: A driver exists
// When: UpdateProfileSettingsUseCase.execute() is called with invalid email "invalid-email"
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should correctly validate avatar file', async () => {
// TODO: Implement test
// Scenario: Avatar file validation
// Given: A driver exists
// When: UpdateAvatarUseCase.execute() is called with valid avatar file
// Then: The avatar should be accepted
// And: EventPublisher should emit AvatarUpdatedEvent
});
it('should correctly reject invalid avatar file', async () => {
// TODO: Implement test
// Scenario: Invalid avatar file
// Given: A driver exists
// When: UpdateAvatarUseCase.execute() is called with invalid avatar file
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
it('should correctly calculate profile completion percentage', async () => {
// TODO: Implement test
// Scenario: Profile completion calculation
// Given: A driver exists
// And: The driver has name, email, avatar, bio, location, social links
// When: GetProfileSettingsUseCase.execute() is called
// Then: The result should show 100% completion
// And: The result should show no incomplete sections
});
it('should correctly identify incomplete profile sections', async () => {
// TODO: Implement test
// Scenario: Incomplete profile sections
// Given: A driver exists
// And: The driver has name and email only
// When: GetProfileSettingsUseCase.execute() is called
// Then: The result should show incomplete sections:
// - Avatar
// - Bio
// - Location
// - Social links
// - Team affiliation
});
});
});

View File

@@ -1,666 +0,0 @@
/**
* Integration Test: Profile Sponsorship Requests Use Case Orchestration
*
* Tests the orchestration logic of profile sponsorship requests-related Use Cases:
* - GetProfileSponsorshipRequestsUseCase: Retrieves driver's sponsorship requests
* - GetSponsorshipRequestDetailsUseCase: Retrieves sponsorship request details
* - AcceptSponsorshipRequestUseCase: Accepts a sponsorship offer
* - RejectSponsorshipRequestUseCase: Rejects a sponsorship offer
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { InMemoryDriverRepository } from '../../../adapters/drivers/persistence/inmemory/InMemoryDriverRepository';
import { InMemorySponsorshipRepository } from '../../../adapters/sponsorship/persistence/inmemory/InMemorySponsorshipRepository';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { GetProfileSponsorshipRequestsUseCase } from '../../../core/profile/use-cases/GetProfileSponsorshipRequestsUseCase';
import { GetSponsorshipRequestDetailsUseCase } from '../../../core/sponsorship/use-cases/GetSponsorshipRequestDetailsUseCase';
import { AcceptSponsorshipRequestUseCase } from '../../../core/sponsorship/use-cases/AcceptSponsorshipRequestUseCase';
import { RejectSponsorshipRequestUseCase } from '../../../core/sponsorship/use-cases/RejectSponsorshipRequestUseCase';
import { ProfileSponsorshipRequestsQuery } from '../../../core/profile/ports/ProfileSponsorshipRequestsQuery';
import { SponsorshipRequestDetailsQuery } from '../../../core/sponsorship/ports/SponsorshipRequestDetailsQuery';
import { AcceptSponsorshipRequestCommand } from '../../../core/sponsorship/ports/AcceptSponsorshipRequestCommand';
import { RejectSponsorshipRequestCommand } from '../../../core/sponsorship/ports/RejectSponsorshipRequestCommand';
describe('Profile Sponsorship Requests Use Case Orchestration', () => {
let driverRepository: InMemoryDriverRepository;
let sponsorshipRepository: InMemorySponsorshipRepository;
let eventPublisher: InMemoryEventPublisher;
let getProfileSponsorshipRequestsUseCase: GetProfileSponsorshipRequestsUseCase;
let getSponsorshipRequestDetailsUseCase: GetSponsorshipRequestDetailsUseCase;
let acceptSponsorshipRequestUseCase: AcceptSponsorshipRequestUseCase;
let rejectSponsorshipRequestUseCase: RejectSponsorshipRequestUseCase;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// driverRepository = new InMemoryDriverRepository();
// sponsorshipRepository = new InMemorySponsorshipRepository();
// eventPublisher = new InMemoryEventPublisher();
// getProfileSponsorshipRequestsUseCase = new GetProfileSponsorshipRequestsUseCase({
// driverRepository,
// sponsorshipRepository,
// eventPublisher,
// });
// getSponsorshipRequestDetailsUseCase = new GetSponsorshipRequestDetailsUseCase({
// sponsorshipRepository,
// eventPublisher,
// });
// acceptSponsorshipRequestUseCase = new AcceptSponsorshipRequestUseCase({
// driverRepository,
// sponsorshipRepository,
// eventPublisher,
// });
// rejectSponsorshipRequestUseCase = new RejectSponsorshipRequestUseCase({
// driverRepository,
// sponsorshipRepository,
// eventPublisher,
// });
});
beforeEach(() => {
// TODO: Clear all In-Memory repositories before each test
// driverRepository.clear();
// sponsorshipRepository.clear();
// eventPublisher.clear();
});
describe('GetProfileSponsorshipRequestsUseCase - Success Path', () => {
it('should retrieve complete list of sponsorship requests', async () => {
// TODO: Implement test
// Scenario: Driver with multiple sponsorship requests
// Given: A driver exists
// And: The driver has 3 sponsorship requests
// And: Each request has different status (Pending/Accepted/Rejected)
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should contain all sponsorship requests
// And: Each request should display sponsor name, offer details, and status
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with minimal data', async () => {
// TODO: Implement test
// Scenario: Driver with minimal sponsorship requests
// Given: A driver exists
// And: The driver has 1 sponsorship request
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should contain the sponsorship request
// And: The request should display basic information
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with sponsor information', async () => {
// TODO: Implement test
// Scenario: Driver with sponsorship requests having sponsor info
// Given: A driver exists
// And: The driver has sponsorship requests with sponsor details
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should show sponsor information for each request
// And: Sponsor info should include name, logo, and description
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with offer terms', async () => {
// TODO: Implement test
// Scenario: Driver with sponsorship requests having offer terms
// Given: A driver exists
// And: The driver has sponsorship requests with offer terms
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should show offer terms for each request
// And: Terms should include financial offer and required commitments
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with status', async () => {
// TODO: Implement test
// Scenario: Driver with sponsorship requests having different statuses
// Given: A driver exists
// And: The driver has a pending sponsorship request
// And: The driver has an accepted sponsorship request
// And: The driver has a rejected sponsorship request
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should show status for each request
// And: Pending requests should be clearly marked
// And: Accepted requests should be clearly marked
// And: Rejected requests should be clearly marked
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with duration', async () => {
// TODO: Implement test
// Scenario: Driver with sponsorship requests having duration
// Given: A driver exists
// And: The driver has sponsorship requests with duration
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should show duration for each request
// And: Duration should include start and end dates
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with financial details', async () => {
// TODO: Implement test
// Scenario: Driver with sponsorship requests having financial details
// Given: A driver exists
// And: The driver has sponsorship requests with financial offers
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should show financial details for each request
// And: Financial details should include offer amount and payment terms
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with requirements', async () => {
// TODO: Implement test
// Scenario: Driver with sponsorship requests having requirements
// Given: A driver exists
// And: The driver has sponsorship requests with requirements
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should show requirements for each request
// And: Requirements should include deliverables and commitments
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with expiration date', async () => {
// TODO: Implement test
// Scenario: Driver with sponsorship requests having expiration dates
// Given: A driver exists
// And: The driver has sponsorship requests with expiration dates
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should show expiration date for each request
// And: The date should be formatted correctly
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with creation date', async () => {
// TODO: Implement test
// Scenario: Driver with sponsorship requests having creation dates
// Given: A driver exists
// And: The driver has sponsorship requests with creation dates
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should show creation date for each request
// And: The date should be formatted correctly
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should retrieve sponsorship requests with revenue tracking', async () => {
// TODO: Implement test
// Scenario: Driver with sponsorship requests having revenue tracking
// Given: A driver exists
// And: The driver has accepted sponsorship requests with revenue tracking
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should show revenue tracking for each request
// And: Revenue tracking should include total earnings and payment history
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
});
describe('GetProfileSponsorshipRequestsUseCase - Edge Cases', () => {
it('should handle driver with no sponsorship requests', async () => {
// TODO: Implement test
// Scenario: Driver without sponsorship requests
// Given: A driver exists without sponsorship requests
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should contain empty list
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should handle driver with only pending requests', async () => {
// TODO: Implement test
// Scenario: Driver with only pending requests
// Given: A driver exists
// And: The driver has only pending sponsorship requests
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should contain only pending requests
// And: All requests should show Pending status
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should handle driver with only accepted requests', async () => {
// TODO: Implement test
// Scenario: Driver with only accepted requests
// Given: A driver exists
// And: The driver has only accepted sponsorship requests
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should contain only accepted requests
// And: All requests should show Accepted status
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should handle driver with only rejected requests', async () => {
// TODO: Implement test
// Scenario: Driver with only rejected requests
// Given: A driver exists
// And: The driver has only rejected sponsorship requests
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should contain only rejected requests
// And: All requests should show Rejected status
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
it('should handle driver with expired requests', async () => {
// TODO: Implement test
// Scenario: Driver with expired requests
// Given: A driver exists
// And: The driver has sponsorship requests that have expired
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with driver ID
// Then: The result should contain expired requests
// And: Expired requests should be clearly marked
// And: EventPublisher should emit ProfileSponsorshipRequestsAccessedEvent
});
});
describe('GetProfileSponsorshipRequestsUseCase - 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
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when driver ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid driver ID
// Given: An invalid driver ID (e.g., empty string, null, undefined)
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with invalid driver 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 driver exists
// And: DriverRepository throws an error during query
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('GetSponsorshipRequestDetailsUseCase - Success Path', () => {
it('should retrieve complete sponsorship request details', async () => {
// TODO: Implement test
// Scenario: Sponsorship request with complete details
// Given: A sponsorship request exists with complete information
// And: The request has sponsor info, offer terms, duration, requirements
// When: GetSponsorshipRequestDetailsUseCase.execute() is called with request ID
// Then: The result should contain all request details
// And: EventPublisher should emit SponsorshipRequestDetailsAccessedEvent
});
it('should retrieve sponsorship request details with minimal information', async () => {
// TODO: Implement test
// Scenario: Sponsorship request with minimal details
// Given: A sponsorship request exists with minimal information
// And: The request has only sponsor name and offer amount
// When: GetSponsorshipRequestDetailsUseCase.execute() is called with request ID
// Then: The result should contain basic request details
// And: EventPublisher should emit SponsorshipRequestDetailsAccessedEvent
});
it('should retrieve sponsorship request details with sponsor information', async () => {
// TODO: Implement test
// Scenario: Sponsorship request with sponsor info
// Given: A sponsorship request exists with sponsor details
// When: GetSponsorshipRequestDetailsUseCase.execute() is called with request ID
// Then: The result should show sponsor information
// And: Sponsor info should include name, logo, and description
// And: EventPublisher should emit SponsorshipRequestDetailsAccessedEvent
});
it('should retrieve sponsorship request details with offer terms', async () => {
// TODO: Implement test
// Scenario: Sponsorship request with offer terms
// Given: A sponsorship request exists with offer terms
// When: GetSponsorshipRequestDetailsUseCase.execute() is called with request ID
// Then: The result should show offer terms
// And: Terms should include financial offer and required commitments
// And: EventPublisher should emit SponsorshipRequestDetailsAccessedEvent
});
it('should retrieve sponsorship request details with duration', async () => {
// TODO: Implement test
// Scenario: Sponsorship request with duration
// Given: A sponsorship request exists with duration
// When: GetSponsorshipRequestDetailsUseCase.execute() is called with request ID
// Then: The result should show duration
// And: Duration should include start and end dates
// And: EventPublisher should emit SponsorshipRequestDetailsAccessedEvent
});
it('should retrieve sponsorship request details with financial details', async () => {
// TODO: Implement test
// Scenario: Sponsorship request with financial details
// Given: A sponsorship request exists with financial details
// When: GetSponsorshipRequestDetailsUseCase.execute() is called with request ID
// Then: The result should show financial details
// And: Financial details should include offer amount and payment terms
// And: EventPublisher should emit SponsorshipRequestDetailsAccessedEvent
});
it('should retrieve sponsorship request details with requirements', async () => {
// TODO: Implement test
// Scenario: Sponsorship request with requirements
// Given: A sponsorship request exists with requirements
// When: GetSponsorshipRequestDetailsUseCase.execute() is called with request ID
// Then: The result should show requirements
// And: Requirements should include deliverables and commitments
// And: EventPublisher should emit SponsorshipRequestDetailsAccessedEvent
});
});
describe('GetSponsorshipRequestDetailsUseCase - Error Handling', () => {
it('should throw error when sponsorship request does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent sponsorship request
// Given: No sponsorship request exists with the given ID
// When: GetSponsorshipRequestDetailsUseCase.execute() is called with non-existent request ID
// Then: Should throw SponsorshipRequestNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when sponsorship request ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid sponsorship request ID
// Given: An invalid sponsorship request ID (e.g., empty string, null, undefined)
// When: GetSponsorshipRequestDetailsUseCase.execute() is called with invalid request ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('AcceptSponsorshipRequestUseCase - Success Path', () => {
it('should allow driver to accept a sponsorship offer', async () => {
// TODO: Implement test
// Scenario: Driver accepts a sponsorship offer
// Given: A driver exists
// And: The driver has a pending sponsorship request
// When: AcceptSponsorshipRequestUseCase.execute() is called with driver ID and request ID
// Then: The sponsorship should be accepted
// And: EventPublisher should emit SponsorshipAcceptedEvent
});
it('should allow driver to accept multiple sponsorship offers', async () => {
// TODO: Implement test
// Scenario: Driver accepts multiple sponsorship offers
// Given: A driver exists
// And: The driver has 3 pending sponsorship requests
// When: AcceptSponsorshipRequestUseCase.execute() is called for each request
// Then: All sponsorships should be accepted
// And: EventPublisher should emit SponsorshipAcceptedEvent for each request
});
it('should allow driver to accept sponsorship with revenue tracking', async () => {
// TODO: Implement test
// Scenario: Driver accepts sponsorship with revenue tracking
// Given: A driver exists
// And: The driver has a pending sponsorship request with revenue tracking
// When: AcceptSponsorshipRequestUseCase.execute() is called with driver ID and request ID
// Then: The sponsorship should be accepted
// And: Revenue tracking should be initialized
// And: EventPublisher should emit SponsorshipAcceptedEvent
});
});
describe('AcceptSponsorshipRequestUseCase - Validation', () => {
it('should reject accepting sponsorship when request is not pending', async () => {
// TODO: Implement test
// Scenario: Request not pending
// Given: A driver exists
// And: The driver has an accepted sponsorship request
// When: AcceptSponsorshipRequestUseCase.execute() is called with driver ID and request ID
// Then: Should throw NotPendingError
// And: EventPublisher should NOT emit any events
});
it('should reject accepting sponsorship with invalid request ID', async () => {
// TODO: Implement test
// Scenario: Invalid request ID
// Given: A driver exists
// When: AcceptSponsorshipRequestUseCase.execute() is called with invalid request ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('AcceptSponsorshipRequestUseCase - 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
// When: AcceptSponsorshipRequestUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when sponsorship request does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent sponsorship request
// Given: A driver exists
// And: No sponsorship request exists with the given ID
// When: AcceptSponsorshipRequestUseCase.execute() is called with non-existent request ID
// Then: Should throw SponsorshipRequestNotFoundError
// 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: SponsorshipRepository throws an error during update
// When: AcceptSponsorshipRequestUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('RejectSponsorshipRequestUseCase - Success Path', () => {
it('should allow driver to reject a sponsorship offer', async () => {
// TODO: Implement test
// Scenario: Driver rejects a sponsorship offer
// Given: A driver exists
// And: The driver has a pending sponsorship request
// When: RejectSponsorshipRequestUseCase.execute() is called with driver ID and request ID
// Then: The sponsorship should be rejected
// And: EventPublisher should emit SponsorshipRejectedEvent
});
it('should allow driver to reject multiple sponsorship offers', async () => {
// TODO: Implement test
// Scenario: Driver rejects multiple sponsorship offers
// Given: A driver exists
// And: The driver has 3 pending sponsorship requests
// When: RejectSponsorshipRequestUseCase.execute() is called for each request
// Then: All sponsorships should be rejected
// And: EventPublisher should emit SponsorshipRejectedEvent for each request
});
it('should allow driver to reject sponsorship with reason', async () => {
// TODO: Implement test
// Scenario: Driver rejects sponsorship with reason
// Given: A driver exists
// And: The driver has a pending sponsorship request
// When: RejectSponsorshipRequestUseCase.execute() is called with driver ID, request ID, and reason
// Then: The sponsorship should be rejected
// And: The rejection reason should be recorded
// And: EventPublisher should emit SponsorshipRejectedEvent
});
});
describe('RejectSponsorshipRequestUseCase - Validation', () => {
it('should reject rejecting sponsorship when request is not pending', async () => {
// TODO: Implement test
// Scenario: Request not pending
// Given: A driver exists
// And: The driver has an accepted sponsorship request
// When: RejectSponsorshipRequestUseCase.execute() is called with driver ID and request ID
// Then: Should throw NotPendingError
// And: EventPublisher should NOT emit any events
});
it('should reject rejecting sponsorship with invalid request ID', async () => {
// TODO: Implement test
// Scenario: Invalid request ID
// Given: A driver exists
// When: RejectSponsorshipRequestUseCase.execute() is called with invalid request ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('RejectSponsorshipRequestUseCase - 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
// When: RejectSponsorshipRequestUseCase.execute() is called with non-existent driver ID
// Then: Should throw DriverNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when sponsorship request does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent sponsorship request
// Given: A driver exists
// And: No sponsorship request exists with the given ID
// When: RejectSponsorshipRequestUseCase.execute() is called with non-existent request ID
// Then: Should throw SponsorshipRequestNotFoundError
// 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: SponsorshipRepository throws an error during update
// When: RejectSponsorshipRequestUseCase.execute() is called
// Then: Should propagate the error appropriately
// And: EventPublisher should NOT emit any events
});
});
describe('Profile Sponsorship Requests Data Orchestration', () => {
it('should correctly format sponsorship status with visual cues', async () => {
// TODO: Implement test
// Scenario: Sponsorship status formatting
// Given: A driver exists
// And: The driver has a pending sponsorship request
// And: The driver has an accepted sponsorship request
// And: The driver has a rejected sponsorship request
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: Pending requests should show "Pending" status with yellow indicator
// And: Accepted requests should show "Accepted" status with green indicator
// And: Rejected requests should show "Rejected" status with red indicator
});
it('should correctly format sponsorship duration', async () => {
// TODO: Implement test
// Scenario: Sponsorship duration formatting
// Given: A driver exists
// And: The driver has a sponsorship request with duration from 2024-01-15 to 2024-12-31
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: Duration should show as "January 15, 2024 - December 31, 2024" or similar format
});
it('should correctly format financial offer as currency', async () => {
// TODO: Implement test
// Scenario: Financial offer formatting
// Given: A driver exists
// And: The driver has a sponsorship request with offer $1000
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: Financial offer should show as "$1,000" or "1000 USD"
});
it('should correctly format sponsorship expiration date', async () => {
// TODO: Implement test
// Scenario: Sponsorship expiration date formatting
// Given: A driver exists
// And: The driver has a sponsorship request with expiration date 2024-06-30
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: Expiration date should show as "June 30, 2024" or similar format
});
it('should correctly format sponsorship creation date', async () => {
// TODO: Implement test
// Scenario: Sponsorship creation date formatting
// Given: A driver exists
// And: The driver has a sponsorship request created on 2024-01-15
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: Creation date should show as "January 15, 2024" or similar format
});
it('should correctly filter sponsorship requests by status', async () => {
// TODO: Implement test
// Scenario: Sponsorship filtering by status
// Given: A driver exists
// And: The driver has 2 pending requests and 1 accepted request
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with status filter "Pending"
// Then: The result should show only the 2 pending requests
// And: The accepted request should be hidden
});
it('should correctly search sponsorship requests by sponsor name', async () => {
// TODO: Implement test
// Scenario: Sponsorship search by sponsor name
// Given: A driver exists
// And: The driver has sponsorship requests from "Sponsor A" and "Sponsor B"
// When: GetProfileSponsorshipRequestsUseCase.execute() is called with search term "Sponsor A"
// Then: The result should show only "Sponsor A" request
// And: "Sponsor B" request should be hidden
});
it('should correctly identify sponsorship request owner', async () => {
// TODO: Implement test
// Scenario: Sponsorship request owner identification
// Given: A driver exists
// And: The driver has a sponsorship request
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: The request should be associated with the driver
// And: The driver should be able to accept or reject the request
});
it('should correctly handle sponsorship request with pending status', async () => {
// TODO: Implement test
// Scenario: Pending sponsorship request handling
// Given: A driver exists
// And: The driver has a pending sponsorship request
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: The request should show "Pending" status
// And: The request should show accept and reject buttons
});
it('should correctly handle sponsorship request with accepted status', async () => {
// TODO: Implement test
// Scenario: Accepted sponsorship request handling
// Given: A driver exists
// And: The driver has an accepted sponsorship request
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: The request should show "Accepted" status
// And: The request should show sponsorship details
});
it('should correctly handle sponsorship request with rejected status', async () => {
// TODO: Implement test
// Scenario: Rejected sponsorship request handling
// Given: A driver exists
// And: The driver has a rejected sponsorship request
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: The request should show "Rejected" status
// And: The request should show rejection reason (if available)
});
it('should correctly calculate sponsorship revenue tracking', async () => {
// TODO: Implement test
// Scenario: Sponsorship revenue tracking calculation
// Given: A driver exists
// And: The driver has an accepted sponsorship request with $1000 offer
// And: The sponsorship has 2 payments of $500 each
// When: GetProfileSponsorshipRequestsUseCase.execute() is called
// Then: Revenue tracking should show total earnings of $1000
// And: Revenue tracking should show payment history with 2 payments
});
});
});

View File

@@ -1,303 +0,0 @@
/**
* 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);
});
});
});

View File

@@ -0,0 +1,37 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ProfileTestContext } from '../ProfileTestContext';
import { GetDriverLiveriesUseCase } from '../../../../core/racing/application/use-cases/GetDriverLiveriesUseCase';
import { DriverLivery } from '../../../../core/racing/domain/entities/DriverLivery';
describe('GetDriverLiveriesUseCase', () => {
let context: ProfileTestContext;
let useCase: GetDriverLiveriesUseCase;
beforeEach(async () => {
context = new ProfileTestContext();
useCase = new GetDriverLiveriesUseCase(context.liveryRepository, context.logger);
await context.clear();
});
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 context.liveryRepository.createDriverLivery(livery);
// When: GetDriverLiveriesUseCase.execute() is called
const result = await useCase.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');
});
});

View File

@@ -0,0 +1,50 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ProfileTestContext } from '../ProfileTestContext';
import { GetLeagueMembershipsUseCase } from '../../../../core/racing/application/use-cases/GetLeagueMembershipsUseCase';
import { Driver } from '../../../../core/racing/domain/entities/Driver';
import { League } from '../../../../core/racing/domain/entities/League';
import { LeagueMembership } from '../../../../core/racing/domain/entities/LeagueMembership';
describe('GetLeagueMembershipsUseCase', () => {
let context: ProfileTestContext;
let useCase: GetLeagueMembershipsUseCase;
beforeEach(async () => {
context = new ProfileTestContext();
useCase = new GetLeagueMembershipsUseCase(
context.leagueMembershipRepository,
context.driverRepository,
context.leagueRepository
);
await context.clear();
});
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 context.leagueRepository.create(league);
const membership = LeagueMembership.create({
id: 'm1',
leagueId,
driverId,
role: 'member',
status: 'active'
});
await context.leagueMembershipRepository.saveMembership(membership);
const driver = Driver.create({ id: driverId, iracingId: '4', name: 'Member Driver', country: 'US' });
await context.driverRepository.create(driver);
// When: GetLeagueMembershipsUseCase.execute() is called
const result = await useCase.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);
});
});

View File

@@ -0,0 +1,56 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ProfileTestContext } from '../ProfileTestContext';
import { GetPendingSponsorshipRequestsUseCase } from '../../../../core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import { Sponsor } from '../../../../core/racing/domain/entities/sponsor/Sponsor';
import { SponsorshipRequest } from '../../../../core/racing/domain/entities/SponsorshipRequest';
import { Money } from '../../../../core/racing/domain/value-objects/Money';
describe('GetPendingSponsorshipRequestsUseCase', () => {
let context: ProfileTestContext;
let useCase: GetPendingSponsorshipRequestsUseCase;
beforeEach(async () => {
context = new ProfileTestContext();
useCase = new GetPendingSponsorshipRequestsUseCase(
context.sponsorshipRequestRepository,
context.sponsorRepository
);
await context.clear();
});
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 context.sponsorRepository.create(sponsor);
const request = SponsorshipRequest.create({
id: 'sr1',
sponsorId,
entityType: 'driver',
entityId: driverId,
tier: 'main',
offeredAmount: Money.create(1000, 'USD')
});
await context.sponsorshipRequestRepository.create(request);
// When: GetPendingSponsorshipRequestsUseCase.execute() is called
const result = await useCase.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);
});
});

View File

@@ -0,0 +1,94 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ProfileTestContext } from '../ProfileTestContext';
import { GetProfileOverviewUseCase } from '../../../../core/racing/application/use-cases/GetProfileOverviewUseCase';
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';
describe('GetProfileOverviewUseCase', () => {
let context: ProfileTestContext;
let useCase: GetProfileOverviewUseCase;
beforeEach(async () => {
context = new ProfileTestContext();
const driverStatsUseCase = new DriverStatsUseCase(
context.resultRepository,
context.standingRepository,
context.driverStatsRepository,
context.logger
);
const rankingUseCase = new RankingUseCase(
context.standingRepository,
context.driverRepository,
context.driverStatsRepository,
context.logger
);
useCase = new GetProfileOverviewUseCase(
context.driverRepository,
context.teamRepository,
context.teamMembershipRepository,
context.socialRepository,
context.driverExtendedProfileProvider,
driverStatsUseCase,
rankingUseCase
);
await context.clear();
});
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 context.driverRepository.create(driver);
await context.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'
} as any);
const team = Team.create({ id: 't1', name: 'Team 1', tag: 'T1', description: 'Desc', ownerId: 'other', leagues: [] });
await context.teamRepository.create(team);
await context.teamMembershipRepository.saveMembership({
teamId: 't1',
driverId: driverId,
role: 'driver',
status: 'active',
joinedAt: new Date()
});
context.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 useCase.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);
});
it('should return error when driver does not exist', async () => {
const result = await useCase.execute({ driverId: 'non-existent' });
expect(result.isErr()).toBe(true);
expect((result.error as any).code).toBe('DRIVER_NOT_FOUND');
});
});

View File

@@ -0,0 +1,44 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ProfileTestContext } from '../ProfileTestContext';
import { UpdateDriverProfileUseCase } from '../../../../core/racing/application/use-cases/UpdateDriverProfileUseCase';
import { Driver } from '../../../../core/racing/domain/entities/Driver';
describe('UpdateDriverProfileUseCase', () => {
let context: ProfileTestContext;
let useCase: UpdateDriverProfileUseCase;
beforeEach(async () => {
context = new ProfileTestContext();
useCase = new UpdateDriverProfileUseCase(context.driverRepository, context.logger);
await context.clear();
});
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 context.driverRepository.create(driver);
// When: UpdateDriverProfileUseCase.execute() is called
const result = await useCase.execute({
driverId,
bio: 'New bio',
country: 'DE',
});
// Then: The driver should be updated
expect(result.isOk()).toBe(true);
const updatedDriver = await context.driverRepository.findById(driverId);
expect(updatedDriver?.bio?.toString()).toBe('New bio');
expect(updatedDriver?.country.toString()).toBe('DE');
});
it('should return error when driver does not exist', async () => {
const result = await useCase.execute({
driverId: 'non-existent',
bio: 'New bio',
});
expect(result.isErr()).toBe(true);
expect((result.error as any).code).toBe('DRIVER_NOT_FOUND');
});
});

View File

@@ -0,0 +1,54 @@
import { Logger } from '../../../core/shared/domain/Logger';
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 { InMemoryPenaltyRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryPenaltyRepository';
import { InMemoryProtestRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryProtestRepository';
export class RacesTestContext {
public readonly logger: Logger;
public readonly raceRepository: InMemoryRaceRepository;
public readonly leagueRepository: InMemoryLeagueRepository;
public readonly driverRepository: InMemoryDriverRepository;
public readonly raceRegistrationRepository: InMemoryRaceRegistrationRepository;
public readonly resultRepository: InMemoryResultRepository;
public readonly leagueMembershipRepository: InMemoryLeagueMembershipRepository;
public readonly penaltyRepository: InMemoryPenaltyRepository;
public readonly protestRepository: InMemoryProtestRepository;
private constructor() {
this.logger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
this.raceRepository = new InMemoryRaceRepository(this.logger);
this.leagueRepository = new InMemoryLeagueRepository(this.logger);
this.driverRepository = new InMemoryDriverRepository(this.logger);
this.raceRegistrationRepository = new InMemoryRaceRegistrationRepository(this.logger);
this.resultRepository = new InMemoryResultRepository(this.logger, this.raceRepository);
this.leagueMembershipRepository = new InMemoryLeagueMembershipRepository(this.logger);
this.penaltyRepository = new InMemoryPenaltyRepository(this.logger);
this.protestRepository = new InMemoryProtestRepository(this.logger);
}
public static create(): RacesTestContext {
return new RacesTestContext();
}
public async clear(): Promise<void> {
(this.raceRepository as any).races.clear();
this.leagueRepository.clear();
await this.driverRepository.clear();
(this.raceRegistrationRepository as any).registrations.clear();
(this.resultRepository as any).results.clear();
this.leagueMembershipRepository.clear();
(this.penaltyRepository as any).penalties.clear();
(this.protestRepository as any).protests.clear();
}
}

View File

@@ -0,0 +1,98 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RacesTestContext } from '../RacesTestContext';
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';
describe('GetRaceDetailUseCase', () => {
let context: RacesTestContext;
let getRaceDetailUseCase: GetRaceDetailUseCase;
beforeAll(() => {
context = RacesTestContext.create();
getRaceDetailUseCase = new GetRaceDetailUseCase(
context.raceRepository,
context.leagueRepository,
context.driverRepository,
context.raceRegistrationRepository,
context.resultRepository,
context.leagueMembershipRepository
);
});
beforeEach(async () => {
await context.clear();
});
it('should retrieve race detail with complete information', async () => {
// Given: A race and league exist
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.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 context.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.toString()).toBe(leagueId);
expect(data.isUserRegistered).toBe(false);
});
it('should throw error when race does not exist', async () => {
// When: GetRaceDetailUseCase.execute() is called with non-existent race ID
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 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 context.raceRepository.create(race);
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
// Mock registration
await context.raceRegistrationRepository.register({
raceId: raceId as any,
driverId: driverId as any,
registeredAt: new Date()
} as any);
// When: GetRaceDetailUseCase.execute() is called with driverId
const result = await getRaceDetailUseCase.execute({ raceId, driverId });
// Then: isUserRegistered should be true
expect(result.isOk()).toBe(true);
expect(result.unwrap().isUserRegistered).toBe(true);
});
});

View File

@@ -0,0 +1,105 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RacesTestContext } from '../RacesTestContext';
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';
describe('GetAllRacesUseCase', () => {
let context: RacesTestContext;
let getAllRacesUseCase: GetAllRacesUseCase;
beforeAll(() => {
context = RacesTestContext.create();
getAllRacesUseCase = new GetAllRacesUseCase(
context.raceRepository,
context.leagueRepository,
context.logger
);
});
beforeEach(async () => {
await context.clear();
});
it('should retrieve comprehensive list of all races', async () => {
// Given: Multiple races exist
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.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 context.raceRepository.create(race1);
await context.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 return empty list when no races exist', async () => {
// When: GetAllRacesUseCase.execute() is called
const result = await getAllRacesUseCase.execute({});
// Then: The result should be empty
expect(result.isOk()).toBe(true);
expect(result.unwrap().races).toHaveLength(0);
expect(result.unwrap().totalCount).toBe(0);
});
it('should retrieve upcoming and recent races (main page logic)', async () => {
// Given: Upcoming and completed races exist
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.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 context.raceRepository.create(upcomingRace);
await context.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);
});
});

View File

@@ -1,145 +0,0 @@
/**
* Integration Test: Race Detail Use Case Orchestration
*
* Tests the orchestration logic of race detail page-related Use Cases:
* - GetRaceDetailUseCase: Retrieves comprehensive race details
*
* 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 { 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 leagueRepository: InMemoryLeagueRepository;
let driverRepository: InMemoryDriverRepository;
let raceRegistrationRepository: InMemoryRaceRegistrationRepository;
let resultRepository: InMemoryResultRepository;
let leagueMembershipRepository: InMemoryLeagueMembershipRepository;
let getRaceDetailUseCase: GetRaceDetailUseCase;
let mockLogger: Logger;
beforeAll(() => {
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(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', () => {
it('should retrieve race detail with complete information', async () => {
// 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 throw error when race does not exist', async () => {
// When: GetRaceDetailUseCase.execute() is called with non-existent race ID
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 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);
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await driverRepository.create(driver);
// 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);
// When: GetRaceDetailUseCase.execute() is called with driverId
const result = await getRaceDetailUseCase.execute({ raceId, driverId });
// Then: isUserRegistered should be true
expect(result.isOk()).toBe(true);
expect(result.unwrap().isUserRegistered).toBe(true);
});
});
});

View File

@@ -1,159 +0,0 @@
/**
* Integration Test: Race Results Use Case Orchestration
*
* Tests the orchestration logic of race results page-related Use Cases:
* - GetRaceResultsDetailUseCase: Retrieves complete race results (all finishers)
* - GetRacePenaltiesUseCase: Retrieves race penalties and incidents
*
* 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 { 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 leagueRepository: InMemoryLeagueRepository;
let driverRepository: InMemoryDriverRepository;
let resultRepository: InMemoryResultRepository;
let penaltyRepository: InMemoryPenaltyRepository;
let getRaceResultsDetailUseCase: GetRaceResultsDetailUseCase;
let getRacePenaltiesUseCase: GetRacePenaltiesUseCase;
let mockLogger: Logger;
beforeAll(() => {
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(async () => {
(raceRepository as any).races.clear();
leagueRepository.clear();
await driverRepository.clear();
(resultRepository as any).results.clear();
(penaltyRepository as any).penalties.clear();
});
describe('GetRaceResultsDetailUseCase', () => {
it('should retrieve complete race results with all finishers', async () => {
// 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);
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);
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await driverRepository.create(driver);
const raceResult = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 1,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25
});
await resultRepository.create(raceResult);
// When: GetRaceResultsDetailUseCase.execute() is called
const result = await getRaceResultsDetailUseCase.execute({ raceId });
// 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('GetRacePenaltiesUseCase', () => {
it('should retrieve race penalties with driver information', async () => {
// Given: A race with penalties
const raceId = 'r1';
const driverId = 'd1';
const stewardId = 's1';
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);
const penalty = Penalty.create({
id: 'p1',
raceId,
driverId,
type: 'time',
value: 5,
reason: 'Track limits',
issuedBy: stewardId,
status: 'applied'
});
await penaltyRepository.create(penalty);
// When: GetRacePenaltiesUseCase.execute() is called
const result = await getRacePenaltiesUseCase.execute({ raceId });
// 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);
});
});
});

View File

@@ -1,177 +0,0 @@
/**
* Integration Test: Race Stewarding Use Case Orchestration
*
* Tests the orchestration logic of race stewarding page-related Use Cases:
* - GetLeagueProtestsUseCase: Retrieves comprehensive race stewarding information
* - ReviewProtestUseCase: Reviews a protest
*
* 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 { InMemoryRaceRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { InMemoryProtestRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryProtestRepository';
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryLeagueRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
import { InMemoryLeagueMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
import { GetLeagueProtestsUseCase } from '../../../core/racing/application/use-cases/GetLeagueProtestsUseCase';
import { ReviewProtestUseCase } from '../../../core/racing/application/use-cases/ReviewProtestUseCase';
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 { Protest } from '../../../core/racing/domain/entities/Protest';
import { LeagueMembership } from '../../../core/racing/domain/entities/LeagueMembership';
import { Logger } from '../../../core/shared/domain/Logger';
describe('Race Stewarding Use Case Orchestration', () => {
let raceRepository: InMemoryRaceRepository;
let protestRepository: InMemoryProtestRepository;
let driverRepository: InMemoryDriverRepository;
let leagueRepository: InMemoryLeagueRepository;
let leagueMembershipRepository: InMemoryLeagueMembershipRepository;
let getLeagueProtestsUseCase: GetLeagueProtestsUseCase;
let reviewProtestUseCase: ReviewProtestUseCase;
let mockLogger: Logger;
beforeAll(() => {
mockLogger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
raceRepository = new InMemoryRaceRepository(mockLogger);
protestRepository = new InMemoryProtestRepository(mockLogger);
driverRepository = new InMemoryDriverRepository(mockLogger);
leagueRepository = new InMemoryLeagueRepository(mockLogger);
leagueMembershipRepository = new InMemoryLeagueMembershipRepository(mockLogger);
getLeagueProtestsUseCase = new GetLeagueProtestsUseCase(
raceRepository,
protestRepository,
driverRepository,
leagueRepository
);
reviewProtestUseCase = new ReviewProtestUseCase(
protestRepository,
raceRepository,
leagueMembershipRepository
);
});
beforeEach(async () => {
(raceRepository as any).races.clear();
(protestRepository as any).protests.clear();
await driverRepository.clear();
leagueRepository.clear();
leagueMembershipRepository.clear();
});
describe('GetLeagueProtestsUseCase', () => {
it('should retrieve league protests with all related entities', async () => {
// Given: A league, race, drivers and a protest 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(),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await raceRepository.create(race);
const driver1Id = 'd1';
const driver2Id = 'd2';
const driver1 = Driver.create({ id: driver1Id, iracingId: '100', name: 'Protester', country: 'US' });
const driver2 = Driver.create({ id: driver2Id, iracingId: '200', name: 'Accused', country: 'UK' });
await driverRepository.create(driver1);
await driverRepository.create(driver2);
const protest = Protest.create({
id: 'p1',
raceId,
protestingDriverId: driver1Id,
accusedDriverId: driver2Id,
reason: 'Unsafe rejoin',
timestamp: new Date()
});
await protestRepository.create(protest);
// When: GetLeagueProtestsUseCase.execute() is called
const result = await getLeagueProtestsUseCase.execute({ leagueId });
// Then: It should return the protest with race and driver info
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.protests).toHaveLength(1);
expect(data.protests[0].protest.id).toBe('p1');
expect(data.protests[0].race?.id).toBe(raceId);
expect(data.protests[0].protestingDriver?.id).toBe(driver1Id);
expect(data.protests[0].accusedDriver?.id).toBe(driver2Id);
});
});
describe('ReviewProtestUseCase', () => {
it('should allow a steward to review a protest', async () => {
// Given: A protest and a steward membership
const leagueId = 'l1';
const raceId = 'r1';
const stewardId = 's1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await raceRepository.create(race);
const protest = Protest.create({
id: 'p1',
raceId,
protestingDriverId: 'd1',
accusedDriverId: 'd2',
reason: 'Unsafe rejoin',
timestamp: new Date()
});
await protestRepository.create(protest);
const membership = LeagueMembership.create({
id: 'm1',
leagueId,
driverId: stewardId,
role: 'steward',
status: 'active'
});
await leagueMembershipRepository.saveMembership(membership);
// When: ReviewProtestUseCase.execute() is called
const result = await reviewProtestUseCase.execute({
protestId: 'p1',
stewardId,
decision: 'accepted',
comment: 'Clear violation'
});
// Then: The protest should be updated
expect(result.isOk()).toBe(true);
const updatedProtest = await protestRepository.findById('p1');
expect(updatedProtest?.status.toString()).toBe('accepted');
expect(updatedProtest?.reviewedBy).toBe(stewardId);
});
});
});

View File

@@ -1,99 +0,0 @@
/**
* Integration Test: All Races Use Case Orchestration
*
* Tests the orchestration logic of all races page-related Use Cases:
* - GetAllRacesUseCase: Retrieves comprehensive list of all 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, 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 leagueRepository: InMemoryLeagueRepository;
let getAllRacesUseCase: GetAllRacesUseCase;
let mockLogger: Logger;
beforeAll(() => {
mockLogger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
raceRepository = new InMemoryRaceRepository(mockLogger);
leagueRepository = new InMemoryLeagueRepository(mockLogger);
getAllRacesUseCase = new GetAllRacesUseCase(
raceRepository,
leagueRepository,
mockLogger
);
});
beforeEach(async () => {
(raceRepository as any).races.clear();
leagueRepository.clear();
});
describe('GetAllRacesUseCase', () => {
it('should retrieve comprehensive list of all races', async () => {
// Given: Multiple races exist
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 return empty list when no races exist', async () => {
// When: GetAllRacesUseCase.execute() is called
const result = await getAllRacesUseCase.execute({});
// Then: The result should be empty
expect(result.isOk()).toBe(true);
expect(result.unwrap().races).toHaveLength(0);
expect(result.unwrap().totalCount).toBe(0);
});
});
});

View File

@@ -1,89 +0,0 @@
/**
* Integration Test: Races Main Use Case Orchestration
*
* Tests the orchestration logic of races main page-related Use Cases:
* - 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, 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 leagueRepository: InMemoryLeagueRepository;
let getAllRacesUseCase: GetAllRacesUseCase;
let mockLogger: Logger;
beforeAll(() => {
mockLogger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
raceRepository = new InMemoryRaceRepository(mockLogger);
leagueRepository = new InMemoryLeagueRepository(mockLogger);
getAllRacesUseCase = new GetAllRacesUseCase(
raceRepository,
leagueRepository,
mockLogger
);
});
beforeEach(async () => {
(raceRepository as any).races.clear();
leagueRepository.clear();
});
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);
});
});
});

View File

@@ -0,0 +1,59 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RacesTestContext } from '../RacesTestContext';
import { GetRacePenaltiesUseCase } from '../../../../core/racing/application/use-cases/GetRacePenaltiesUseCase';
import { Driver } from '../../../../core/racing/domain/entities/Driver';
import { Penalty } from '../../../../core/racing/domain/entities/penalty/Penalty';
describe('GetRacePenaltiesUseCase', () => {
let context: RacesTestContext;
let getRacePenaltiesUseCase: GetRacePenaltiesUseCase;
beforeAll(() => {
context = RacesTestContext.create();
getRacePenaltiesUseCase = new GetRacePenaltiesUseCase(
context.penaltyRepository,
context.driverRepository
);
});
beforeEach(async () => {
await context.clear();
});
it('should retrieve race penalties with driver information', async () => {
// Given: A race with penalties
const leagueId = 'l1';
const raceId = 'r1';
const driverId = 'd1';
const stewardId = 's1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const steward = Driver.create({ id: stewardId, iracingId: '200', name: 'Steward', country: 'UK' });
await context.driverRepository.create(steward);
const penalty = Penalty.create({
id: 'p1',
leagueId,
raceId,
driverId,
type: 'time_penalty',
value: 5,
reason: 'Track limits',
issuedBy: stewardId,
status: 'applied'
});
await context.penaltyRepository.create(penalty);
// When: GetRacePenaltiesUseCase.execute() is called
const result = await getRacePenaltiesUseCase.execute({ raceId });
// 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);
});
});

View File

@@ -0,0 +1,73 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RacesTestContext } from '../RacesTestContext';
import { GetRaceResultsDetailUseCase } from '../../../../core/racing/application/use-cases/GetRaceResultsDetailUseCase';
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';
describe('GetRaceResultsDetailUseCase', () => {
let context: RacesTestContext;
let getRaceResultsDetailUseCase: GetRaceResultsDetailUseCase;
beforeAll(() => {
context = RacesTestContext.create();
getRaceResultsDetailUseCase = new GetRaceResultsDetailUseCase(
context.raceRepository,
context.leagueRepository,
context.resultRepository,
context.driverRepository,
context.penaltyRepository
);
});
beforeEach(async () => {
await context.clear();
});
it('should retrieve complete race results with all finishers', async () => {
// Given: A completed race with results
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(Date.now() - 86400000),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const driverId = 'd1';
const driver = Driver.create({ id: driverId, iracingId: '100', name: 'John Doe', country: 'US' });
await context.driverRepository.create(driver);
const raceResult = RaceResult.create({
id: 'res1',
raceId,
driverId,
position: 1,
lapsCompleted: 20,
totalTime: 3600,
fastestLap: 105,
points: 25,
incidents: 0,
startPosition: 1
});
await context.resultRepository.create(raceResult);
// When: GetRaceResultsDetailUseCase.execute() is called
const result = await getRaceResultsDetailUseCase.execute({ raceId });
// 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);
});
});

View File

@@ -0,0 +1,73 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RacesTestContext } from '../RacesTestContext';
import { GetLeagueProtestsUseCase } from '../../../../core/racing/application/use-cases/GetLeagueProtestsUseCase';
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 { Protest } from '../../../../core/racing/domain/entities/Protest';
describe('GetLeagueProtestsUseCase', () => {
let context: RacesTestContext;
let getLeagueProtestsUseCase: GetLeagueProtestsUseCase;
beforeAll(() => {
context = RacesTestContext.create();
getLeagueProtestsUseCase = new GetLeagueProtestsUseCase(
context.raceRepository,
context.protestRepository,
context.driverRepository,
context.leagueRepository
);
});
beforeEach(async () => {
await context.clear();
});
it('should retrieve league protests with all related entities', async () => {
// Given: A league, race, drivers and a protest exist
const leagueId = 'l1';
const league = League.create({ id: leagueId, name: 'Pro League', description: 'Desc', ownerId: 'o1' });
await context.leagueRepository.create(league);
const raceId = 'r1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const driver1Id = 'd1';
const driver2Id = 'd2';
const driver1 = Driver.create({ id: driver1Id, iracingId: '100', name: 'Protester', country: 'US' });
const driver2 = Driver.create({ id: driver2Id, iracingId: '200', name: 'Accused', country: 'UK' });
await context.driverRepository.create(driver1);
await context.driverRepository.create(driver2);
const protest = Protest.create({
id: 'p1',
raceId,
protestingDriverId: driver1Id,
accusedDriverId: driver2Id,
incident: { lap: 1, description: 'Unsafe rejoin' },
timestamp: new Date()
});
await context.protestRepository.create(protest);
// When: GetLeagueProtestsUseCase.execute() is called
const result = await getLeagueProtestsUseCase.execute({ leagueId });
// Then: It should return the protest with race and driver info
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.protests).toHaveLength(1);
expect(data.protests[0].protest.id).toBe('p1');
expect(data.protests[0].race?.id).toBe(raceId);
expect(data.protests[0].protestingDriver?.id).toBe(driver1Id);
expect(data.protests[0].accusedDriver?.id).toBe(driver2Id);
});
});

View File

@@ -0,0 +1,75 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { RacesTestContext } from '../RacesTestContext';
import { ReviewProtestUseCase } from '../../../../core/racing/application/use-cases/ReviewProtestUseCase';
import { Race } from '../../../../core/racing/domain/entities/Race';
import { Protest } from '../../../../core/racing/domain/entities/Protest';
import { LeagueMembership } from '../../../../core/racing/domain/entities/LeagueMembership';
describe('ReviewProtestUseCase', () => {
let context: RacesTestContext;
let reviewProtestUseCase: ReviewProtestUseCase;
beforeAll(() => {
context = RacesTestContext.create();
reviewProtestUseCase = new ReviewProtestUseCase(
context.protestRepository,
context.raceRepository,
context.leagueMembershipRepository,
context.logger
);
});
beforeEach(async () => {
await context.clear();
});
it('should allow a steward to review a protest', async () => {
// Given: A protest and a steward membership
const leagueId = 'l1';
const raceId = 'r1';
const stewardId = 's1';
const race = Race.create({
id: raceId,
leagueId,
scheduledAt: new Date(),
track: 'Spa',
car: 'GT3',
status: 'completed'
});
await context.raceRepository.create(race);
const protest = Protest.create({
id: 'p1',
raceId,
protestingDriverId: 'd1',
accusedDriverId: 'd2',
incident: { lap: 1, description: 'Unsafe rejoin' },
filedAt: new Date()
});
await context.protestRepository.create(protest);
const membership = LeagueMembership.create({
id: 'm1',
leagueId,
driverId: stewardId,
role: 'admin',
status: 'active'
});
await context.leagueMembershipRepository.saveMembership(membership);
// When: ReviewProtestUseCase.execute() is called
const result = await reviewProtestUseCase.execute({
protestId: 'p1',
stewardId,
decision: 'uphold',
decisionNotes: 'Clear violation'
});
// Then: The protest should be updated
expect(result.isOk()).toBe(true);
const updatedProtest = await context.protestRepository.findById('p1');
expect(updatedProtest?.status.toString()).toBe('upheld');
expect(updatedProtest?.reviewedBy).toBe(stewardId);
});
});

View File

@@ -0,0 +1,54 @@
import { Logger } from '../../../core/shared/domain/Logger';
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 { InMemoryPaymentRepository } from '../../../adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
import { InMemorySponsorshipPricingRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
export class SponsorTestContext {
public readonly logger: Logger;
public readonly sponsorRepository: InMemorySponsorRepository;
public readonly seasonSponsorshipRepository: InMemorySeasonSponsorshipRepository;
public readonly seasonRepository: InMemorySeasonRepository;
public readonly leagueRepository: InMemoryLeagueRepository;
public readonly leagueMembershipRepository: InMemoryLeagueMembershipRepository;
public readonly raceRepository: InMemoryRaceRepository;
public readonly paymentRepository: InMemoryPaymentRepository;
public readonly sponsorshipPricingRepository: InMemorySponsorshipPricingRepository;
public readonly eventPublisher: InMemoryEventPublisher;
constructor() {
this.logger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
this.sponsorRepository = new InMemorySponsorRepository(this.logger);
this.seasonSponsorshipRepository = new InMemorySeasonSponsorshipRepository(this.logger);
this.seasonRepository = new InMemorySeasonRepository(this.logger);
this.leagueRepository = new InMemoryLeagueRepository(this.logger);
this.leagueMembershipRepository = new InMemoryLeagueMembershipRepository(this.logger);
this.raceRepository = new InMemoryRaceRepository(this.logger);
this.paymentRepository = new InMemoryPaymentRepository(this.logger);
this.sponsorshipPricingRepository = new InMemorySponsorshipPricingRepository(this.logger);
this.eventPublisher = new InMemoryEventPublisher();
}
public clear(): void {
this.sponsorRepository.clear();
this.seasonSponsorshipRepository.clear();
this.seasonRepository.clear();
this.leagueRepository.clear();
this.leagueMembershipRepository.clear();
this.raceRepository.clear();
this.paymentRepository.clear();
this.sponsorshipPricingRepository.clear();
this.eventPublisher.clear();
}
}

View File

@@ -0,0 +1,181 @@
import { describe, it, expect, beforeEach } from 'vitest';
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 { SponsorTestContext } from '../SponsorTestContext';
describe('Sponsor Billing Use Case Orchestration', () => {
let context: SponsorTestContext;
let getSponsorBillingUseCase: GetSponsorBillingUseCase;
beforeEach(() => {
context = new SponsorTestContext();
getSponsorBillingUseCase = new GetSponsorBillingUseCase(
context.paymentRepository,
context.seasonSponsorshipRepository,
context.sponsorRepository,
);
});
describe('GetSponsorBillingUseCase - Success Path', () => {
it('should retrieve billing statistics for a sponsor with paid invoices', async () => {
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await context.sponsorRepository.create(sponsor);
const sponsorship1 = SeasonSponsorship.create({
id: 'sponsorship-1',
sponsorId: 'sponsor-123',
seasonId: 'season-1',
tier: 'main',
pricing: Money.create(1000, 'USD'),
status: 'active',
});
await context.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 context.seasonSponsorshipRepository.create(sponsorship2);
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 context.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 context.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 context.paymentRepository.create(payment3);
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
expect(billing.invoices).toHaveLength(3);
// Total spent = (1000 + 190) + (2000 + 380) + (3000 + 570) = 1190 + 2380 + 3570 = 7140
expect(billing.stats.totalSpent).toBe(7140);
expect(billing.stats.pendingAmount).toBe(0);
expect(billing.stats.activeSponsorships).toBe(2);
});
it('should retrieve billing statistics with pending invoices', async () => {
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await context.sponsorRepository.create(sponsor);
const sponsorship = SeasonSponsorship.create({
id: 'sponsorship-1',
sponsorId: 'sponsor-123',
seasonId: 'season-1',
tier: 'main',
pricing: Money.create(1000, 'USD'),
status: 'active',
});
await context.seasonSponsorshipRepository.create(sponsorship);
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 context.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 context.paymentRepository.create(payment2);
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
expect(billing.invoices).toHaveLength(2);
expect(billing.stats.totalSpent).toBe(1190);
expect(billing.stats.pendingAmount).toBe(595);
expect(billing.stats.nextPaymentAmount).toBe(595);
});
});
describe('GetSponsorBillingUseCase - Error Handling', () => {
it('should return error when sponsor does not exist', async () => {
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'non-existent-sponsor' });
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('SPONSOR_NOT_FOUND');
});
});
});

View File

@@ -0,0 +1,131 @@
import { describe, it, expect, beforeEach } from 'vitest';
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 { SponsorTestContext } from '../SponsorTestContext';
describe('Sponsor Campaigns Use Case Orchestration', () => {
let context: SponsorTestContext;
let getSponsorSponsorshipsUseCase: GetSponsorSponsorshipsUseCase;
beforeEach(() => {
context = new SponsorTestContext();
getSponsorSponsorshipsUseCase = new GetSponsorSponsorshipsUseCase(
context.sponsorRepository,
context.seasonSponsorshipRepository,
context.seasonRepository,
context.leagueRepository,
context.leagueMembershipRepository,
context.raceRepository,
);
});
describe('GetSponsorSponsorshipsUseCase - Success Path', () => {
it('should retrieve all sponsorships for a sponsor', async () => {
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await context.sponsorRepository.create(sponsor);
const league1 = League.create({
id: 'league-1',
name: 'League 1',
description: 'Description 1',
ownerId: 'owner-1',
});
await context.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 context.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 context.seasonSponsorshipRepository.create(sponsorship1);
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 context.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 context.raceRepository.create(race);
}
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
expect(result.isOk()).toBe(true);
const sponsorships = result.unwrap();
expect(sponsorships.sponsor.name.toString()).toBe('Test Company');
expect(sponsorships.sponsorships).toHaveLength(1);
expect(sponsorships.summary.totalSponsorships).toBe(1);
expect(sponsorships.summary.activeSponsorships).toBe(1);
expect(sponsorships.summary.totalInvestment.amount).toBe(1000);
const s1 = sponsorships.sponsorships[0];
expect(s1.metrics.drivers).toBe(10);
expect(s1.metrics.races).toBe(5);
expect(s1.metrics.impressions).toBe(5000);
});
it('should retrieve sponsorships with empty result when no sponsorships exist', async () => {
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await context.sponsorRepository.create(sponsor);
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'sponsor-123' });
expect(result.isOk()).toBe(true);
const sponsorships = result.unwrap();
expect(sponsorships.sponsorships).toHaveLength(0);
expect(sponsorships.summary.totalSponsorships).toBe(0);
});
});
describe('GetSponsorSponsorshipsUseCase - Error Handling', () => {
it('should return error when sponsor does not exist', async () => {
const result = await getSponsorSponsorshipsUseCase.execute({ sponsorId: 'non-existent-sponsor' });
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('SPONSOR_NOT_FOUND');
});
});
});

View File

@@ -0,0 +1,127 @@
import { describe, it, expect, beforeEach } from 'vitest';
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 { SponsorTestContext } from '../SponsorTestContext';
describe('Sponsor Dashboard Use Case Orchestration', () => {
let context: SponsorTestContext;
let getSponsorDashboardUseCase: GetSponsorDashboardUseCase;
beforeEach(() => {
context = new SponsorTestContext();
getSponsorDashboardUseCase = new GetSponsorDashboardUseCase(
context.sponsorRepository,
context.seasonSponsorshipRepository,
context.seasonRepository,
context.leagueRepository,
context.leagueMembershipRepository,
context.raceRepository,
);
});
describe('GetSponsorDashboardUseCase - Success Path', () => {
it('should retrieve dashboard metrics for a sponsor with active sponsorships', async () => {
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await context.sponsorRepository.create(sponsor);
const league1 = League.create({
id: 'league-1',
name: 'League 1',
description: 'Description 1',
ownerId: 'owner-1',
});
await context.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 context.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 context.seasonSponsorshipRepository.create(sponsorship1);
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 context.leagueMembershipRepository.saveMembership(membership);
}
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 context.raceRepository.create(race);
}
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'sponsor-123' });
expect(result.isOk()).toBe(true);
const dashboard = result.unwrap();
expect(dashboard.sponsorName).toBe('Test Company');
expect(dashboard.metrics.races).toBe(3);
expect(dashboard.metrics.drivers).toBe(5);
expect(dashboard.sponsoredLeagues).toHaveLength(1);
expect(dashboard.investment.activeSponsorships).toBe(1);
expect(dashboard.investment.totalInvestment.amount).toBe(1000);
});
it('should retrieve dashboard with zero values when sponsor has no sponsorships', async () => {
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await context.sponsorRepository.create(sponsor);
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'sponsor-123' });
expect(result.isOk()).toBe(true);
const dashboard = result.unwrap();
expect(dashboard.metrics.impressions).toBe(0);
expect(dashboard.sponsoredLeagues).toHaveLength(0);
});
});
describe('GetSponsorDashboardUseCase - Error Handling', () => {
it('should return error when sponsor does not exist', async () => {
const result = await getSponsorDashboardUseCase.execute({ sponsorId: 'non-existent-sponsor' });
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('SPONSOR_NOT_FOUND');
});
});
});

View File

@@ -0,0 +1,64 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetEntitySponsorshipPricingUseCase } from '../../../../core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
import { SponsorTestContext } from '../SponsorTestContext';
describe('Sponsor League Detail Use Case Orchestration', () => {
let context: SponsorTestContext;
let getEntitySponsorshipPricingUseCase: GetEntitySponsorshipPricingUseCase;
beforeEach(() => {
context = new SponsorTestContext();
getEntitySponsorshipPricingUseCase = new GetEntitySponsorshipPricingUseCase(
context.sponsorshipPricingRepository,
context.logger,
);
});
describe('GetEntitySponsorshipPricingUseCase - Success Path', () => {
it('should retrieve sponsorship pricing for a league', async () => {
const leagueId = 'league-123';
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 context.sponsorshipPricingRepository.create(pricing);
const result = await getEntitySponsorshipPricingUseCase.execute({
entityType: 'league',
entityId: leagueId,
});
expect(result.isOk()).toBe(true);
const pricingResult = result.unwrap();
expect(pricingResult.entityType).toBe('league');
expect(pricingResult.entityId).toBe(leagueId);
expect(pricingResult.acceptingApplications).toBe(true);
expect(pricingResult.tiers).toHaveLength(2);
expect(pricingResult.tiers[0].name).toBe('main');
expect(pricingResult.tiers[0].price.amount).toBe(10000);
});
});
describe('GetEntitySponsorshipPricingUseCase - Error Handling', () => {
it('should return error when pricing is not configured', async () => {
const result = await getEntitySponsorshipPricingUseCase.execute({
entityType: 'league',
entityId: 'non-existent',
});
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('PRICING_NOT_CONFIGURED');
});
});
});

View File

@@ -0,0 +1,42 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { Sponsor } from '../../../../core/racing/domain/entities/sponsor/Sponsor';
import { SponsorTestContext } from '../SponsorTestContext';
import { GetSponsorUseCase } from '../../../../core/racing/application/use-cases/GetSponsorUseCase';
describe('Sponsor Settings Use Case Orchestration', () => {
let context: SponsorTestContext;
let getSponsorUseCase: GetSponsorUseCase;
beforeEach(() => {
context = new SponsorTestContext();
getSponsorUseCase = new GetSponsorUseCase(context.sponsorRepository);
});
describe('GetSponsorUseCase - Success Path', () => {
it('should retrieve sponsor profile information', async () => {
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'john@example.com',
});
await context.sponsorRepository.create(sponsor);
const result = await getSponsorUseCase.execute({ sponsorId: 'sponsor-123' });
expect(result.isOk()).toBe(true);
const { sponsor: retrievedSponsor } = result.unwrap();
expect(retrievedSponsor.name.toString()).toBe('Test Company');
expect(retrievedSponsor.contactEmail.toString()).toBe('john@example.com');
});
});
describe('GetSponsorUseCase - Error Handling', () => {
it('should return error when sponsor does not exist', async () => {
const result = await getSponsorUseCase.execute({ sponsorId: 'non-existent' });
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('SPONSOR_NOT_FOUND');
});
});
});

View File

@@ -1,45 +1,19 @@
/**
* Integration Test: Sponsor Signup Use Case Orchestration
*
* Tests the orchestration logic of sponsor signup-related Use Cases:
* - CreateSponsorUseCase: Creates a new sponsor account
* - 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, 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';
import { describe, it, expect, beforeEach } from 'vitest';
import { CreateSponsorUseCase } from '../../../../core/racing/application/use-cases/CreateSponsorUseCase';
import { Sponsor } from '../../../../core/racing/domain/entities/sponsor/Sponsor';
import { SponsorTestContext } from '../SponsorTestContext';
describe('Sponsor Signup Use Case Orchestration', () => {
let sponsorRepository: InMemorySponsorRepository;
let context: SponsorTestContext;
let createSponsorUseCase: CreateSponsorUseCase;
let mockLogger: Logger;
beforeAll(() => {
mockLogger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
sponsorRepository = new InMemorySponsorRepository(mockLogger);
createSponsorUseCase = new CreateSponsorUseCase(sponsorRepository, mockLogger);
});
beforeEach(() => {
sponsorRepository.clear();
context = new SponsorTestContext();
createSponsorUseCase = new CreateSponsorUseCase(context.sponsorRepository, context.logger);
});
describe('CreateSponsorUseCase - Success Path', () => {
it('should create a new sponsor account with valid information', async () => {
// Given: No sponsor exists with the given email
const sponsorId = 'sponsor-123';
const sponsorData = {
name: 'Test Company',
contactEmail: 'test@example.com',
@@ -47,116 +21,82 @@ describe('Sponsor Signup Use Case Orchestration', () => {
logoUrl: 'https://testcompany.com/logo.png',
};
// When: CreateSponsorUseCase.execute() is called with valid sponsor 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 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
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
expect(createdSponsor.createdAt).toBeDefined();
// And: The sponsor should be retrievable from the repository
const retrievedSponsor = await sponsorRepository.findById(createdSponsor.id.toString());
const retrievedSponsor = await context.sponsorRepository.findById(createdSponsor.id.toString());
expect(retrievedSponsor).toBeDefined();
expect(retrievedSponsor?.name.toString()).toBe('Test Company');
});
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 fields only', async () => {
// Given: No sponsor exists
const sponsorData = {
name: 'Optional Fields Company',
contactEmail: 'optional@example.com',
websiteUrl: 'https://optional.com',
};
// 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 () => {
// 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);
await context.sponsorRepository.create(existingSponsor);
// When: CreateSponsorUseCase.execute() is called with the same email
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 () => {
// Given: No sponsor exists
// When: CreateSponsorUseCase.execute() is called with invalid email
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');
@@ -164,14 +104,11 @@ describe('Sponsor Signup Use Case Orchestration', () => {
});
it('should reject sponsor creation with missing required fields', async () => {
// Given: No sponsor exists
// When: CreateSponsorUseCase.execute() is called without company name
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');
@@ -179,15 +116,12 @@ describe('Sponsor Signup Use Case Orchestration', () => {
});
it('should reject sponsor creation with invalid website URL', async () => {
// Given: No sponsor exists
// When: CreateSponsorUseCase.execute() is called with invalid URL
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');
@@ -195,14 +129,11 @@ describe('Sponsor Signup Use Case Orchestration', () => {
});
it('should reject sponsor creation with missing email', async () => {
// Given: No sponsor exists
// When: CreateSponsorUseCase.execute() is called without email
const result = await createSponsorUseCase.execute({
name: 'Test Company',
contactEmail: '',
});
// Then: Should return an error
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('VALIDATION_ERROR');
@@ -212,7 +143,6 @@ describe('Sponsor Signup Use Case Orchestration', () => {
describe('Sponsor Data Orchestration', () => {
it('should correctly create sponsor with all optional fields', async () => {
// Given: No sponsor exists
const sponsorData = {
name: 'Full Featured Company',
contactEmail: 'full@example.com',
@@ -220,10 +150,8 @@ describe('Sponsor Signup Use Case Orchestration', () => {
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;
@@ -235,7 +163,6 @@ describe('Sponsor Signup Use Case Orchestration', () => {
});
it('should generate unique IDs for each sponsor', async () => {
// Given: No sponsors exist
const sponsorData1 = {
name: 'Company 1',
contactEmail: 'company1@example.com',
@@ -245,11 +172,9 @@ describe('Sponsor Signup Use Case Orchestration', () => {
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);
@@ -260,20 +185,17 @@ describe('Sponsor Signup Use Case Orchestration', () => {
});
it('should persist sponsor in repository after creation', async () => {
// Given: No sponsor exists
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());
const retrievedSponsor = await context.sponsorRepository.findById(createdSponsor.id.toString());
expect(retrievedSponsor).toBeDefined();
expect(retrievedSponsor?.name.toString()).toBe('Persistent Company');
expect(retrievedSponsor?.contactEmail.toString()).toBe('persistent@example.com');

View File

@@ -1,568 +0,0 @@
/**
* Integration Test: Sponsor Billing Use Case Orchestration
*
* Tests the orchestration logic of sponsor billing-related Use Cases:
* - 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, 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 seasonSponsorshipRepository: InMemorySeasonSponsorshipRepository;
let paymentRepository: InMemoryPaymentRepository;
let getSponsorBillingUseCase: GetSponsorBillingUseCase;
let mockLogger: Logger;
beforeAll(() => {
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(() => {
sponsorRepository.clear();
seasonSponsorshipRepository.clear();
paymentRepository.clear();
});
describe('GetSponsorBillingUseCase - Success Path', () => {
it('should retrieve billing statistics for a sponsor with paid 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 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 billing statistics with 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 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 retrieve billing statistics with zero values when no invoices exist', 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 no invoices
// 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 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"
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 with different statuses
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
});
});
});

Some files were not shown because too many files have changed in this diff Show More