diff --git a/apps/website/lib/di-config.ts b/apps/website/lib/di-config.ts index 80558d7da..12c5b0fda 100644 --- a/apps/website/lib/di-config.ts +++ b/apps/website/lib/di-config.ts @@ -367,6 +367,7 @@ export function configureDIContainer(): void { const penalty = Penalty.create({ id: `penalty-${race.id}-${i}`, + leagueId: race.leagueId, raceId: race.id, driverId: accusedResult.driverId, type: penaltyType, @@ -390,6 +391,7 @@ export function configureDIContainer(): void { if (penalizedResult) { const penalty = Penalty.create({ id: `penalty-direct-${race.id}`, + leagueId: race.leagueId, raceId: race.id, driverId: penalizedResult.driverId, type: 'points_deduction', @@ -410,6 +412,7 @@ export function configureDIContainer(): void { if (penalizedResult) { const penalty = Penalty.create({ id: `penalty-direct-2-${race.id}`, + leagueId: race.leagueId, raceId: race.id, driverId: penalizedResult.driverId, type: 'points_deduction', diff --git a/packages/automation/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts b/packages/automation/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts index 950968773..4b84366ef 100644 --- a/packages/automation/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts +++ b/packages/automation/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts @@ -438,7 +438,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti private static readonly PAUSE_CHECK_INTERVAL = 300; /** Checkout confirmation callback - called before clicking checkout button */ - private checkoutConfirmationCallback?: (price: CheckoutPrice, state: CheckoutState) => Promise; + private checkoutConfirmationCallback: ((price: CheckoutPrice, state: CheckoutState) => Promise) | undefined; /** Page state validator instance */ private pageStateValidator: PageStateValidator; diff --git a/packages/automation/infrastructure/adapters/automation/dom/IRacingDomInteractor.ts b/packages/automation/infrastructure/adapters/automation/dom/IRacingDomInteractor.ts index f6b365b9d..a5848761d 100644 --- a/packages/automation/infrastructure/adapters/automation/dom/IRacingDomInteractor.ts +++ b/packages/automation/infrastructure/adapters/automation/dom/IRacingDomInteractor.ts @@ -508,7 +508,7 @@ export class IRacingDomInteractor { for (const sel of cands) { try { - const els = Array.from(document.querySelectorAll(sel)) as HTMLInputElement[]; + const els = Array.from(document.querySelectorAll(sel)) as HTMLElement[]; if (els.length === 0) continue; for (const el of els) { try { @@ -516,10 +516,10 @@ export class IRacingDomInteractor { (el as HTMLInputElement).checked = Boolean(should); el.dispatchEvent(new Event('change', { bubbles: true })); } else { - (el as HTMLElement).setAttribute('aria-checked', String(Boolean(should))); + el.setAttribute('aria-checked', String(Boolean(should))); el.dispatchEvent(new Event('change', { bubbles: true })); try { - (el as HTMLElement).click(); + el.click(); } catch { // ignore } @@ -615,7 +615,7 @@ export class IRacingDomInteractor { const applied = await page.evaluate( ({ sel, val }) => { try { - const els = Array.from(document.querySelectorAll(sel)) as HTMLElement[]; + const els = Array.from(document.querySelectorAll(sel)) as HTMLInputElement[]; if (els.length === 0) return false; for (const el of els) { try { diff --git a/packages/racing/application/use-cases/ApplyPenaltyUseCase.ts b/packages/racing/application/use-cases/ApplyPenaltyUseCase.ts index 0960efc40..39088438d 100644 --- a/packages/racing/application/use-cases/ApplyPenaltyUseCase.ts +++ b/packages/racing/application/use-cases/ApplyPenaltyUseCase.ts @@ -67,6 +67,7 @@ export class ApplyPenaltyUseCase // Create the penalty const penalty = Penalty.create({ id: randomUUID(), + leagueId: race.leagueId, raceId: command.raceId, driverId: command.driverId, type: command.type, diff --git a/packages/racing/domain/entities/Penalty.ts b/packages/racing/domain/entities/Penalty.ts index e3c779c21..c5426fb00 100644 --- a/packages/racing/domain/entities/Penalty.ts +++ b/packages/racing/domain/entities/Penalty.ts @@ -23,6 +23,7 @@ export type PenaltyStatus = 'pending' | 'applied' | 'appealed' | 'overturned'; export interface PenaltyProps { id: string; + leagueId: string; raceId: string; /** The driver receiving the penalty */ driverId: string; @@ -51,6 +52,7 @@ export class Penalty implements IEntity { static create(props: PenaltyProps): Penalty { if (!props.id) throw new RacingDomainValidationError('Penalty ID is required'); + if (!props.leagueId) throw new RacingDomainValidationError('League ID is required'); if (!props.raceId) throw new RacingDomainValidationError('Race ID is required'); if (!props.driverId) throw new RacingDomainValidationError('Driver ID is required'); if (!props.type) throw new RacingDomainValidationError('Penalty type is required'); @@ -72,6 +74,7 @@ export class Penalty implements IEntity { } get id(): string { return this.props.id; } + get leagueId(): string { return this.props.leagueId; } get raceId(): string { return this.props.raceId; } get driverId(): string { return this.props.driverId; } get type(): PenaltyType { return this.props.type; } diff --git a/tests/unit/application/services/OverlaySyncService.timeout.test.ts b/tests/unit/application/services/OverlaySyncService.timeout.test.ts index 8834fc9e6..88418c568 100644 --- a/tests/unit/application/services/OverlaySyncService.timeout.test.ts +++ b/tests/unit/application/services/OverlaySyncService.timeout.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest' -import { OverlayAction } from '../../../../packages/automation/application/ports/IOverlaySyncPort' +import { OverlayAction } from '../../../../packages/automation/application/ports/OverlaySyncPort' import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../../packages/automation/infrastructure/adapters/IAutomationLifecycleEmitter' import { OverlaySyncService } from '../../../../packages/automation/application/services/OverlaySyncService' @@ -11,7 +11,7 @@ class MockLifecycleEmitter implements IAutomationLifecycleEmitter { offLifecycle(cb: LifecycleCallback): void { this.callbacks.delete(cb) } - async emit(event: { type: string; actionId: string; timestamp: number }) { + async emit(event: { type: 'panel-attached'|'modal-opened'|'action-started'|'action-complete'|'action-failed'|'panel-missing'; actionId: string; timestamp: number }) { for (const cb of Array.from(this.callbacks)) { cb(event) } diff --git a/tests/unit/application/use-cases/CompleteRaceCreationUseCase.test.ts b/tests/unit/application/use-cases/CompleteRaceCreationUseCase.test.ts index 5bcf7b35e..3fd320a4b 100644 --- a/tests/unit/application/use-cases/CompleteRaceCreationUseCase.test.ts +++ b/tests/unit/application/use-cases/CompleteRaceCreationUseCase.test.ts @@ -56,7 +56,7 @@ describe('CompleteRaceCreationUseCase', () => { const state = CheckoutState.ready(); vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue( - Result.ok({ price: undefined, state, buttonHtml: 'n/a' }) + Result.ok({ price: null, state, buttonHtml: 'n/a' }) ); const result = await useCase.execute('test-session-123'); diff --git a/tests/unit/application/use-cases/ConfirmCheckoutUseCase.enhanced.test.ts b/tests/unit/application/use-cases/ConfirmCheckoutUseCase.enhanced.test.ts index aa4333458..e26d6628f 100644 --- a/tests/unit/application/use-cases/ConfirmCheckoutUseCase.enhanced.test.ts +++ b/tests/unit/application/use-cases/ConfirmCheckoutUseCase.enhanced.test.ts @@ -4,12 +4,12 @@ import { Result } from '@gridpilot/shared-result'; import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice'; import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState'; import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation'; -import type { ICheckoutService } from '@gridpilot/automation/application/ports/ICheckoutService'; -import type { ICheckoutConfirmationPort } from '@gridpilot/automation/application/ports/ICheckoutConfirmationPort'; +import type { CheckoutServicePort } from '@gridpilot/automation/application/ports/CheckoutServicePort'; +import type { CheckoutConfirmationPort } from '@gridpilot/automation/application/ports/CheckoutConfirmationPort'; describe('ConfirmCheckoutUseCase - Enhanced with Confirmation Port', () => { - let mockCheckoutService: ICheckoutService; - let mockConfirmationPort: ICheckoutConfirmationPort; + let mockCheckoutService: CheckoutServicePort; + let mockConfirmationPort: CheckoutConfirmationPort; let useCase: ConfirmCheckoutUseCase; beforeEach(() => { diff --git a/tests/unit/application/use-cases/ConfirmCheckoutUseCase.test.ts b/tests/unit/application/use-cases/ConfirmCheckoutUseCase.test.ts index 03796d03e..709bde23e 100644 --- a/tests/unit/application/use-cases/ConfirmCheckoutUseCase.test.ts +++ b/tests/unit/application/use-cases/ConfirmCheckoutUseCase.test.ts @@ -3,7 +3,7 @@ import { Result } from '../../../../packages/shared/result/Result'; import { ConfirmCheckoutUseCase } from '../../../../packages/automation/application/use-cases/ConfirmCheckoutUseCase'; import type { CheckoutServicePort } from '../../../../packages/automation/application/ports/CheckoutServicePort'; import type { CheckoutConfirmationPort } from '../../../../packages/automation/application/ports/CheckoutConfirmationPort'; -import type { CheckoutInfoDTODTO } from '../../../../packages/automation/application/dto/CheckoutInfoDTODTO'; +import type { CheckoutInfoDTO } from '../../../../packages/automation/application/dto/CheckoutInfoDTO'; import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice'; import { CheckoutState, CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState'; import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation'; diff --git a/tests/unit/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts b/tests/unit/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts index 5376a428b..0cc0d4558 100644 --- a/tests/unit/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts +++ b/tests/unit/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts @@ -8,10 +8,10 @@ import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IR import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/IPenaltyRepository'; import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/repositories/IChampionshipStandingRepository'; -import type { Season } from '@gridpilot/racing/domain/entities/Season'; +import { Season } from '@gridpilot/racing/domain/entities/Season'; import type { LeagueScoringConfig } from '@gridpilot/racing/domain/entities/LeagueScoringConfig'; -import type { Race } from '@gridpilot/racing/domain/entities/Race'; -import type { Result } from '@gridpilot/racing/domain/entities/Result'; +import { Race } from '@gridpilot/racing/domain/entities/Race'; +import { Result } from '@gridpilot/racing/domain/entities/Result'; import type { Penalty } from '@gridpilot/racing/domain/entities/Penalty'; import type { ChampionshipStanding } from '@gridpilot/racing/domain/entities/ChampionshipStanding'; import type { ChampionshipConfig } from '@gridpilot/racing/domain/types/ChampionshipConfig'; @@ -34,6 +34,30 @@ class InMemorySeasonRepository implements ISeasonRepository { return this.seasons.filter((s) => s.leagueId === leagueId); } + async create(season: Season): Promise { + this.seasons.push(season); + return season; + } + + async add(season: Season): Promise { + this.seasons.push(season); + } + + async update(season: Season): Promise { + const index = this.seasons.findIndex((s) => s.id === season.id); + if (index >= 0) { + this.seasons[index] = season; + } + } + + async listByLeague(leagueId: string): Promise { + return this.seasons.filter((s) => s.leagueId === leagueId); + } + + async listActiveByLeague(leagueId: string): Promise { + return this.seasons.filter((s) => s.leagueId === leagueId && s.status === 'active'); + } + seedSeason(season: Season): void { this.seasons.push(season); } @@ -46,6 +70,16 @@ class InMemoryLeagueScoringConfigRepository implements ILeagueScoringConfigRepos return this.configs.find((c) => c.seasonId === seasonId) || null; } + async save(config: LeagueScoringConfig): Promise { + const index = this.configs.findIndex((c) => c.id === config.id); + if (index >= 0) { + this.configs[index] = config; + } else { + this.configs.push(config); + } + return config; + } + seedConfig(config: LeagueScoringConfig): void { this.configs.push(config); } @@ -113,10 +147,60 @@ class InMemoryRaceRepository implements IRaceRepository { class InMemoryResultRepository implements IResultRepository { private results: Result[] = []; + async findById(id: string): Promise { + return this.results.find((r) => r.id === id) || null; + } + + async findAll(): Promise { + return [...this.results]; + } + async findByRaceId(raceId: string): Promise { return this.results.filter((r) => r.raceId === raceId); } + async findByDriverId(driverId: string): Promise { + return this.results.filter((r) => r.driverId === driverId); + } + + async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise { + return this.results.filter((r) => r.driverId === driverId && r.raceId.startsWith(leagueId)); + } + + async create(result: Result): Promise { + this.results.push(result); + return result; + } + + async createMany(results: Result[]): Promise { + this.results.push(...results); + return results; + } + + async update(result: Result): Promise { + const index = this.results.findIndex((r) => r.id === result.id); + if (index >= 0) { + this.results[index] = result; + } + return result; + } + + async delete(id: string): Promise { + this.results = this.results.filter((r) => r.id !== id); + } + + async deleteByRaceId(raceId: string): Promise { + this.results = this.results.filter((r) => r.raceId !== raceId); + } + + async exists(id: string): Promise { + return this.results.some((r) => r.id === id); + } + + async existsByRaceId(raceId: string): Promise { + return this.results.some((r) => r.raceId === raceId); + } + seedResult(result: Result): void { this.results.push(result); } @@ -146,6 +230,41 @@ class InMemoryPenaltyRepository implements IPenaltyRepository { return [...this.penalties]; } + async findById(id: string): Promise { + return this.penalties.find((p) => p.id === id) || null; + } + + async findByDriverId(driverId: string): Promise { + return this.penalties.filter((p) => p.driverId === driverId); + } + + async findByProtestId(protestId: string): Promise { + return this.penalties.filter((p) => p.protestId === protestId); + } + + async findPending(): Promise { + return this.penalties.filter((p) => p.status === 'pending'); + } + + async findIssuedBy(stewardId: string): Promise { + return this.penalties.filter((p) => p.issuedBy === stewardId); + } + + async create(penalty: Penalty): Promise { + this.penalties.push(penalty); + } + + async update(penalty: Penalty): Promise { + const index = this.penalties.findIndex((p) => p.id === penalty.id); + if (index >= 0) { + this.penalties[index] = penalty; + } + } + + async exists(id: string): Promise { + return this.penalties.some((p) => p.id === id); + } + seedPenalty(penalty: Penalty): void { this.penalties.push(penalty); } @@ -267,7 +386,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => { championshipAggregator, ); - const season: Season = { + const season = Season.create({ id: seasonId, leagueId, gameId: 'iracing', @@ -277,7 +396,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => { order: 1, startDate: new Date('2025-01-01'), endDate: new Date('2025-12-31'), - }; + }); seasonRepository.seedSeason(season); @@ -292,7 +411,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => { leagueScoringConfigRepository.seedConfig(leagueScoringConfig); const races: Race[] = [ - { + Race.create({ id: 'race-1-sprint', leagueId, scheduledAt: new Date('2025-02-01'), @@ -300,8 +419,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => { car: 'Car A', sessionType: 'race', status: 'completed', - }, - { + }), + Race.create({ id: 'race-1-main', leagueId, scheduledAt: new Date('2025-02-01'), @@ -309,8 +428,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => { car: 'Car A', sessionType: 'race', status: 'completed', - }, - { + }), + Race.create({ id: 'race-2-sprint', leagueId, scheduledAt: new Date('2025-03-01'), @@ -318,8 +437,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => { car: 'Car A', sessionType: 'race', status: 'completed', - }, - { + }), + Race.create({ id: 'race-2-main', leagueId, scheduledAt: new Date('2025-03-01'), @@ -327,8 +446,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => { car: 'Car A', sessionType: 'race', status: 'completed', - }, - { + }), + Race.create({ id: 'race-3-sprint', leagueId, scheduledAt: new Date('2025-04-01'), @@ -336,8 +455,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => { car: 'Car A', sessionType: 'race', status: 'completed', - }, - { + }), + Race.create({ id: 'race-3-main', leagueId, scheduledAt: new Date('2025-04-01'), @@ -345,7 +464,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => { car: 'Car A', sessionType: 'race', status: 'completed', - }, + }), ]; races.forEach((race) => raceRepository.seedRace(race)); @@ -392,7 +511,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => { let resultIdCounter = 1; for (const raceData of resultsData) { raceData.finishingOrder.forEach((driverId, index) => { - const result: Result = { + const result = Result.create({ id: `result-${resultIdCounter++}`, raceId: raceData.raceId, driverId, @@ -400,7 +519,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => { fastestLap: driverId === raceData.fastestLapDriverId ? 90000 : 91000 + index * 100, incidents: 0, startPosition: index + 1, - }; + }); resultRepository.seedResult(result); }); } @@ -423,7 +542,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => { sorted.map((r) => r.participant.id), ); - const leader = rows[0]; + const leader = rows[0]!; expect(leader.resultsCounted).toBeLessThanOrEqual(6); expect(leader.resultsDropped).toBeGreaterThanOrEqual(0); }); diff --git a/tests/unit/application/use-cases/StartAutomationSession.test.ts b/tests/unit/application/use-cases/StartAutomationSession.test.ts index 4b510ae62..5e6431da0 100644 --- a/tests/unit/application/use-cases/StartAutomationSession.test.ts +++ b/tests/unit/application/use-cases/StartAutomationSession.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { StartAutomationSessionUseCase } from '../../../../packages/automation/application/use-cases/StartAutomationSessionUseCase'; -import { IAutomationEngine } from '../../../../packages/automation/application/ports/IAutomationEngine'; -import { IScreenAutomation } from '../../../../packages/automation/application/ports/IScreenAutomation'; -import { ISessionRepository } from '../../../../packages/automation/application/ports/ISessionRepository'; +import { AutomationEnginePort as IAutomationEngine } from '../../../../packages/automation/application/ports/AutomationEnginePort'; +import { IBrowserAutomation as IScreenAutomation } from '../../../../packages/automation/application/ports/ScreenAutomationPort'; +import { SessionRepositoryPort as ISessionRepository } from '../../../../packages/automation/application/ports/SessionRepositoryPort'; import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession'; describe('StartAutomationSessionUseCase', () => { diff --git a/tests/unit/application/use-cases/VerifyAuthenticatedPageUseCase.test.ts b/tests/unit/application/use-cases/VerifyAuthenticatedPageUseCase.test.ts index 04bddcc5a..47621c710 100644 --- a/tests/unit/application/use-cases/VerifyAuthenticatedPageUseCase.test.ts +++ b/tests/unit/application/use-cases/VerifyAuthenticatedPageUseCase.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { VerifyAuthenticatedPageUseCase } from '../../../../packages/automation/application/use-cases/VerifyAuthenticatedPageUseCase'; -import { IAuthenticationService } from '../../../../packages/automation/application/ports/IAuthenticationService'; +import { AuthenticationServicePort as IAuthenticationService } from '../../../../packages/automation/application/ports/AuthenticationServicePort'; import { Result } from '../../../../packages/shared/result/Result'; import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState'; import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState'; diff --git a/tests/unit/domain/services/EventScoringService.test.ts b/tests/unit/domain/services/EventScoringService.test.ts index 86e4a2990..3cf23f0f1 100644 --- a/tests/unit/domain/services/EventScoringService.test.ts +++ b/tests/unit/domain/services/EventScoringService.test.ts @@ -6,9 +6,9 @@ import type { SessionType } from '@gridpilot/racing/domain/types/SessionType'; import { PointsTable } from '@gridpilot/racing/domain/value-objects/PointsTable'; import type { BonusRule } from '@gridpilot/racing/domain/types/BonusRule'; import type { ChampionshipConfig } from '@gridpilot/racing/domain/types/ChampionshipConfig'; -import type { Result } from '@gridpilot/racing/domain/entities/Result'; +import { Result } from '@gridpilot/racing/domain/entities/Result'; import type { Penalty } from '@gridpilot/racing/domain/entities/Penalty'; -import type { ChampionshipType } from '@gridpilot/racing/domain/value-objects/ChampionshipType'; +import type { ChampionshipType } from '@gridpilot/racing/domain/types/ChampionshipType'; function makeDriverRef(id: string): ParticipantRef { return { @@ -83,7 +83,7 @@ describe('EventScoringService', () => { }); const results: Result[] = [ - { + Result.create({ id: 'result-1', raceId: 'race-1', driverId: 'driver-1', @@ -91,8 +91,8 @@ describe('EventScoringService', () => { fastestLap: 90000, incidents: 0, startPosition: 1, - }, - { + }), + Result.create({ id: 'result-2', raceId: 'race-1', driverId: 'driver-2', @@ -100,8 +100,8 @@ describe('EventScoringService', () => { fastestLap: 90500, incidents: 0, startPosition: 2, - }, - { + }), + Result.create({ id: 'result-3', raceId: 'race-1', driverId: 'driver-3', @@ -109,8 +109,8 @@ describe('EventScoringService', () => { fastestLap: 91000, incidents: 0, startPosition: 3, - }, - { + }), + Result.create({ id: 'result-4', raceId: 'race-1', driverId: 'driver-4', @@ -118,8 +118,8 @@ describe('EventScoringService', () => { fastestLap: 91500, incidents: 0, startPosition: 4, - }, - { + }), + Result.create({ id: 'result-5', raceId: 'race-1', driverId: 'driver-5', @@ -127,7 +127,7 @@ describe('EventScoringService', () => { fastestLap: 92000, incidents: 0, startPosition: 5, - }, + }), ]; const penalties: Penalty[] = []; @@ -179,30 +179,30 @@ describe('EventScoringService', () => { } as const; const resultsP11Fastest: Result[] = [ - { + Result.create({ id: 'result-1', ...baseResultTemplate, driverId: 'driver-1', position: 1, startPosition: 1, fastestLap: 91000, - }, - { + }), + Result.create({ id: 'result-2', ...baseResultTemplate, driverId: 'driver-2', position: 2, startPosition: 2, fastestLap: 90500, - }, - { + }), + Result.create({ id: 'result-3', ...baseResultTemplate, driverId: 'driver-3', position: 11, startPosition: 15, fastestLap: 90000, - }, + }), ]; const penalties: Penalty[] = []; @@ -220,30 +220,30 @@ describe('EventScoringService', () => { expect(mapNoBonus.get('driver-3')?.bonusPoints).toBe(0); const resultsP8Fastest: Result[] = [ - { + Result.create({ id: 'result-1', ...baseResultTemplate, driverId: 'driver-1', position: 1, startPosition: 1, fastestLap: 91000, - }, - { + }), + Result.create({ id: 'result-2', ...baseResultTemplate, driverId: 'driver-2', position: 2, startPosition: 2, fastestLap: 90500, - }, - { + }), + Result.create({ id: 'result-3', ...baseResultTemplate, driverId: 'driver-3', position: 8, startPosition: 15, fastestLap: 90000, - }, + }), ]; const pointsWithBonus = service.scoreSession({ diff --git a/tests/unit/domain/services/ScheduleCalculator.test.ts b/tests/unit/domain/services/ScheduleCalculator.test.ts index 516950012..041b37686 100644 --- a/tests/unit/domain/services/ScheduleCalculator.test.ts +++ b/tests/unit/domain/services/ScheduleCalculator.test.ts @@ -75,9 +75,9 @@ describe('ScheduleCalculator', () => { expect(date.getDay()).toBe(6); // Saturday }); // First race should be Jan 6 - expect(result.raceDates[0].toISOString().split('T')[0]).toBe('2024-01-06'); + expect(result.raceDates[0]!.toISOString().split('T')[0]).toBe('2024-01-06'); // Last race should be 7 weeks later (Feb 24) - expect(result.raceDates[7].toISOString().split('T')[0]).toBe('2024-02-24'); + expect(result.raceDates[7]!.toISOString().split('T')[0]).toBe('2024-02-24'); }); it('should schedule races on multiple weekdays', () => { @@ -138,13 +138,13 @@ describe('ScheduleCalculator', () => { // Then expect(result.raceDates.length).toBe(4); // First race Jan 6 - expect(result.raceDates[0].toISOString().split('T')[0]).toBe('2024-01-06'); + expect(result.raceDates[0]!.toISOString().split('T')[0]).toBe('2024-01-06'); // Second race 2 weeks later (Jan 20) - expect(result.raceDates[1].toISOString().split('T')[0]).toBe('2024-01-20'); + expect(result.raceDates[1]!.toISOString().split('T')[0]).toBe('2024-01-20'); // Third race 2 weeks later (Feb 3) - expect(result.raceDates[2].toISOString().split('T')[0]).toBe('2024-02-03'); + expect(result.raceDates[2]!.toISOString().split('T')[0]).toBe('2024-02-03'); // Fourth race 2 weeks later (Feb 17) - expect(result.raceDates[3].toISOString().split('T')[0]).toBe('2024-02-17'); + expect(result.raceDates[3]!.toISOString().split('T')[0]).toBe('2024-02-17'); }); }); @@ -165,7 +165,7 @@ describe('ScheduleCalculator', () => { // Then expect(result.raceDates.length).toBe(8); // First race should be at or near start - expect(result.raceDates[0].toISOString().split('T')[0]).toBe('2024-01-06'); + expect(result.raceDates[0]!.toISOString().split('T')[0]).toBe('2024-01-06'); // Races should be spread across the range, not consecutive weeks }); diff --git a/tests/unit/domain/value-objects/CheckoutConfirmation.test.ts b/tests/unit/domain/value-objects/CheckoutConfirmation.test.ts index 19ec1bcc5..6523d76c6 100644 --- a/tests/unit/domain/value-objects/CheckoutConfirmation.test.ts +++ b/tests/unit/domain/value-objects/CheckoutConfirmation.test.ts @@ -19,7 +19,7 @@ describe('CheckoutConfirmation Value Object', () => { }); it('should throw error for invalid decision', () => { - expect(() => CheckoutConfirmation.create('invalid')).toThrow( + expect(() => CheckoutConfirmation.create('invalid' as any)).toThrow( 'Invalid checkout confirmation decision', ); }); diff --git a/tests/unit/domain/value-objects/SessionState.test.ts b/tests/unit/domain/value-objects/SessionState.test.ts index ab2e11eb6..58dc2ec60 100644 --- a/tests/unit/domain/value-objects/SessionState.test.ts +++ b/tests/unit/domain/value-objects/SessionState.test.ts @@ -44,11 +44,11 @@ describe('SessionState Value Object', () => { }); it('should throw error for invalid state', () => { - expect(() => SessionState.create('INVALID')).toThrow('Invalid session state'); + expect(() => SessionState.create('INVALID' as any)).toThrow('Invalid session state'); }); - + it('should throw error for empty string', () => { - expect(() => SessionState.create('')).toThrow('Invalid session state'); + expect(() => SessionState.create('' as any)).toThrow('Invalid session state'); }); }); diff --git a/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.initiateLogin.browserMode.test.ts b/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.initiateLogin.browserMode.test.ts index 5e70ae1dc..fff2de165 100644 --- a/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.initiateLogin.browserMode.test.ts +++ b/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.initiateLogin.browserMode.test.ts @@ -4,7 +4,7 @@ import { PlaywrightAuthSessionService } from '../../../../packages/automation/in import type { PlaywrightBrowserSession } from '../../../../packages/automation/infrastructure/adapters/automation/core/PlaywrightBrowserSession'; import type { SessionCookieStore } from '../../../../packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore'; import type { IPlaywrightAuthFlow } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthFlow'; -import type { ILogger } from '../../../../packages/automation/application/ports/ILogger'; +import type { LoggerPort as ILogger } from '../../../../packages/automation/application/ports/LoggerPort'; import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState'; import { Result } from '../../../../packages/shared/result/Result'; @@ -25,6 +25,9 @@ describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', () info: vi.fn(), warn: vi.fn(), error: vi.fn(), + fatal: vi.fn(), + child: vi.fn(), + flush: vi.fn().mockResolvedValue(undefined), }; mockPage = { @@ -107,7 +110,7 @@ describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', () }), ); - const calledUrl = (mockPage.goto as unknown as ReturnType).mock.calls[0][0] as string; + const calledUrl = (mockPage.goto as unknown as ReturnType).mock.calls[0]![0] as string; expect(calledUrl).not.toEqual('about:blank'); }); diff --git a/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.verifyPageAuthentication.test.ts b/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.verifyPageAuthentication.test.ts index 7b78b35c4..6f4e437cc 100644 --- a/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.verifyPageAuthentication.test.ts +++ b/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.verifyPageAuthentication.test.ts @@ -3,7 +3,7 @@ import type { Page, Locator } from 'playwright'; import { PlaywrightAuthSessionService } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService'; import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState'; import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState'; -import type { ILogger } from '../../../../packages/automation/application/ports/ILogger'; +import type { LoggerPort as ILogger } from '../../../../packages/automation/application/ports/LoggerPort'; import type { Result } from '../../../../packages/shared/result/Result'; import type { PlaywrightBrowserSession } from '../../../../packages/automation/infrastructure/adapters/automation/core/PlaywrightBrowserSession'; import type { SessionCookieStore } from '../../../../packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore'; @@ -21,6 +21,9 @@ describe('PlaywrightAuthSessionService.verifyPageAuthentication', () => { info: vi.fn(), warn: vi.fn(), error: vi.fn(), + fatal: vi.fn(), + child: vi.fn(), + flush: vi.fn().mockResolvedValue(undefined), }; const mockLocator: Locator = { diff --git a/tests/unit/infrastructure/adapters/SessionCookieStore.test.ts b/tests/unit/infrastructure/adapters/SessionCookieStore.test.ts index 9a866682f..14460ca6e 100644 --- a/tests/unit/infrastructure/adapters/SessionCookieStore.test.ts +++ b/tests/unit/infrastructure/adapters/SessionCookieStore.test.ts @@ -197,7 +197,7 @@ describe('SessionCookieStore - Cookie Validation', () => { const validCookies = cookieStore.getValidCookiesForUrl(targetUrl); expect(validCookies).toHaveLength(1); - expect(validCookies[0].name).toBe('valid_cookie'); + expect(validCookies[0]!.name).toBe('valid_cookie'); }); test('should filter out cookies with mismatched domains', async () => { @@ -228,7 +228,7 @@ describe('SessionCookieStore - Cookie Validation', () => { const validCookies = cookieStore.getValidCookiesForUrl(targetUrl); expect(validCookies).toHaveLength(1); - expect(validCookies[0].name).toBe('cookie1'); + expect(validCookies[0]!.name).toBe('cookie1'); }); test('should filter out cookies with invalid paths', async () => { @@ -259,7 +259,7 @@ describe('SessionCookieStore - Cookie Validation', () => { const validCookies = cookieStore.getValidCookiesForUrl(targetUrl); expect(validCookies).toHaveLength(1); - expect(validCookies[0].name).toBe('valid_path_cookie'); + expect(validCookies[0]!.name).toBe('valid_path_cookie'); }); test('should return empty array when no cookies are valid', async () => { diff --git a/tests/unit/racing-application/DashboardOverviewUseCase.test.ts b/tests/unit/racing-application/DashboardOverviewUseCase.test.ts index c93819328..d62df617c 100644 --- a/tests/unit/racing-application/DashboardOverviewUseCase.test.ts +++ b/tests/unit/racing-application/DashboardOverviewUseCase.test.ts @@ -1,6 +1,13 @@ import { describe, it, expect } from 'vitest'; import { GetDashboardOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetDashboardOverviewUseCase'; +import { Driver } from '@gridpilot/racing/domain/entities/Driver'; +import { Race } from '@gridpilot/racing/domain/entities/Race'; +import { Result } from '@gridpilot/racing/domain/entities/Result'; +import { League } from '@gridpilot/racing/domain/entities/League'; +import { Standing } from '@gridpilot/racing/domain/entities/Standing'; +import { LeagueMembership } from '@gridpilot/racing/domain/entities/LeagueMembership'; +import type { FeedItem } from '@gridpilot/social/domain/types/FeedItem'; import type { IDashboardOverviewPresenter, DashboardOverviewViewModel, @@ -25,11 +32,17 @@ class FakeDashboardOverviewPresenter implements IDashboardOverviewPresenter { interface TestImageService { getDriverAvatar(driverId: string): string; + getTeamLogo(teamId: string): string; + getLeagueCover(leagueId: string): string; + getLeagueLogo(leagueId: string): string; } function createTestImageService(): TestImageService { return { getDriverAvatar: (driverId: string) => `avatar-${driverId}`, + getTeamLogo: (teamId: string) => `team-logo-${teamId}`, + getLeagueCover: (leagueId: string) => `league-cover-${leagueId}`, + getLeagueLogo: (leagueId: string) => `league-logo-${leagueId}`, }; } @@ -38,143 +51,173 @@ describe('GetDashboardOverviewUseCase', () => { // Given a driver with memberships in two leagues and future races with mixed registration const driverId = 'driver-1'; - const driver = { id: driverId, name: 'Alice Racer', country: 'US' }; + const driver = Driver.create({ id: driverId, iracingId: '12345', name: 'Alice Racer', country: 'US' }); const leagues = [ - { id: 'league-1', name: 'Alpha League' }, - { id: 'league-2', name: 'Beta League' }, + League.create({ id: 'league-1', name: 'Alpha League', description: 'First league', ownerId: 'owner-1' }), + League.create({ id: 'league-2', name: 'Beta League', description: 'Second league', ownerId: 'owner-2' }), ]; const now = Date.now(); const races = [ - { + Race.create({ id: 'race-1', leagueId: 'league-1', track: 'Monza', car: 'GT3', scheduledAt: new Date(now + 60 * 60 * 1000), - status: 'scheduled' as const, - }, - { + status: 'scheduled', + }), + Race.create({ id: 'race-2', leagueId: 'league-1', track: 'Spa', car: 'GT3', scheduledAt: new Date(now + 2 * 60 * 60 * 1000), - status: 'scheduled' as const, - }, - { + status: 'scheduled', + }), + Race.create({ id: 'race-3', leagueId: 'league-2', track: 'Silverstone', car: 'GT4', scheduledAt: new Date(now + 3 * 60 * 60 * 1000), - status: 'scheduled' as const, - }, - { + status: 'scheduled', + }), + Race.create({ id: 'race-4', leagueId: 'league-2', track: 'Imola', car: 'GT4', scheduledAt: new Date(now + 4 * 60 * 60 * 1000), - status: 'scheduled' as const, - }, + status: 'scheduled', + }), ]; - const results: unknown[] = []; + const results: Result[] = []; const memberships = [ - { + LeagueMembership.create({ leagueId: 'league-1', driverId, + role: 'member', status: 'active', - }, - { + }), + LeagueMembership.create({ leagueId: 'league-2', driverId, + role: 'member', status: 'active', - }, + }), ]; const registeredRaceIds = new Set(['race-1', 'race-3']); - const feedItems: DashboardFeedItemSummaryViewModel[] = []; - const friends: Array<{ id: string }> = []; + const feedItems: FeedItem[] = []; + const friends: Driver[] = []; - const driverRepository: { - findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>; - } = { - findById: async (id: string) => (id === driver.id ? driver : null), + const driverRepository = { + findById: async (id: string): Promise => (id === driver.id ? driver : null), + findByIRacingId: async (): Promise => null, + findAll: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByIRacingId: async (): Promise => false, }; - const raceRepository: { - findAll: () => Promise< - Array<{ - id: string; - leagueId: string; - track: string; - car: string; - scheduledAt: Date; - status: 'scheduled'; - }> - >; - } = { - findAll: async () => races, + const raceRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => races, + findByLeagueId: async (): Promise => [], + findUpcomingByLeagueId: async (): Promise => [], + findCompletedByLeagueId: async (): Promise => [], + findByStatus: async (): Promise => [], + findByDateRange: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; - const resultRepository: { - findAll: () => Promise; - } = { - findAll: async () => results, + const resultRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => results, + findByRaceId: async (): Promise => [], + findByDriverId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + createMany: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByRaceId: async (): Promise => false, }; - const leagueRepository: { - findAll: () => Promise>; - } = { - findAll: async () => leagues, + const leagueRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => leagues, + findByOwnerId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + searchByName: async (): Promise => [], }; - const standingRepository: { - findByLeagueId: (leagueId: string) => Promise; - } = { - findByLeagueId: async () => [], + const standingRepository = { + findByLeagueId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => null, + findAll: async (): Promise => [], + save: async (): Promise => { throw new Error('Not implemented'); }, + saveMany: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + recalculate: async (): Promise => [], }; - const leagueMembershipRepository: { - getMembership: ( - leagueId: string, - driverIdParam: string, - ) => Promise<{ leagueId: string; driverId: string; status: string } | null>; - } = { - getMembership: async (leagueId: string, driverIdParam: string) => { + const leagueMembershipRepository = { + getMembership: async (leagueId: string, driverIdParam: string): Promise => { return ( memberships.find( (m) => m.leagueId === leagueId && m.driverId === driverIdParam, ) ?? null ); }, + getLeagueMembers: async (): Promise => [], + getJoinRequests: async (): Promise => [], + saveMembership: async (): Promise => { throw new Error('Not implemented'); }, + removeMembership: async (): Promise => { throw new Error('Not implemented'); }, + saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, + removeJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, }; - const raceRegistrationRepository: { - isRegistered: (raceId: string, driverIdParam: string) => Promise; - } = { - isRegistered: async (raceId: string, driverIdParam: string) => { + const raceRegistrationRepository = { + isRegistered: async (raceId: string, driverIdParam: string): Promise => { if (driverIdParam !== driverId) return false; return registeredRaceIds.has(raceId); }, + getRegisteredDrivers: async (): Promise => [], + getRegistrationCount: async (): Promise => 0, + register: async (): Promise => { throw new Error('Not implemented'); }, + withdraw: async (): Promise => { throw new Error('Not implemented'); }, + getDriverRegistrations: async (): Promise => [], + clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, }; - const feedRepository: { - getFeedForDriver: (driverIdParam: string) => Promise; - } = { - getFeedForDriver: async () => feedItems, + const feedRepository = { + getFeedForDriver: async (): Promise => feedItems, + getGlobalFeed: async (): Promise => [], }; - const socialRepository: { - getFriends: (driverIdParam: string) => Promise>; - } = { - getFriends: async () => friends, + const socialRepository = { + getFriends: async (): Promise => friends, + getFriendIds: async (): Promise => [], + getSuggestedFriends: async (): Promise => [], }; const imageService = createTestImageService(); @@ -230,138 +273,181 @@ describe('GetDashboardOverviewUseCase', () => { // Given completed races with results and standings const driverId = 'driver-2'; - const driver = { id: driverId, name: 'Result Driver', country: 'DE' }; + const driver = Driver.create({ id: driverId, iracingId: '67890', name: 'Result Driver', country: 'DE' }); const leagues = [ - { id: 'league-A', name: 'Results League A' }, - { id: 'league-B', name: 'Results League B' }, + League.create({ id: 'league-A', name: 'Results League A', description: 'League A', ownerId: 'owner-A' }), + League.create({ id: 'league-B', name: 'Results League B', description: 'League B', ownerId: 'owner-B' }), ]; - const raceOld = { + const raceOld = Race.create({ id: 'race-old', leagueId: 'league-A', track: 'Old Circuit', car: 'GT3', scheduledAt: new Date('2024-01-01T10:00:00Z'), - status: 'completed' as const, - }; + status: 'completed', + }); - const raceNew = { + const raceNew = Race.create({ id: 'race-new', leagueId: 'league-B', track: 'New Circuit', car: 'GT4', scheduledAt: new Date('2024-02-01T10:00:00Z'), - status: 'completed' as const, - }; + status: 'completed', + }); const races = [raceOld, raceNew]; const results = [ - { + Result.create({ id: 'result-old', raceId: raceOld.id, driverId, position: 5, + fastestLap: 120, incidents: 3, - }, - { + startPosition: 5, + }), + Result.create({ id: 'result-new', raceId: raceNew.id, driverId, position: 2, + fastestLap: 115, incidents: 1, - }, + startPosition: 2, + }), ]; const memberships = [ - { + LeagueMembership.create({ leagueId: 'league-A', driverId, + role: 'member', status: 'active', - }, - { + }), + LeagueMembership.create({ leagueId: 'league-B', driverId, + role: 'member', status: 'active', - }, + }), ]; const standingsByLeague = new Map< string, - Array<{ leagueId: string; driverId: string; position: number; points: number }> + Standing[] >(); standingsByLeague.set('league-A', [ - { leagueId: 'league-A', driverId, position: 3, points: 50 }, - { leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 }, + Standing.create({ leagueId: 'league-A', driverId, position: 3, points: 50 }), + Standing.create({ leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 }), ]); standingsByLeague.set('league-B', [ - { leagueId: 'league-B', driverId, position: 1, points: 100 }, - { leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 }, + Standing.create({ leagueId: 'league-B', driverId, position: 1, points: 100 }), + Standing.create({ leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 }), ]); - const driverRepository: { - findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>; - } = { - findById: async (id: string) => (id === driver.id ? driver : null), + const driverRepository = { + findById: async (id: string): Promise => (id === driver.id ? driver : null), + findByIRacingId: async (): Promise => null, + findAll: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByIRacingId: async (): Promise => false, }; - const raceRepository: { - findAll: () => Promise; - } = { - findAll: async () => races, + const raceRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => races, + findByLeagueId: async (): Promise => [], + findUpcomingByLeagueId: async (): Promise => [], + findCompletedByLeagueId: async (): Promise => [], + findByStatus: async (): Promise => [], + findByDateRange: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; - const resultRepository: { - findAll: () => Promise; - } = { - findAll: async () => results, + const resultRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => results, + findByRaceId: async (): Promise => [], + findByDriverId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + createMany: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByRaceId: async (): Promise => false, }; - const leagueRepository: { - findAll: () => Promise; - } = { - findAll: async () => leagues, + const leagueRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => leagues, + findByOwnerId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + searchByName: async (): Promise => [], }; - const standingRepository: { - findByLeagueId: (leagueId: string) => Promise>; - } = { - findByLeagueId: async (leagueId: string) => + const standingRepository = { + findByLeagueId: async (leagueId: string): Promise => standingsByLeague.get(leagueId) ?? [], + findByDriverIdAndLeagueId: async (): Promise => null, + findAll: async (): Promise => [], + save: async (): Promise => { throw new Error('Not implemented'); }, + saveMany: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + recalculate: async (): Promise => [], }; - const leagueMembershipRepository: { - getMembership: ( - leagueId: string, - driverIdParam: string, - ) => Promise<{ leagueId: string; driverId: string; status: string } | null>; - } = { - getMembership: async (leagueId: string, driverIdParam: string) => { + const leagueMembershipRepository = { + getMembership: async (leagueId: string, driverIdParam: string): Promise => { return ( memberships.find( (m) => m.leagueId === leagueId && m.driverId === driverIdParam, ) ?? null ); }, + getLeagueMembers: async (): Promise => [], + getJoinRequests: async (): Promise => [], + saveMembership: async (): Promise => { throw new Error('Not implemented'); }, + removeMembership: async (): Promise => { throw new Error('Not implemented'); }, + saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, + removeJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, }; - const raceRegistrationRepository: { - isRegistered: (raceId: string, driverIdParam: string) => Promise; - } = { - isRegistered: async () => false, + const raceRegistrationRepository = { + isRegistered: async (): Promise => false, + getRegisteredDrivers: async (): Promise => [], + getRegistrationCount: async (): Promise => 0, + register: async (): Promise => { throw new Error('Not implemented'); }, + withdraw: async (): Promise => { throw new Error('Not implemented'); }, + getDriverRegistrations: async (): Promise => [], + clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, }; - const feedRepository: { - getFeedForDriver: (driverIdParam: string) => Promise; - } = { - getFeedForDriver: async () => [], + const feedRepository = { + getFeedForDriver: async (): Promise => [], + getGlobalFeed: async (): Promise => [], }; - const socialRepository: { - getFriends: (driverIdParam: string) => Promise>; - } = { - getFriends: async () => [], + const socialRepository = { + getFriends: async (): Promise => [], + getFriendIds: async (): Promise => [], + getSuggestedFriends: async (): Promise => [], }; const imageService = createTestImageService(); @@ -430,54 +516,100 @@ describe('GetDashboardOverviewUseCase', () => { // Given a driver with no related data const driverId = 'driver-empty'; - const driver = { id: driverId, name: 'New Racer', country: 'FR' }; + const driver = Driver.create({ id: driverId, iracingId: '11111', name: 'New Racer', country: 'FR' }); - const driverRepository: { - findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>; - } = { - findById: async (id: string) => (id === driver.id ? driver : null), + const driverRepository = { + findById: async (id: string): Promise => (id === driver.id ? driver : null), + findByIRacingId: async (): Promise => null, + findAll: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByIRacingId: async (): Promise => false, }; - const raceRepository: { findAll: () => Promise } = { - findAll: async () => [], + const raceRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByLeagueId: async (): Promise => [], + findUpcomingByLeagueId: async (): Promise => [], + findCompletedByLeagueId: async (): Promise => [], + findByStatus: async (): Promise => [], + findByDateRange: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; - const resultRepository: { findAll: () => Promise } = { - findAll: async () => [], + const resultRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByRaceId: async (): Promise => [], + findByDriverId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + createMany: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByRaceId: async (): Promise => false, }; - const leagueRepository: { findAll: () => Promise } = { - findAll: async () => [], + const leagueRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByOwnerId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + searchByName: async (): Promise => [], }; - const standingRepository: { - findByLeagueId: (leagueId: string) => Promise; - } = { - findByLeagueId: async () => [], + const standingRepository = { + findByLeagueId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => null, + findAll: async (): Promise => [], + save: async (): Promise => { throw new Error('Not implemented'); }, + saveMany: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + recalculate: async (): Promise => [], }; - const leagueMembershipRepository: { - getMembership: (leagueId: string, driverIdParam: string) => Promise; - } = { - getMembership: async () => null, + const leagueMembershipRepository = { + getMembership: async (): Promise => null, + getLeagueMembers: async (): Promise => [], + getJoinRequests: async (): Promise => [], + saveMembership: async (): Promise => { throw new Error('Not implemented'); }, + removeMembership: async (): Promise => { throw new Error('Not implemented'); }, + saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, + removeJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, }; - const raceRegistrationRepository: { - isRegistered: (raceId: string, driverIdParam: string) => Promise; - } = { - isRegistered: async () => false, + const raceRegistrationRepository = { + isRegistered: async (): Promise => false, + getRegisteredDrivers: async (): Promise => [], + getRegistrationCount: async (): Promise => 0, + register: async (): Promise => { throw new Error('Not implemented'); }, + withdraw: async (): Promise => { throw new Error('Not implemented'); }, + getDriverRegistrations: async (): Promise => [], + clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, }; - const feedRepository: { - getFeedForDriver: (driverIdParam: string) => Promise; - } = { - getFeedForDriver: async () => [], + const feedRepository = { + getFeedForDriver: async (): Promise => [], + getGlobalFeed: async (): Promise => [], }; - const socialRepository: { - getFriends: (driverIdParam: string) => Promise>; - } = { - getFriends: async () => [], + const socialRepository = { + getFriends: async (): Promise => [], + getFriendIds: async (): Promise => [], + getSuggestedFriends: async (): Promise => [], }; const imageService = createTestImageService(); diff --git a/tests/unit/racing-application/MembershipUseCases.test.ts b/tests/unit/racing-application/MembershipUseCases.test.ts index e94653e59..5d3f3832a 100644 --- a/tests/unit/racing-application/MembershipUseCases.test.ts +++ b/tests/unit/racing-application/MembershipUseCases.test.ts @@ -2,10 +2,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { JoinLeagueUseCase } from '@gridpilot/racing/application/use-cases/JoinLeagueUseCase'; import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository'; -import type { +import { LeagueMembership, - MembershipRole, - MembershipStatus, + type MembershipRole, + type MembershipStatus, } from '@gridpilot/racing/domain/entities/LeagueMembership'; class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository { @@ -109,13 +109,13 @@ describe('Membership use-cases', () => { const leagueId = 'league-1'; const driverId = 'driver-1'; - repository.seedMembership({ + repository.seedMembership(LeagueMembership.create({ leagueId, driverId, role: 'member', status: 'active', joinedAt: new Date('2024-01-01'), - }); + })); await expect( useCase.execute({ leagueId, driverId }), diff --git a/tests/unit/racing-application/RaceResultsUseCases.test.ts b/tests/unit/racing-application/RaceResultsUseCases.test.ts index 0e8a8d355..f1d5fda4d 100644 --- a/tests/unit/racing-application/RaceResultsUseCases.test.ts +++ b/tests/unit/racing-application/RaceResultsUseCases.test.ts @@ -4,6 +4,8 @@ import { Race } from '@gridpilot/racing/domain/entities/Race'; import { League } from '@gridpilot/racing/domain/entities/League'; import { Result } from '@gridpilot/racing/domain/entities/Result'; import { Penalty } from '@gridpilot/racing/domain/entities/Penalty'; +import { Standing } from '@gridpilot/racing/domain/entities/Standing'; +import { Driver } from '@gridpilot/racing/domain/entities/Driver'; import { GetRaceResultsDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceResultsDetailUseCase'; import { ImportRaceResultsUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsUseCase'; @@ -77,37 +79,64 @@ describe('ImportRaceResultsUseCase', () => { let existsByRaceIdCalled = false; const recalcCalls: string[] = []; - const raceRepository: { - findById: (id: string) => Promise; - } = { - findById: async (id: string) => races.get(id) ?? null, + const raceRepository = { + findById: async (id: string): Promise => races.get(id) ?? null, + findAll: async (): Promise => [], + findByLeagueId: async (): Promise => [], + findUpcomingByLeagueId: async (): Promise => [], + findCompletedByLeagueId: async (): Promise => [], + findByStatus: async (): Promise => [], + findByDateRange: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; - const leagueRepository: { - findById: (id: string) => Promise; - } = { - findById: async (id: string) => leagues.get(id) ?? null, + const leagueRepository = { + findById: async (id: string): Promise => leagues.get(id) ?? null, + findAll: async (): Promise => [], + findByOwnerId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + searchByName: async (): Promise => [], }; - const resultRepository: { - existsByRaceId: (raceId: string) => Promise; - createMany: (results: Result[]) => Promise; - } = { - existsByRaceId: async (raceId: string) => { - existsByRaceIdCalled = true; - return storedResults.some((r) => r.raceId === raceId); - }, - createMany: async (results: Result[]) => { + const resultRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByRaceId: async (): Promise => [], + findByDriverId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + createMany: async (results: Result[]): Promise => { storedResults.push(...results); return results; }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByRaceId: async (raceId: string): Promise => { + existsByRaceIdCalled = true; + return storedResults.some((r) => r.raceId === raceId); + }, }; - const standingRepository: { - recalculate: (leagueId: string) => Promise; - } = { - recalculate: async (leagueId: string) => { + const standingRepository = { + findByLeagueId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => null, + findAll: async (): Promise => [], + save: async (): Promise => { throw new Error('Not implemented'); }, + saveMany: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + recalculate: async (leagueId: string): Promise => { recalcCalls.push(leagueId); + return []; }, }; @@ -196,34 +225,60 @@ describe('ImportRaceResultsUseCase', () => { }), ]; - const raceRepository: { - findById: (id: string) => Promise; - } = { - findById: async (id: string) => races.get(id) ?? null, + const raceRepository = { + findById: async (id: string): Promise => races.get(id) ?? null, + findAll: async (): Promise => [], + findByLeagueId: async (): Promise => [], + findUpcomingByLeagueId: async (): Promise => [], + findCompletedByLeagueId: async (): Promise => [], + findByStatus: async (): Promise => [], + findByDateRange: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; - const leagueRepository: { - findById: (id: string) => Promise; - } = { - findById: async (id: string) => leagues.get(id) ?? null, + const leagueRepository = { + findById: async (id: string): Promise => leagues.get(id) ?? null, + findAll: async (): Promise => [], + findByOwnerId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + searchByName: async (): Promise => [], }; - const resultRepository: { - existsByRaceId: (raceId: string) => Promise; - createMany: (results: Result[]) => Promise; - } = { - existsByRaceId: async (raceId: string) => { - return storedResults.some((r) => r.raceId === raceId); - }, - createMany: async (_results: Result[]) => { + const resultRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByRaceId: async (): Promise => [], + findByDriverId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + createMany: async (_results: Result[]): Promise => { throw new Error('Should not be called when results already exist'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByRaceId: async (raceId: string): Promise => { + return storedResults.some((r) => r.raceId === raceId); + }, }; - const standingRepository: { - recalculate: (leagueId: string) => Promise; - } = { - recalculate: async (_leagueId: string) => { + const standingRepository = { + findByLeagueId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => null, + findAll: async (): Promise => [], + save: async (): Promise => { throw new Error('Not implemented'); }, + saveMany: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByLeagueId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + recalculate: async (_leagueId: string): Promise => { throw new Error('Should not be called when results already exist'); }, }; @@ -315,35 +370,68 @@ describe('GetRaceResultsDetailUseCase', () => { const results = [result1, result2]; const drivers = [driver1, driver2]; - const raceRepository: { - findById: (id: string) => Promise; - } = { - findById: async (id: string) => races.get(id) ?? null, + const raceRepository = { + findById: async (id: string): Promise => races.get(id) ?? null, + findAll: async (): Promise => [], + findByLeagueId: async (): Promise => [], + findUpcomingByLeagueId: async (): Promise => [], + findCompletedByLeagueId: async (): Promise => [], + findByStatus: async (): Promise => [], + findByDateRange: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; - const leagueRepository: { - findById: (id: string) => Promise; - } = { - findById: async (id: string) => leagues.get(id) ?? null, + const leagueRepository = { + findById: async (id: string): Promise => leagues.get(id) ?? null, + findAll: async (): Promise => [], + findByOwnerId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + searchByName: async (): Promise => [], }; - const resultRepository: { - findByRaceId: (raceId: string) => Promise; - } = { - findByRaceId: async (raceId: string) => + const resultRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByRaceId: async (raceId: string): Promise => results.filter((r) => r.raceId === raceId), + findByDriverId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + createMany: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByRaceId: async (): Promise => false, }; - const driverRepository: { - findAll: () => Promise>; - } = { - findAll: async () => drivers, + const driverRepository = { + findById: async (): Promise => null, + findByIRacingId: async (): Promise => null, + findAll: async (): Promise => drivers.map(d => Driver.create({ id: d.id, iracingId: '123', name: d.name, country: d.country })), + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByIRacingId: async (): Promise => false, }; - const penaltyRepository: { - findByRaceId: (raceId: string) => Promise; - } = { - findByRaceId: async () => [] as Penalty[], + const penaltyRepository = { + findById: async (): Promise => null, + findByRaceId: async (): Promise => [] as Penalty[], + findByDriverId: async (): Promise => [], + findByProtestId: async (): Promise => [], + findPending: async (): Promise => [], + findIssuedBy: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; const presenter = new FakeRaceResultsDetailPresenter(); @@ -354,7 +442,6 @@ describe('GetRaceResultsDetailUseCase', () => { resultRepository, driverRepository, penaltyRepository, - presenter, ); // When executing the query @@ -364,8 +451,8 @@ describe('GetRaceResultsDetailUseCase', () => { expect(viewModel).not.toBeNull(); // Then points system matches the default F1-style configuration - expect(viewModel!.pointsSystem[1]).toBe(25); - expect(viewModel!.pointsSystem[2]).toBe(18); + expect(viewModel!.pointsSystem?.[1]).toBe(25); + expect(viewModel!.pointsSystem?.[2]).toBe(18); // And fastest lap is identified correctly expect(viewModel!.fastestLapTime).toBeCloseTo(88.456, 3); @@ -408,6 +495,7 @@ describe('GetRaceResultsDetailUseCase', () => { const penalty = Penalty.create({ id: 'pen-1', + leagueId: league.id, raceId: race.id, driverId: driver.id, type: 'points_deduction', @@ -424,36 +512,69 @@ describe('GetRaceResultsDetailUseCase', () => { const drivers = [driver]; const penalties = [penalty]; - const raceRepository: { - findById: (id: string) => Promise; - } = { - findById: async (id: string) => races.get(id) ?? null, + const raceRepository = { + findById: async (id: string): Promise => races.get(id) ?? null, + findAll: async (): Promise => [], + findByLeagueId: async (): Promise => [], + findUpcomingByLeagueId: async (): Promise => [], + findCompletedByLeagueId: async (): Promise => [], + findByStatus: async (): Promise => [], + findByDateRange: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; - const leagueRepository: { - findById: (id: string) => Promise; - } = { - findById: async (id: string) => leagues.get(id) ?? null, + const leagueRepository = { + findById: async (id: string): Promise => leagues.get(id) ?? null, + findAll: async (): Promise => [], + findByOwnerId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + searchByName: async (): Promise => [], }; - const resultRepository: { - findByRaceId: (raceId: string) => Promise; - } = { - findByRaceId: async (raceId: string) => + const resultRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByRaceId: async (raceId: string): Promise => results.filter((r) => r.raceId === raceId), + findByDriverId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + createMany: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByRaceId: async (): Promise => false, }; - const driverRepository: { - findAll: () => Promise>; - } = { - findAll: async () => drivers, + const driverRepository = { + findById: async (): Promise => null, + findByIRacingId: async (): Promise => null, + findAll: async (): Promise => drivers.map(d => Driver.create({ id: d.id, iracingId: '123', name: d.name, country: d.country })), + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByIRacingId: async (): Promise => false, }; - const penaltyRepository: { - findByRaceId: (raceId: string) => Promise; - } = { - findByRaceId: async (raceId: string) => + const penaltyRepository = { + findById: async (): Promise => null, + findByRaceId: async (raceId: string): Promise => penalties.filter((p) => p.raceId === raceId), + findByDriverId: async (): Promise => [], + findByProtestId: async (): Promise => [], + findPending: async (): Promise => [], + findIssuedBy: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; const presenter = new FakeRaceResultsDetailPresenter(); @@ -464,7 +585,6 @@ describe('GetRaceResultsDetailUseCase', () => { resultRepository, driverRepository, penaltyRepository, - presenter, ); // When @@ -491,34 +611,67 @@ describe('GetRaceResultsDetailUseCase', () => { it('presents an error when race does not exist', async () => { // Given repositories without the requested race - const raceRepository: { - findById: (id: string) => Promise; - } = { - findById: async () => null, + const raceRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByLeagueId: async (): Promise => [], + findUpcomingByLeagueId: async (): Promise => [], + findCompletedByLeagueId: async (): Promise => [], + findByStatus: async (): Promise => [], + findByDateRange: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; - const leagueRepository: { - findById: (id: string) => Promise; - } = { - findById: async () => null, + const leagueRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByOwnerId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + searchByName: async (): Promise => [], }; - const resultRepository: { - findByRaceId: (raceId: string) => Promise; - } = { - findByRaceId: async () => [] as Result[], + const resultRepository = { + findById: async (): Promise => null, + findAll: async (): Promise => [], + findByRaceId: async (): Promise => [] as Result[], + findByDriverId: async (): Promise => [], + findByDriverIdAndLeagueId: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + createMany: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + deleteByRaceId: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByRaceId: async (): Promise => false, }; - const driverRepository: { - findAll: () => Promise>; - } = { - findAll: async () => [], + const driverRepository = { + findById: async (): Promise => null, + findByIRacingId: async (): Promise => null, + findAll: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + delete: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, + existsByIRacingId: async (): Promise => false, }; - const penaltyRepository: { - findByRaceId: (raceId: string) => Promise; - } = { - findByRaceId: async () => [] as Penalty[], + const penaltyRepository = { + findById: async (): Promise => null, + findByRaceId: async (): Promise => [] as Penalty[], + findByDriverId: async (): Promise => [], + findByProtestId: async (): Promise => [], + findPending: async (): Promise => [], + findIssuedBy: async (): Promise => [], + create: async (): Promise => { throw new Error('Not implemented'); }, + update: async (): Promise => { throw new Error('Not implemented'); }, + exists: async (): Promise => false, }; const presenter = new FakeRaceResultsDetailPresenter(); @@ -529,7 +682,6 @@ describe('GetRaceResultsDetailUseCase', () => { resultRepository, driverRepository, penaltyRepository, - presenter, ); // When diff --git a/tests/unit/racing-application/RegistrationAndTeamUseCases.test.ts b/tests/unit/racing-application/RegistrationAndTeamUseCases.test.ts index dc35502fd..2377741e8 100644 --- a/tests/unit/racing-application/RegistrationAndTeamUseCases.test.ts +++ b/tests/unit/racing-application/RegistrationAndTeamUseCases.test.ts @@ -9,13 +9,14 @@ import { LeagueMembership, type MembershipStatus, } from '@gridpilot/racing/domain/entities/LeagueMembership'; +import { Team } from '@gridpilot/racing/domain/entities/Team'; +import { Driver } from '@gridpilot/racing/domain/entities/Driver'; import type { - Team, TeamMembership, TeamMembershipStatus, TeamRole, TeamJoinRequest, -} from '@gridpilot/racing/domain/entities/Team'; +} from '@gridpilot/racing/domain/types/TeamMembership'; import { RegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/RegisterForRaceUseCase'; import { WithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/WithdrawFromRaceUseCase'; @@ -169,10 +170,23 @@ class TestDriverRegistrationStatusPresenter implements IDriverRegistrationStatus raceId: string | null = null; driverId: string | null = null; - present(isRegistered: boolean, raceId: string, driverId: string): void { + present(isRegistered: boolean, raceId: string, driverId: string) { this.isRegistered = isRegistered; this.raceId = raceId; this.driverId = driverId; + return { + isRegistered, + raceId, + driverId, + }; + } + + getViewModel() { + return { + isRegistered: this.isRegistered!, + raceId: this.raceId!, + driverId: this.driverId!, + }; } } @@ -185,9 +199,20 @@ class TestRaceRegistrationsPresenter implements IRaceRegistrationsPresenter { this.driverIds = []; } - present(input: RaceRegistrationsResultDTO): void { + present(input: RaceRegistrationsResultDTO) { this.driverIds = input.registeredDriverIds; this.raceId = null; + return { + registeredDriverIds: input.registeredDriverIds, + count: input.registeredDriverIds.length, + }; + } + + getViewModel() { + return { + registeredDriverIds: this.driverIds, + count: this.driverIds.length, + }; } } @@ -411,8 +436,43 @@ describe('Racing application use-cases - teams', () => { let getDriverTeamUseCase: GetDriverTeamUseCase; class FakeDriverRepository { - async findById(driverId: string): Promise<{ id: string; name: string } | null> { - return { id: driverId, name: `Driver ${driverId}` }; + async findById(driverId: string): Promise { + return Driver.create({ id: driverId, iracingId: '123', name: `Driver ${driverId}`, country: 'US' }); + } + + async findByIRacingId(id: string): Promise { + return null; + } + + async findAll(): Promise { + return []; + } + + async create(driver: Driver): Promise { + return driver; + } + + async update(driver: Driver): Promise { + return driver; + } + + async delete(id: string): Promise { + } + + async exists(id: string): Promise { + return false; + } + + async existsByIRacingId(iracingId: string): Promise { + return false; + } + + async findByLeagueId(leagueId: string): Promise { + return []; + } + + async findByTeamId(teamId: string): Promise { + return []; } } @@ -420,6 +480,18 @@ describe('Racing application use-cases - teams', () => { getDriverAvatar(driverId: string): string { return `https://example.com/avatar/${driverId}.png`; } + + getTeamLogo(teamId: string): string { + return `https://example.com/logo/${teamId}.png`; + } + + getLeagueCover(leagueId: string): string { + return `https://example.com/cover/${leagueId}.png`; + } + + getLeagueLogo(leagueId: string): string { + return `https://example.com/logo/${leagueId}.png`; + } } class TestAllTeamsPresenter implements IAllTeamsPresenter { @@ -438,9 +510,9 @@ describe('Racing application use-cases - teams', () => { description: team.description, memberCount: team.memberCount, leagues: team.leagues, - specialization: team.specialization, - region: team.region, - languages: team.languages, + specialization: (team as any).specialization, + region: (team as any).region, + languages: (team as any).languages, })), totalCount: input.teams.length, }; @@ -487,7 +559,7 @@ describe('Racing application use-cases - teams', () => { return { driverId, driverName, - role: membership.role, + role: ((membership.role as any) === 'owner' ? 'owner' : (membership.role as any) === 'member' ? 'member' : (membership.role as any) === 'manager' ? 'manager' : (membership.role as any) === 'driver' ? 'member' : 'member') as "owner" | "member" | "manager", joinedAt: membership.joinedAt.toISOString(), isActive: membership.status === 'active', avatarUrl, @@ -496,7 +568,7 @@ describe('Racing application use-cases - teams', () => { const ownerCount = members.filter((m) => m.role === 'owner').length; const managerCount = members.filter((m) => m.role === 'manager').length; - const memberCount = members.filter((m) => m.role === 'member').length; + const memberCount = members.filter((m) => (m.role as any) === 'member').length; this.viewModel = { members, @@ -534,7 +606,7 @@ describe('Racing application use-cases - teams', () => { driverId, driverName, teamId: request.teamId, - status: 'pending', + status: 'pending' as const, requestedAt: request.requestedAt.toISOString(), avatarUrl, }; @@ -559,7 +631,7 @@ describe('Racing application use-cases - teams', () => { } class TestDriverTeamPresenter implements IDriverTeamPresenter { - private viewModel: DriverTeamViewModel | null = null; + viewModel: DriverTeamViewModel | null = null; reset(): void { this.viewModel = null; @@ -579,12 +651,9 @@ describe('Racing application use-cases - teams', () => { description: team.description, ownerId: team.ownerId, leagues: team.leagues, - specialization: team.specialization, - region: team.region, - languages: team.languages, }, membership: { - role: membership.role, + role: (membership.role === 'owner' || membership.role === 'manager') ? membership.role : 'member' as "owner" | "member" | "manager", joinedAt: membership.joinedAt.toISOString(), isActive: membership.status === 'active', }, @@ -619,19 +688,17 @@ describe('Racing application use-cases - teams', () => { getAllTeamsUseCase = new GetAllTeamsUseCase( teamRepo, membershipRepo, - allTeamsPresenter, ); teamDetailsPresenter = new TestTeamDetailsPresenter(); getTeamDetailsUseCase = new GetTeamDetailsUseCase( teamRepo, membershipRepo, - teamDetailsPresenter, ); const driverRepository = new FakeDriverRepository(); const imageService = new FakeImageService(); - + teamMembersPresenter = new TestTeamMembersPresenter(); getTeamMembersUseCase = new GetTeamMembersUseCase( membershipRepo, diff --git a/tests/unit/structure/packages/PackageDependencies.test.ts b/tests/unit/structure/packages/PackageDependencies.test.ts index 8a60393c0..fedb5f5a0 100644 --- a/tests/unit/structure/packages/PackageDependencies.test.ts +++ b/tests/unit/structure/packages/PackageDependencies.test.ts @@ -75,13 +75,13 @@ function extractImportModule(line: string): string | null { // Handle: import ... from 'x'; const fromMatch = trimmed.match(/from\s+['"](.*)['"]/); if (fromMatch) { - return fromMatch[1]; + return fromMatch[1] || null; } // Handle: import 'x'; const sideEffectMatch = trimmed.match(/^import\s+['"](.*)['"]\s*;?$/); if (sideEffectMatch) { - return sideEffectMatch[1]; + return sideEffectMatch[1] || null; } return null; diff --git a/tests/unit/website/AlphaNav.test.tsx b/tests/unit/website/AlphaNav.test.tsx index 662c3156e..af37396bd 100644 --- a/tests/unit/website/AlphaNav.test.tsx +++ b/tests/unit/website/AlphaNav.test.tsx @@ -45,8 +45,8 @@ vi.mock('../../../apps/website/lib/auth/AuthContext', () => { refreshSession: async () => {}, }); - const AuthProvider = ({ value, children }: { value: any; children: React.ReactNode }) => ( - {children} + const AuthProvider = ({ initialSession, children }: { initialSession?: any; children: React.ReactNode }) => ( + {}, logout: async () => {}, refreshSession: async () => {} }}>{children} ); const useAuth = () => React.useContext(AuthContext); @@ -65,13 +65,7 @@ describe('AlphaNav', () => { it('hides Dashboard link and uses Home when unauthenticated', () => { render( {}, - logout: async () => {}, - refreshSession: async () => {}, - }} + initialSession={null} > , @@ -87,14 +81,11 @@ describe('AlphaNav', () => { it('shows Dashboard link and hides Home when authenticated', () => { render( {}, - logout: async () => {}, - refreshSession: async () => {}, + initialSession={{ + user: { id: 'user-1', displayName: 'Test User' }, + issuedAt: Date.now(), + expiresAt: Date.now() + 3600000, + token: 'fake-token', }} > diff --git a/tests/unit/website/auth/IracingRoutes.test.ts b/tests/unit/website/auth/IracingRoutes.test.ts index be1a6bc57..fc1e4c453 100644 --- a/tests/unit/website/auth/IracingRoutes.test.ts +++ b/tests/unit/website/auth/IracingRoutes.test.ts @@ -35,7 +35,9 @@ describe('iRacing auth route handlers', () => { expect(location).toMatch(/state=/); expect(cookieStore.set).toHaveBeenCalled(); - const [name] = cookieStore.set.mock.calls[0]; + const call = cookieStore.set.mock.calls[0]; + expect(call).toBeDefined(); + const [name] = call as [string, string]; expect(name).toBe('gp_demo_auth_state'); }); @@ -58,7 +60,9 @@ describe('iRacing auth route handlers', () => { expect(location).toBe('http://localhost/dashboard'); expect(cookieStore.set).toHaveBeenCalled(); - const [sessionName, sessionValue] = cookieStore.set.mock.calls[0]; + const call = cookieStore.set.mock.calls[0]; + expect(call).toBeDefined(); + const [sessionName, sessionValue] = call as [string, string]; expect(sessionName).toBe('gp_demo_session'); expect(typeof sessionValue).toBe('string'); diff --git a/tests/unit/website/getAppMode.test.ts b/tests/unit/website/getAppMode.test.ts index 449d4de3a..e76a036d3 100644 --- a/tests/unit/website/getAppMode.test.ts +++ b/tests/unit/website/getAppMode.test.ts @@ -44,7 +44,7 @@ describe('getAppMode', () => { it('falls back to "pre-launch" and logs when NEXT_PUBLIC_GRIDPILOT_MODE is invalid in production', () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); - process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid-mode'; + process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid-mode' as any; const mode = getAppMode(); @@ -56,7 +56,7 @@ describe('getAppMode', () => { it('throws in development when NEXT_PUBLIC_GRIDPILOT_MODE is invalid', () => { (process.env as any).NODE_ENV = 'development'; - process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid-mode'; + process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid-mode' as any; expect(() => getAppMode()).toThrowError(/Invalid NEXT_PUBLIC_GRIDPILOT_MODE/); }); diff --git a/tests/unit/website/leagues/CreateLeaguePage.wizardStep.test.tsx b/tests/unit/website/leagues/CreateLeaguePage.wizardStep.test.tsx index 32015fc5f..41dfb99a6 100644 --- a/tests/unit/website/leagues/CreateLeaguePage.wizardStep.test.tsx +++ b/tests/unit/website/leagues/CreateLeaguePage.wizardStep.test.tsx @@ -103,7 +103,9 @@ describe('CreateLeaguePage - URL-bound wizard steps', () => { fireEvent.click(backButton); expect(routerInstance.push).toHaveBeenCalledTimes(1); - const callArg = routerInstance.push.mock.calls[0][0] as string; + const call = routerInstance.push.mock.calls[0]; + expect(call).toBeDefined(); + const callArg = (call as [string])[0]; expect(callArg).toContain('/leagues/create'); expect(callArg).toContain('step=structure'); }); diff --git a/tests/unit/website/signupRoute.test.ts b/tests/unit/website/signupRoute.test.ts index 0cfe56bcf..3723e2f48 100644 --- a/tests/unit/website/signupRoute.test.ts +++ b/tests/unit/website/signupRoute.test.ts @@ -6,12 +6,12 @@ type RateLimitResult = { resetAt: number; }; -const mockCheckRateLimit = vi.fn<[], Promise>(); -const mockGetClientIp = vi.fn<[], string>(); +const mockCheckRateLimit = vi.fn(() => Promise.resolve({ allowed: true, remaining: 4, resetAt: 0 })); +const mockGetClientIp = vi.fn(() => '127.0.0.1'); vi.mock('../../../apps/website/lib/rate-limit', () => ({ - checkRateLimit: (...args: unknown[]) => mockCheckRateLimit(...(args as [])), - getClientIp: (..._args: unknown[]) => mockGetClientIp(), + checkRateLimit: mockCheckRateLimit, + getClientIp: mockGetClientIp, })); async function getPostHandler() {