This commit is contained in:
2025-12-12 23:49:56 +01:00
parent cae81b1088
commit 8f1db21fb1
29 changed files with 879 additions and 399 deletions

View File

@@ -367,6 +367,7 @@ export function configureDIContainer(): void {
const penalty = Penalty.create({ const penalty = Penalty.create({
id: `penalty-${race.id}-${i}`, id: `penalty-${race.id}-${i}`,
leagueId: race.leagueId,
raceId: race.id, raceId: race.id,
driverId: accusedResult.driverId, driverId: accusedResult.driverId,
type: penaltyType, type: penaltyType,
@@ -390,6 +391,7 @@ export function configureDIContainer(): void {
if (penalizedResult) { if (penalizedResult) {
const penalty = Penalty.create({ const penalty = Penalty.create({
id: `penalty-direct-${race.id}`, id: `penalty-direct-${race.id}`,
leagueId: race.leagueId,
raceId: race.id, raceId: race.id,
driverId: penalizedResult.driverId, driverId: penalizedResult.driverId,
type: 'points_deduction', type: 'points_deduction',
@@ -410,6 +412,7 @@ export function configureDIContainer(): void {
if (penalizedResult) { if (penalizedResult) {
const penalty = Penalty.create({ const penalty = Penalty.create({
id: `penalty-direct-2-${race.id}`, id: `penalty-direct-2-${race.id}`,
leagueId: race.leagueId,
raceId: race.id, raceId: race.id,
driverId: penalizedResult.driverId, driverId: penalizedResult.driverId,
type: 'points_deduction', type: 'points_deduction',

View File

@@ -438,7 +438,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
private static readonly PAUSE_CHECK_INTERVAL = 300; private static readonly PAUSE_CHECK_INTERVAL = 300;
/** Checkout confirmation callback - called before clicking checkout button */ /** Checkout confirmation callback - called before clicking checkout button */
private checkoutConfirmationCallback?: (price: CheckoutPrice, state: CheckoutState) => Promise<CheckoutConfirmation>; private checkoutConfirmationCallback: ((price: CheckoutPrice, state: CheckoutState) => Promise<CheckoutConfirmation>) | undefined;
/** Page state validator instance */ /** Page state validator instance */
private pageStateValidator: PageStateValidator; private pageStateValidator: PageStateValidator;

View File

@@ -508,7 +508,7 @@ export class IRacingDomInteractor {
for (const sel of cands) { for (const sel of cands) {
try { try {
const els = Array.from(document.querySelectorAll(sel)) as HTMLInputElement[]; const els = Array.from(document.querySelectorAll(sel)) as HTMLElement[];
if (els.length === 0) continue; if (els.length === 0) continue;
for (const el of els) { for (const el of els) {
try { try {
@@ -516,10 +516,10 @@ export class IRacingDomInteractor {
(el as HTMLInputElement).checked = Boolean(should); (el as HTMLInputElement).checked = Boolean(should);
el.dispatchEvent(new Event('change', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true }));
} else { } else {
(el as HTMLElement).setAttribute('aria-checked', String(Boolean(should))); el.setAttribute('aria-checked', String(Boolean(should)));
el.dispatchEvent(new Event('change', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true }));
try { try {
(el as HTMLElement).click(); el.click();
} catch { } catch {
// ignore // ignore
} }
@@ -615,7 +615,7 @@ export class IRacingDomInteractor {
const applied = await page.evaluate( const applied = await page.evaluate(
({ sel, val }) => { ({ sel, val }) => {
try { 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; if (els.length === 0) return false;
for (const el of els) { for (const el of els) {
try { try {

View File

@@ -67,6 +67,7 @@ export class ApplyPenaltyUseCase
// Create the penalty // Create the penalty
const penalty = Penalty.create({ const penalty = Penalty.create({
id: randomUUID(), id: randomUUID(),
leagueId: race.leagueId,
raceId: command.raceId, raceId: command.raceId,
driverId: command.driverId, driverId: command.driverId,
type: command.type, type: command.type,

View File

@@ -23,6 +23,7 @@ export type PenaltyStatus = 'pending' | 'applied' | 'appealed' | 'overturned';
export interface PenaltyProps { export interface PenaltyProps {
id: string; id: string;
leagueId: string;
raceId: string; raceId: string;
/** The driver receiving the penalty */ /** The driver receiving the penalty */
driverId: string; driverId: string;
@@ -51,6 +52,7 @@ export class Penalty implements IEntity<string> {
static create(props: PenaltyProps): Penalty { static create(props: PenaltyProps): Penalty {
if (!props.id) throw new RacingDomainValidationError('Penalty ID is required'); 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.raceId) throw new RacingDomainValidationError('Race ID is required');
if (!props.driverId) throw new RacingDomainValidationError('Driver ID is required'); if (!props.driverId) throw new RacingDomainValidationError('Driver ID is required');
if (!props.type) throw new RacingDomainValidationError('Penalty type is required'); if (!props.type) throw new RacingDomainValidationError('Penalty type is required');
@@ -72,6 +74,7 @@ export class Penalty implements IEntity<string> {
} }
get id(): string { return this.props.id; } get id(): string { return this.props.id; }
get leagueId(): string { return this.props.leagueId; }
get raceId(): string { return this.props.raceId; } get raceId(): string { return this.props.raceId; }
get driverId(): string { return this.props.driverId; } get driverId(): string { return this.props.driverId; }
get type(): PenaltyType { return this.props.type; } get type(): PenaltyType { return this.props.type; }

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest' 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 { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../../packages/automation/infrastructure/adapters/IAutomationLifecycleEmitter'
import { OverlaySyncService } from '../../../../packages/automation/application/services/OverlaySyncService' import { OverlaySyncService } from '../../../../packages/automation/application/services/OverlaySyncService'
@@ -11,7 +11,7 @@ class MockLifecycleEmitter implements IAutomationLifecycleEmitter {
offLifecycle(cb: LifecycleCallback): void { offLifecycle(cb: LifecycleCallback): void {
this.callbacks.delete(cb) 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)) { for (const cb of Array.from(this.callbacks)) {
cb(event) cb(event)
} }

View File

@@ -56,7 +56,7 @@ describe('CompleteRaceCreationUseCase', () => {
const state = CheckoutState.ready(); const state = CheckoutState.ready();
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue( vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
Result.ok({ price: undefined, state, buttonHtml: '<a>n/a</a>' }) Result.ok({ price: null, state, buttonHtml: '<a>n/a</a>' })
); );
const result = await useCase.execute('test-session-123'); const result = await useCase.execute('test-session-123');

View File

@@ -4,12 +4,12 @@ import { Result } from '@gridpilot/shared-result';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice'; import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState'; import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation'; import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
import type { ICheckoutService } from '@gridpilot/automation/application/ports/ICheckoutService'; import type { CheckoutServicePort } from '@gridpilot/automation/application/ports/CheckoutServicePort';
import type { ICheckoutConfirmationPort } from '@gridpilot/automation/application/ports/ICheckoutConfirmationPort'; import type { CheckoutConfirmationPort } from '@gridpilot/automation/application/ports/CheckoutConfirmationPort';
describe('ConfirmCheckoutUseCase - Enhanced with Confirmation Port', () => { describe('ConfirmCheckoutUseCase - Enhanced with Confirmation Port', () => {
let mockCheckoutService: ICheckoutService; let mockCheckoutService: CheckoutServicePort;
let mockConfirmationPort: ICheckoutConfirmationPort; let mockConfirmationPort: CheckoutConfirmationPort;
let useCase: ConfirmCheckoutUseCase; let useCase: ConfirmCheckoutUseCase;
beforeEach(() => { beforeEach(() => {

View File

@@ -3,7 +3,7 @@ import { Result } from '../../../../packages/shared/result/Result';
import { ConfirmCheckoutUseCase } from '../../../../packages/automation/application/use-cases/ConfirmCheckoutUseCase'; import { ConfirmCheckoutUseCase } from '../../../../packages/automation/application/use-cases/ConfirmCheckoutUseCase';
import type { CheckoutServicePort } from '../../../../packages/automation/application/ports/CheckoutServicePort'; import type { CheckoutServicePort } from '../../../../packages/automation/application/ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '../../../../packages/automation/application/ports/CheckoutConfirmationPort'; 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 { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState, CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState'; import { CheckoutState, CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation'; import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';

View File

@@ -8,10 +8,10 @@ import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IR
import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/IPenaltyRepository'; import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/IPenaltyRepository';
import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/repositories/IChampionshipStandingRepository'; 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 { LeagueScoringConfig } from '@gridpilot/racing/domain/entities/LeagueScoringConfig';
import type { Race } from '@gridpilot/racing/domain/entities/Race'; import { Race } from '@gridpilot/racing/domain/entities/Race';
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 { Penalty } from '@gridpilot/racing/domain/entities/Penalty';
import type { ChampionshipStanding } from '@gridpilot/racing/domain/entities/ChampionshipStanding'; import type { ChampionshipStanding } from '@gridpilot/racing/domain/entities/ChampionshipStanding';
import type { ChampionshipConfig } from '@gridpilot/racing/domain/types/ChampionshipConfig'; 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); return this.seasons.filter((s) => s.leagueId === leagueId);
} }
async create(season: Season): Promise<Season> {
this.seasons.push(season);
return season;
}
async add(season: Season): Promise<void> {
this.seasons.push(season);
}
async update(season: Season): Promise<void> {
const index = this.seasons.findIndex((s) => s.id === season.id);
if (index >= 0) {
this.seasons[index] = season;
}
}
async listByLeague(leagueId: string): Promise<Season[]> {
return this.seasons.filter((s) => s.leagueId === leagueId);
}
async listActiveByLeague(leagueId: string): Promise<Season[]> {
return this.seasons.filter((s) => s.leagueId === leagueId && s.status === 'active');
}
seedSeason(season: Season): void { seedSeason(season: Season): void {
this.seasons.push(season); this.seasons.push(season);
} }
@@ -46,6 +70,16 @@ class InMemoryLeagueScoringConfigRepository implements ILeagueScoringConfigRepos
return this.configs.find((c) => c.seasonId === seasonId) || null; return this.configs.find((c) => c.seasonId === seasonId) || null;
} }
async save(config: LeagueScoringConfig): Promise<LeagueScoringConfig> {
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 { seedConfig(config: LeagueScoringConfig): void {
this.configs.push(config); this.configs.push(config);
} }
@@ -113,10 +147,60 @@ class InMemoryRaceRepository implements IRaceRepository {
class InMemoryResultRepository implements IResultRepository { class InMemoryResultRepository implements IResultRepository {
private results: Result[] = []; private results: Result[] = [];
async findById(id: string): Promise<Result | null> {
return this.results.find((r) => r.id === id) || null;
}
async findAll(): Promise<Result[]> {
return [...this.results];
}
async findByRaceId(raceId: string): Promise<Result[]> { async findByRaceId(raceId: string): Promise<Result[]> {
return this.results.filter((r) => r.raceId === raceId); return this.results.filter((r) => r.raceId === raceId);
} }
async findByDriverId(driverId: string): Promise<Result[]> {
return this.results.filter((r) => r.driverId === driverId);
}
async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise<Result[]> {
return this.results.filter((r) => r.driverId === driverId && r.raceId.startsWith(leagueId));
}
async create(result: Result): Promise<Result> {
this.results.push(result);
return result;
}
async createMany(results: Result[]): Promise<Result[]> {
this.results.push(...results);
return results;
}
async update(result: Result): Promise<Result> {
const index = this.results.findIndex((r) => r.id === result.id);
if (index >= 0) {
this.results[index] = result;
}
return result;
}
async delete(id: string): Promise<void> {
this.results = this.results.filter((r) => r.id !== id);
}
async deleteByRaceId(raceId: string): Promise<void> {
this.results = this.results.filter((r) => r.raceId !== raceId);
}
async exists(id: string): Promise<boolean> {
return this.results.some((r) => r.id === id);
}
async existsByRaceId(raceId: string): Promise<boolean> {
return this.results.some((r) => r.raceId === raceId);
}
seedResult(result: Result): void { seedResult(result: Result): void {
this.results.push(result); this.results.push(result);
} }
@@ -146,6 +230,41 @@ class InMemoryPenaltyRepository implements IPenaltyRepository {
return [...this.penalties]; return [...this.penalties];
} }
async findById(id: string): Promise<Penalty | null> {
return this.penalties.find((p) => p.id === id) || null;
}
async findByDriverId(driverId: string): Promise<Penalty[]> {
return this.penalties.filter((p) => p.driverId === driverId);
}
async findByProtestId(protestId: string): Promise<Penalty[]> {
return this.penalties.filter((p) => p.protestId === protestId);
}
async findPending(): Promise<Penalty[]> {
return this.penalties.filter((p) => p.status === 'pending');
}
async findIssuedBy(stewardId: string): Promise<Penalty[]> {
return this.penalties.filter((p) => p.issuedBy === stewardId);
}
async create(penalty: Penalty): Promise<void> {
this.penalties.push(penalty);
}
async update(penalty: Penalty): Promise<void> {
const index = this.penalties.findIndex((p) => p.id === penalty.id);
if (index >= 0) {
this.penalties[index] = penalty;
}
}
async exists(id: string): Promise<boolean> {
return this.penalties.some((p) => p.id === id);
}
seedPenalty(penalty: Penalty): void { seedPenalty(penalty: Penalty): void {
this.penalties.push(penalty); this.penalties.push(penalty);
} }
@@ -267,7 +386,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
championshipAggregator, championshipAggregator,
); );
const season: Season = { const season = Season.create({
id: seasonId, id: seasonId,
leagueId, leagueId,
gameId: 'iracing', gameId: 'iracing',
@@ -277,7 +396,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
order: 1, order: 1,
startDate: new Date('2025-01-01'), startDate: new Date('2025-01-01'),
endDate: new Date('2025-12-31'), endDate: new Date('2025-12-31'),
}; });
seasonRepository.seedSeason(season); seasonRepository.seedSeason(season);
@@ -292,7 +411,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
leagueScoringConfigRepository.seedConfig(leagueScoringConfig); leagueScoringConfigRepository.seedConfig(leagueScoringConfig);
const races: Race[] = [ const races: Race[] = [
{ Race.create({
id: 'race-1-sprint', id: 'race-1-sprint',
leagueId, leagueId,
scheduledAt: new Date('2025-02-01'), scheduledAt: new Date('2025-02-01'),
@@ -300,8 +419,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A', car: 'Car A',
sessionType: 'race', sessionType: 'race',
status: 'completed', status: 'completed',
}, }),
{ Race.create({
id: 'race-1-main', id: 'race-1-main',
leagueId, leagueId,
scheduledAt: new Date('2025-02-01'), scheduledAt: new Date('2025-02-01'),
@@ -309,8 +428,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A', car: 'Car A',
sessionType: 'race', sessionType: 'race',
status: 'completed', status: 'completed',
}, }),
{ Race.create({
id: 'race-2-sprint', id: 'race-2-sprint',
leagueId, leagueId,
scheduledAt: new Date('2025-03-01'), scheduledAt: new Date('2025-03-01'),
@@ -318,8 +437,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A', car: 'Car A',
sessionType: 'race', sessionType: 'race',
status: 'completed', status: 'completed',
}, }),
{ Race.create({
id: 'race-2-main', id: 'race-2-main',
leagueId, leagueId,
scheduledAt: new Date('2025-03-01'), scheduledAt: new Date('2025-03-01'),
@@ -327,8 +446,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A', car: 'Car A',
sessionType: 'race', sessionType: 'race',
status: 'completed', status: 'completed',
}, }),
{ Race.create({
id: 'race-3-sprint', id: 'race-3-sprint',
leagueId, leagueId,
scheduledAt: new Date('2025-04-01'), scheduledAt: new Date('2025-04-01'),
@@ -336,8 +455,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A', car: 'Car A',
sessionType: 'race', sessionType: 'race',
status: 'completed', status: 'completed',
}, }),
{ Race.create({
id: 'race-3-main', id: 'race-3-main',
leagueId, leagueId,
scheduledAt: new Date('2025-04-01'), scheduledAt: new Date('2025-04-01'),
@@ -345,7 +464,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A', car: 'Car A',
sessionType: 'race', sessionType: 'race',
status: 'completed', status: 'completed',
}, }),
]; ];
races.forEach((race) => raceRepository.seedRace(race)); races.forEach((race) => raceRepository.seedRace(race));
@@ -392,7 +511,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
let resultIdCounter = 1; let resultIdCounter = 1;
for (const raceData of resultsData) { for (const raceData of resultsData) {
raceData.finishingOrder.forEach((driverId, index) => { raceData.finishingOrder.forEach((driverId, index) => {
const result: Result = { const result = Result.create({
id: `result-${resultIdCounter++}`, id: `result-${resultIdCounter++}`,
raceId: raceData.raceId, raceId: raceData.raceId,
driverId, driverId,
@@ -400,7 +519,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
fastestLap: driverId === raceData.fastestLapDriverId ? 90000 : 91000 + index * 100, fastestLap: driverId === raceData.fastestLapDriverId ? 90000 : 91000 + index * 100,
incidents: 0, incidents: 0,
startPosition: index + 1, startPosition: index + 1,
}; });
resultRepository.seedResult(result); resultRepository.seedResult(result);
}); });
} }
@@ -423,7 +542,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
sorted.map((r) => r.participant.id), sorted.map((r) => r.participant.id),
); );
const leader = rows[0]; const leader = rows[0]!;
expect(leader.resultsCounted).toBeLessThanOrEqual(6); expect(leader.resultsCounted).toBeLessThanOrEqual(6);
expect(leader.resultsDropped).toBeGreaterThanOrEqual(0); expect(leader.resultsDropped).toBeGreaterThanOrEqual(0);
}); });

View File

@@ -1,8 +1,8 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { StartAutomationSessionUseCase } from '../../../../packages/automation/application/use-cases/StartAutomationSessionUseCase'; import { StartAutomationSessionUseCase } from '../../../../packages/automation/application/use-cases/StartAutomationSessionUseCase';
import { IAutomationEngine } from '../../../../packages/automation/application/ports/IAutomationEngine'; import { AutomationEnginePort as IAutomationEngine } from '../../../../packages/automation/application/ports/AutomationEnginePort';
import { IScreenAutomation } from '../../../../packages/automation/application/ports/IScreenAutomation'; import { IBrowserAutomation as IScreenAutomation } from '../../../../packages/automation/application/ports/ScreenAutomationPort';
import { ISessionRepository } from '../../../../packages/automation/application/ports/ISessionRepository'; import { SessionRepositoryPort as ISessionRepository } from '../../../../packages/automation/application/ports/SessionRepositoryPort';
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession'; import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
describe('StartAutomationSessionUseCase', () => { describe('StartAutomationSessionUseCase', () => {

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, vi } from 'vitest';
import { VerifyAuthenticatedPageUseCase } from '../../../../packages/automation/application/use-cases/VerifyAuthenticatedPageUseCase'; 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 { Result } from '../../../../packages/shared/result/Result';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState'; import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState'; import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';

View File

@@ -6,9 +6,9 @@ import type { SessionType } from '@gridpilot/racing/domain/types/SessionType';
import { PointsTable } from '@gridpilot/racing/domain/value-objects/PointsTable'; import { PointsTable } from '@gridpilot/racing/domain/value-objects/PointsTable';
import type { BonusRule } from '@gridpilot/racing/domain/types/BonusRule'; import type { BonusRule } from '@gridpilot/racing/domain/types/BonusRule';
import type { ChampionshipConfig } from '@gridpilot/racing/domain/types/ChampionshipConfig'; 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 { 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 { function makeDriverRef(id: string): ParticipantRef {
return { return {
@@ -83,7 +83,7 @@ describe('EventScoringService', () => {
}); });
const results: Result[] = [ const results: Result[] = [
{ Result.create({
id: 'result-1', id: 'result-1',
raceId: 'race-1', raceId: 'race-1',
driverId: 'driver-1', driverId: 'driver-1',
@@ -91,8 +91,8 @@ describe('EventScoringService', () => {
fastestLap: 90000, fastestLap: 90000,
incidents: 0, incidents: 0,
startPosition: 1, startPosition: 1,
}, }),
{ Result.create({
id: 'result-2', id: 'result-2',
raceId: 'race-1', raceId: 'race-1',
driverId: 'driver-2', driverId: 'driver-2',
@@ -100,8 +100,8 @@ describe('EventScoringService', () => {
fastestLap: 90500, fastestLap: 90500,
incidents: 0, incidents: 0,
startPosition: 2, startPosition: 2,
}, }),
{ Result.create({
id: 'result-3', id: 'result-3',
raceId: 'race-1', raceId: 'race-1',
driverId: 'driver-3', driverId: 'driver-3',
@@ -109,8 +109,8 @@ describe('EventScoringService', () => {
fastestLap: 91000, fastestLap: 91000,
incidents: 0, incidents: 0,
startPosition: 3, startPosition: 3,
}, }),
{ Result.create({
id: 'result-4', id: 'result-4',
raceId: 'race-1', raceId: 'race-1',
driverId: 'driver-4', driverId: 'driver-4',
@@ -118,8 +118,8 @@ describe('EventScoringService', () => {
fastestLap: 91500, fastestLap: 91500,
incidents: 0, incidents: 0,
startPosition: 4, startPosition: 4,
}, }),
{ Result.create({
id: 'result-5', id: 'result-5',
raceId: 'race-1', raceId: 'race-1',
driverId: 'driver-5', driverId: 'driver-5',
@@ -127,7 +127,7 @@ describe('EventScoringService', () => {
fastestLap: 92000, fastestLap: 92000,
incidents: 0, incidents: 0,
startPosition: 5, startPosition: 5,
}, }),
]; ];
const penalties: Penalty[] = []; const penalties: Penalty[] = [];
@@ -179,30 +179,30 @@ describe('EventScoringService', () => {
} as const; } as const;
const resultsP11Fastest: Result[] = [ const resultsP11Fastest: Result[] = [
{ Result.create({
id: 'result-1', id: 'result-1',
...baseResultTemplate, ...baseResultTemplate,
driverId: 'driver-1', driverId: 'driver-1',
position: 1, position: 1,
startPosition: 1, startPosition: 1,
fastestLap: 91000, fastestLap: 91000,
}, }),
{ Result.create({
id: 'result-2', id: 'result-2',
...baseResultTemplate, ...baseResultTemplate,
driverId: 'driver-2', driverId: 'driver-2',
position: 2, position: 2,
startPosition: 2, startPosition: 2,
fastestLap: 90500, fastestLap: 90500,
}, }),
{ Result.create({
id: 'result-3', id: 'result-3',
...baseResultTemplate, ...baseResultTemplate,
driverId: 'driver-3', driverId: 'driver-3',
position: 11, position: 11,
startPosition: 15, startPosition: 15,
fastestLap: 90000, fastestLap: 90000,
}, }),
]; ];
const penalties: Penalty[] = []; const penalties: Penalty[] = [];
@@ -220,30 +220,30 @@ describe('EventScoringService', () => {
expect(mapNoBonus.get('driver-3')?.bonusPoints).toBe(0); expect(mapNoBonus.get('driver-3')?.bonusPoints).toBe(0);
const resultsP8Fastest: Result[] = [ const resultsP8Fastest: Result[] = [
{ Result.create({
id: 'result-1', id: 'result-1',
...baseResultTemplate, ...baseResultTemplate,
driverId: 'driver-1', driverId: 'driver-1',
position: 1, position: 1,
startPosition: 1, startPosition: 1,
fastestLap: 91000, fastestLap: 91000,
}, }),
{ Result.create({
id: 'result-2', id: 'result-2',
...baseResultTemplate, ...baseResultTemplate,
driverId: 'driver-2', driverId: 'driver-2',
position: 2, position: 2,
startPosition: 2, startPosition: 2,
fastestLap: 90500, fastestLap: 90500,
}, }),
{ Result.create({
id: 'result-3', id: 'result-3',
...baseResultTemplate, ...baseResultTemplate,
driverId: 'driver-3', driverId: 'driver-3',
position: 8, position: 8,
startPosition: 15, startPosition: 15,
fastestLap: 90000, fastestLap: 90000,
}, }),
]; ];
const pointsWithBonus = service.scoreSession({ const pointsWithBonus = service.scoreSession({

View File

@@ -75,9 +75,9 @@ describe('ScheduleCalculator', () => {
expect(date.getDay()).toBe(6); // Saturday expect(date.getDay()).toBe(6); // Saturday
}); });
// First race should be Jan 6 // 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) // 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', () => { it('should schedule races on multiple weekdays', () => {
@@ -138,13 +138,13 @@ describe('ScheduleCalculator', () => {
// Then // Then
expect(result.raceDates.length).toBe(4); expect(result.raceDates.length).toBe(4);
// First race Jan 6 // 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) // 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) // 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) // 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 // Then
expect(result.raceDates.length).toBe(8); expect(result.raceDates.length).toBe(8);
// First race should be at or near start // 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 // Races should be spread across the range, not consecutive weeks
}); });

View File

@@ -19,7 +19,7 @@ describe('CheckoutConfirmation Value Object', () => {
}); });
it('should throw error for invalid decision', () => { it('should throw error for invalid decision', () => {
expect(() => CheckoutConfirmation.create('invalid')).toThrow( expect(() => CheckoutConfirmation.create('invalid' as any)).toThrow(
'Invalid checkout confirmation decision', 'Invalid checkout confirmation decision',
); );
}); });

View File

@@ -44,11 +44,11 @@ describe('SessionState Value Object', () => {
}); });
it('should throw error for invalid state', () => { 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', () => { it('should throw error for empty string', () => {
expect(() => SessionState.create('')).toThrow('Invalid session state'); expect(() => SessionState.create('' as any)).toThrow('Invalid session state');
}); });
}); });

View File

@@ -4,7 +4,7 @@ import { PlaywrightAuthSessionService } from '../../../../packages/automation/in
import type { PlaywrightBrowserSession } from '../../../../packages/automation/infrastructure/adapters/automation/core/PlaywrightBrowserSession'; import type { PlaywrightBrowserSession } from '../../../../packages/automation/infrastructure/adapters/automation/core/PlaywrightBrowserSession';
import type { SessionCookieStore } from '../../../../packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore'; import type { SessionCookieStore } from '../../../../packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore';
import type { IPlaywrightAuthFlow } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthFlow'; 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 { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { Result } from '../../../../packages/shared/result/Result'; import { Result } from '../../../../packages/shared/result/Result';
@@ -25,6 +25,9 @@ describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', ()
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
error: vi.fn(), error: vi.fn(),
fatal: vi.fn(),
child: vi.fn(),
flush: vi.fn().mockResolvedValue(undefined),
}; };
mockPage = { mockPage = {
@@ -107,7 +110,7 @@ describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', ()
}), }),
); );
const calledUrl = (mockPage.goto as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0] as string; const calledUrl = (mockPage.goto as unknown as ReturnType<typeof vi.fn>).mock.calls[0]![0] as string;
expect(calledUrl).not.toEqual('about:blank'); expect(calledUrl).not.toEqual('about:blank');
}); });

View File

@@ -3,7 +3,7 @@ import type { Page, Locator } from 'playwright';
import { PlaywrightAuthSessionService } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService'; import { PlaywrightAuthSessionService } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState'; import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState'; 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 { Result } from '../../../../packages/shared/result/Result';
import type { PlaywrightBrowserSession } from '../../../../packages/automation/infrastructure/adapters/automation/core/PlaywrightBrowserSession'; import type { PlaywrightBrowserSession } from '../../../../packages/automation/infrastructure/adapters/automation/core/PlaywrightBrowserSession';
import type { SessionCookieStore } from '../../../../packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore'; import type { SessionCookieStore } from '../../../../packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore';
@@ -21,6 +21,9 @@ describe('PlaywrightAuthSessionService.verifyPageAuthentication', () => {
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
error: vi.fn(), error: vi.fn(),
fatal: vi.fn(),
child: vi.fn(),
flush: vi.fn().mockResolvedValue(undefined),
}; };
const mockLocator: Locator = { const mockLocator: Locator = {

View File

@@ -197,7 +197,7 @@ describe('SessionCookieStore - Cookie Validation', () => {
const validCookies = cookieStore.getValidCookiesForUrl(targetUrl); const validCookies = cookieStore.getValidCookiesForUrl(targetUrl);
expect(validCookies).toHaveLength(1); 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 () => { test('should filter out cookies with mismatched domains', async () => {
@@ -228,7 +228,7 @@ describe('SessionCookieStore - Cookie Validation', () => {
const validCookies = cookieStore.getValidCookiesForUrl(targetUrl); const validCookies = cookieStore.getValidCookiesForUrl(targetUrl);
expect(validCookies).toHaveLength(1); 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 () => { test('should filter out cookies with invalid paths', async () => {
@@ -259,7 +259,7 @@ describe('SessionCookieStore - Cookie Validation', () => {
const validCookies = cookieStore.getValidCookiesForUrl(targetUrl); const validCookies = cookieStore.getValidCookiesForUrl(targetUrl);
expect(validCookies).toHaveLength(1); 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 () => { test('should return empty array when no cookies are valid', async () => {

View File

@@ -1,6 +1,13 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { GetDashboardOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetDashboardOverviewUseCase'; 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 { import type {
IDashboardOverviewPresenter, IDashboardOverviewPresenter,
DashboardOverviewViewModel, DashboardOverviewViewModel,
@@ -25,11 +32,17 @@ class FakeDashboardOverviewPresenter implements IDashboardOverviewPresenter {
interface TestImageService { interface TestImageService {
getDriverAvatar(driverId: string): string; getDriverAvatar(driverId: string): string;
getTeamLogo(teamId: string): string;
getLeagueCover(leagueId: string): string;
getLeagueLogo(leagueId: string): string;
} }
function createTestImageService(): TestImageService { function createTestImageService(): TestImageService {
return { return {
getDriverAvatar: (driverId: string) => `avatar-${driverId}`, 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 // Given a driver with memberships in two leagues and future races with mixed registration
const driverId = 'driver-1'; 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 = [ const leagues = [
{ id: 'league-1', name: 'Alpha League' }, League.create({ id: 'league-1', name: 'Alpha League', description: 'First league', ownerId: 'owner-1' }),
{ id: 'league-2', name: 'Beta League' }, League.create({ id: 'league-2', name: 'Beta League', description: 'Second league', ownerId: 'owner-2' }),
]; ];
const now = Date.now(); const now = Date.now();
const races = [ const races = [
{ Race.create({
id: 'race-1', id: 'race-1',
leagueId: 'league-1', leagueId: 'league-1',
track: 'Monza', track: 'Monza',
car: 'GT3', car: 'GT3',
scheduledAt: new Date(now + 60 * 60 * 1000), scheduledAt: new Date(now + 60 * 60 * 1000),
status: 'scheduled' as const, status: 'scheduled',
}, }),
{ Race.create({
id: 'race-2', id: 'race-2',
leagueId: 'league-1', leagueId: 'league-1',
track: 'Spa', track: 'Spa',
car: 'GT3', car: 'GT3',
scheduledAt: new Date(now + 2 * 60 * 60 * 1000), scheduledAt: new Date(now + 2 * 60 * 60 * 1000),
status: 'scheduled' as const, status: 'scheduled',
}, }),
{ Race.create({
id: 'race-3', id: 'race-3',
leagueId: 'league-2', leagueId: 'league-2',
track: 'Silverstone', track: 'Silverstone',
car: 'GT4', car: 'GT4',
scheduledAt: new Date(now + 3 * 60 * 60 * 1000), scheduledAt: new Date(now + 3 * 60 * 60 * 1000),
status: 'scheduled' as const, status: 'scheduled',
}, }),
{ Race.create({
id: 'race-4', id: 'race-4',
leagueId: 'league-2', leagueId: 'league-2',
track: 'Imola', track: 'Imola',
car: 'GT4', car: 'GT4',
scheduledAt: new Date(now + 4 * 60 * 60 * 1000), scheduledAt: new Date(now + 4 * 60 * 60 * 1000),
status: 'scheduled' as const, status: 'scheduled',
}, }),
]; ];
const results: unknown[] = []; const results: Result[] = [];
const memberships = [ const memberships = [
{ LeagueMembership.create({
leagueId: 'league-1', leagueId: 'league-1',
driverId, driverId,
role: 'member',
status: 'active', status: 'active',
}, }),
{ LeagueMembership.create({
leagueId: 'league-2', leagueId: 'league-2',
driverId, driverId,
role: 'member',
status: 'active', status: 'active',
}, }),
]; ];
const registeredRaceIds = new Set<string>(['race-1', 'race-3']); const registeredRaceIds = new Set<string>(['race-1', 'race-3']);
const feedItems: DashboardFeedItemSummaryViewModel[] = []; const feedItems: FeedItem[] = [];
const friends: Array<{ id: string }> = []; const friends: Driver[] = [];
const driverRepository: { const driverRepository = {
findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>; findById: async (id: string): Promise<Driver | null> => (id === driver.id ? driver : null),
} = { findByIRacingId: async (): Promise<Driver | null> => null,
findById: async (id: string) => (id === driver.id ? driver : null), findAll: async (): Promise<Driver[]> => [],
create: async (): Promise<Driver> => { throw new Error('Not implemented'); },
update: async (): Promise<Driver> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByIRacingId: async (): Promise<boolean> => false,
}; };
const raceRepository: { const raceRepository = {
findAll: () => Promise< findById: async (): Promise<Race | null> => null,
Array<{ findAll: async (): Promise<Race[]> => races,
id: string; findByLeagueId: async (): Promise<Race[]> => [],
leagueId: string; findUpcomingByLeagueId: async (): Promise<Race[]> => [],
track: string; findCompletedByLeagueId: async (): Promise<Race[]> => [],
car: string; findByStatus: async (): Promise<Race[]> => [],
scheduledAt: Date; findByDateRange: async (): Promise<Race[]> => [],
status: 'scheduled'; create: async (): Promise<Race> => { throw new Error('Not implemented'); },
}> update: async (): Promise<Race> => { throw new Error('Not implemented'); },
>; delete: async (): Promise<void> => { throw new Error('Not implemented'); },
} = { exists: async (): Promise<boolean> => false,
findAll: async () => races,
}; };
const resultRepository: { const resultRepository = {
findAll: () => Promise<unknown[]>; findById: async (): Promise<Result | null> => null,
} = { findAll: async (): Promise<Result[]> => results,
findAll: async () => results, findByRaceId: async (): Promise<Result[]> => [],
findByDriverId: async (): Promise<Result[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (): Promise<Result[]> => { throw new Error('Not implemented'); },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (): Promise<boolean> => false,
}; };
const leagueRepository: { const leagueRepository = {
findAll: () => Promise<Array<{ id: string; name: string }>>; findById: async (): Promise<League | null> => null,
} = { findAll: async (): Promise<League[]> => leagues,
findAll: async () => leagues, findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
}; };
const standingRepository: { const standingRepository = {
findByLeagueId: (leagueId: string) => Promise<unknown[]>; findByLeagueId: async (): Promise<Standing[]> => [],
} = { findByDriverIdAndLeagueId: async (): Promise<Standing | null> => null,
findByLeagueId: async () => [], findAll: async (): Promise<Standing[]> => [],
save: async (): Promise<Standing> => { throw new Error('Not implemented'); },
saveMany: async (): Promise<Standing[]> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByLeagueId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
recalculate: async (): Promise<Standing[]> => [],
}; };
const leagueMembershipRepository: { const leagueMembershipRepository = {
getMembership: ( getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => {
leagueId: string,
driverIdParam: string,
) => Promise<{ leagueId: string; driverId: string; status: string } | null>;
} = {
getMembership: async (leagueId: string, driverIdParam: string) => {
return ( return (
memberships.find( memberships.find(
(m) => m.leagueId === leagueId && m.driverId === driverIdParam, (m) => m.leagueId === leagueId && m.driverId === driverIdParam,
) ?? null ) ?? null
); );
}, },
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
saveMembership: async (): Promise<LeagueMembership> => { throw new Error('Not implemented'); },
removeMembership: async (): Promise<void> => { throw new Error('Not implemented'); },
saveJoinRequest: async (): Promise<any> => { throw new Error('Not implemented'); },
removeJoinRequest: async (): Promise<void> => { throw new Error('Not implemented'); },
}; };
const raceRegistrationRepository: { const raceRegistrationRepository = {
isRegistered: (raceId: string, driverIdParam: string) => Promise<boolean>; isRegistered: async (raceId: string, driverIdParam: string): Promise<boolean> => {
} = {
isRegistered: async (raceId: string, driverIdParam: string) => {
if (driverIdParam !== driverId) return false; if (driverIdParam !== driverId) return false;
return registeredRaceIds.has(raceId); return registeredRaceIds.has(raceId);
}, },
getRegisteredDrivers: async (): Promise<string[]> => [],
getRegistrationCount: async (): Promise<number> => 0,
register: async (): Promise<void> => { throw new Error('Not implemented'); },
withdraw: async (): Promise<void> => { throw new Error('Not implemented'); },
getDriverRegistrations: async (): Promise<string[]> => [],
clearRaceRegistrations: async (): Promise<void> => { throw new Error('Not implemented'); },
}; };
const feedRepository: { const feedRepository = {
getFeedForDriver: (driverIdParam: string) => Promise<DashboardFeedItemSummaryViewModel[]>; getFeedForDriver: async (): Promise<FeedItem[]> => feedItems,
} = { getGlobalFeed: async (): Promise<FeedItem[]> => [],
getFeedForDriver: async () => feedItems,
}; };
const socialRepository: { const socialRepository = {
getFriends: (driverIdParam: string) => Promise<Array<{ id: string }>>; getFriends: async (): Promise<Driver[]> => friends,
} = { getFriendIds: async (): Promise<string[]> => [],
getFriends: async () => friends, getSuggestedFriends: async (): Promise<Driver[]> => [],
}; };
const imageService = createTestImageService(); const imageService = createTestImageService();
@@ -230,138 +273,181 @@ describe('GetDashboardOverviewUseCase', () => {
// Given completed races with results and standings // Given completed races with results and standings
const driverId = 'driver-2'; 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 = [ const leagues = [
{ id: 'league-A', name: 'Results League A' }, League.create({ id: 'league-A', name: 'Results League A', description: 'League A', ownerId: 'owner-A' }),
{ id: 'league-B', name: 'Results League B' }, League.create({ id: 'league-B', name: 'Results League B', description: 'League B', ownerId: 'owner-B' }),
]; ];
const raceOld = { const raceOld = Race.create({
id: 'race-old', id: 'race-old',
leagueId: 'league-A', leagueId: 'league-A',
track: 'Old Circuit', track: 'Old Circuit',
car: 'GT3', car: 'GT3',
scheduledAt: new Date('2024-01-01T10:00:00Z'), scheduledAt: new Date('2024-01-01T10:00:00Z'),
status: 'completed' as const, status: 'completed',
}; });
const raceNew = { const raceNew = Race.create({
id: 'race-new', id: 'race-new',
leagueId: 'league-B', leagueId: 'league-B',
track: 'New Circuit', track: 'New Circuit',
car: 'GT4', car: 'GT4',
scheduledAt: new Date('2024-02-01T10:00:00Z'), scheduledAt: new Date('2024-02-01T10:00:00Z'),
status: 'completed' as const, status: 'completed',
}; });
const races = [raceOld, raceNew]; const races = [raceOld, raceNew];
const results = [ const results = [
{ Result.create({
id: 'result-old', id: 'result-old',
raceId: raceOld.id, raceId: raceOld.id,
driverId, driverId,
position: 5, position: 5,
fastestLap: 120,
incidents: 3, incidents: 3,
}, startPosition: 5,
{ }),
Result.create({
id: 'result-new', id: 'result-new',
raceId: raceNew.id, raceId: raceNew.id,
driverId, driverId,
position: 2, position: 2,
fastestLap: 115,
incidents: 1, incidents: 1,
}, startPosition: 2,
}),
]; ];
const memberships = [ const memberships = [
{ LeagueMembership.create({
leagueId: 'league-A', leagueId: 'league-A',
driverId, driverId,
role: 'member',
status: 'active', status: 'active',
}, }),
{ LeagueMembership.create({
leagueId: 'league-B', leagueId: 'league-B',
driverId, driverId,
role: 'member',
status: 'active', status: 'active',
}, }),
]; ];
const standingsByLeague = new Map< const standingsByLeague = new Map<
string, string,
Array<{ leagueId: string; driverId: string; position: number; points: number }> Standing[]
>(); >();
standingsByLeague.set('league-A', [ standingsByLeague.set('league-A', [
{ leagueId: 'league-A', driverId, position: 3, points: 50 }, Standing.create({ leagueId: 'league-A', driverId, position: 3, points: 50 }),
{ leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 }, Standing.create({ leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 }),
]); ]);
standingsByLeague.set('league-B', [ standingsByLeague.set('league-B', [
{ leagueId: 'league-B', driverId, position: 1, points: 100 }, Standing.create({ leagueId: 'league-B', driverId, position: 1, points: 100 }),
{ leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 }, Standing.create({ leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 }),
]); ]);
const driverRepository: { const driverRepository = {
findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>; findById: async (id: string): Promise<Driver | null> => (id === driver.id ? driver : null),
} = { findByIRacingId: async (): Promise<Driver | null> => null,
findById: async (id: string) => (id === driver.id ? driver : null), findAll: async (): Promise<Driver[]> => [],
create: async (): Promise<Driver> => { throw new Error('Not implemented'); },
update: async (): Promise<Driver> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByIRacingId: async (): Promise<boolean> => false,
}; };
const raceRepository: { const raceRepository = {
findAll: () => Promise<typeof races>; findById: async (): Promise<Race | null> => null,
} = { findAll: async (): Promise<Race[]> => races,
findAll: async () => races, findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const resultRepository: { const resultRepository = {
findAll: () => Promise<typeof results>; findById: async (): Promise<Result | null> => null,
} = { findAll: async (): Promise<Result[]> => results,
findAll: async () => results, findByRaceId: async (): Promise<Result[]> => [],
findByDriverId: async (): Promise<Result[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (): Promise<Result[]> => { throw new Error('Not implemented'); },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (): Promise<boolean> => false,
}; };
const leagueRepository: { const leagueRepository = {
findAll: () => Promise<typeof leagues>; findById: async (): Promise<League | null> => null,
} = { findAll: async (): Promise<League[]> => leagues,
findAll: async () => leagues, findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
}; };
const standingRepository: { const standingRepository = {
findByLeagueId: (leagueId: string) => Promise<Array<{ leagueId: string; driverId: string; position: number; points: number }>>; findByLeagueId: async (leagueId: string): Promise<Standing[]> =>
} = {
findByLeagueId: async (leagueId: string) =>
standingsByLeague.get(leagueId) ?? [], standingsByLeague.get(leagueId) ?? [],
findByDriverIdAndLeagueId: async (): Promise<Standing | null> => null,
findAll: async (): Promise<Standing[]> => [],
save: async (): Promise<Standing> => { throw new Error('Not implemented'); },
saveMany: async (): Promise<Standing[]> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByLeagueId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
recalculate: async (): Promise<Standing[]> => [],
}; };
const leagueMembershipRepository: { const leagueMembershipRepository = {
getMembership: ( getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => {
leagueId: string,
driverIdParam: string,
) => Promise<{ leagueId: string; driverId: string; status: string } | null>;
} = {
getMembership: async (leagueId: string, driverIdParam: string) => {
return ( return (
memberships.find( memberships.find(
(m) => m.leagueId === leagueId && m.driverId === driverIdParam, (m) => m.leagueId === leagueId && m.driverId === driverIdParam,
) ?? null ) ?? null
); );
}, },
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
saveMembership: async (): Promise<LeagueMembership> => { throw new Error('Not implemented'); },
removeMembership: async (): Promise<void> => { throw new Error('Not implemented'); },
saveJoinRequest: async (): Promise<any> => { throw new Error('Not implemented'); },
removeJoinRequest: async (): Promise<void> => { throw new Error('Not implemented'); },
}; };
const raceRegistrationRepository: { const raceRegistrationRepository = {
isRegistered: (raceId: string, driverIdParam: string) => Promise<boolean>; isRegistered: async (): Promise<boolean> => false,
} = { getRegisteredDrivers: async (): Promise<string[]> => [],
isRegistered: async () => false, getRegistrationCount: async (): Promise<number> => 0,
register: async (): Promise<void> => { throw new Error('Not implemented'); },
withdraw: async (): Promise<void> => { throw new Error('Not implemented'); },
getDriverRegistrations: async (): Promise<string[]> => [],
clearRaceRegistrations: async (): Promise<void> => { throw new Error('Not implemented'); },
}; };
const feedRepository: { const feedRepository = {
getFeedForDriver: (driverIdParam: string) => Promise<DashboardFeedItemSummaryViewModel[]>; getFeedForDriver: async (): Promise<FeedItem[]> => [],
} = { getGlobalFeed: async (): Promise<FeedItem[]> => [],
getFeedForDriver: async () => [],
}; };
const socialRepository: { const socialRepository = {
getFriends: (driverIdParam: string) => Promise<Array<{ id: string }>>; getFriends: async (): Promise<Driver[]> => [],
} = { getFriendIds: async (): Promise<string[]> => [],
getFriends: async () => [], getSuggestedFriends: async (): Promise<Driver[]> => [],
}; };
const imageService = createTestImageService(); const imageService = createTestImageService();
@@ -430,54 +516,100 @@ describe('GetDashboardOverviewUseCase', () => {
// Given a driver with no related data // Given a driver with no related data
const driverId = 'driver-empty'; 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: { const driverRepository = {
findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>; findById: async (id: string): Promise<Driver | null> => (id === driver.id ? driver : null),
} = { findByIRacingId: async (): Promise<Driver | null> => null,
findById: async (id: string) => (id === driver.id ? driver : null), findAll: async (): Promise<Driver[]> => [],
create: async (): Promise<Driver> => { throw new Error('Not implemented'); },
update: async (): Promise<Driver> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByIRacingId: async (): Promise<boolean> => false,
}; };
const raceRepository: { findAll: () => Promise<never[]> } = { const raceRepository = {
findAll: async () => [], findById: async (): Promise<Race | null> => null,
findAll: async (): Promise<Race[]> => [],
findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const resultRepository: { findAll: () => Promise<never[]> } = { const resultRepository = {
findAll: async () => [], findById: async (): Promise<Result | null> => null,
findAll: async (): Promise<Result[]> => [],
findByRaceId: async (): Promise<Result[]> => [],
findByDriverId: async (): Promise<Result[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (): Promise<Result[]> => { throw new Error('Not implemented'); },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (): Promise<boolean> => false,
}; };
const leagueRepository: { findAll: () => Promise<never[]> } = { const leagueRepository = {
findAll: async () => [], findById: async (): Promise<League | null> => null,
findAll: async (): Promise<League[]> => [],
findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
}; };
const standingRepository: { const standingRepository = {
findByLeagueId: (leagueId: string) => Promise<never[]>; findByLeagueId: async (): Promise<Standing[]> => [],
} = { findByDriverIdAndLeagueId: async (): Promise<Standing | null> => null,
findByLeagueId: async () => [], findAll: async (): Promise<Standing[]> => [],
save: async (): Promise<Standing> => { throw new Error('Not implemented'); },
saveMany: async (): Promise<Standing[]> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByLeagueId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
recalculate: async (): Promise<Standing[]> => [],
}; };
const leagueMembershipRepository: { const leagueMembershipRepository = {
getMembership: (leagueId: string, driverIdParam: string) => Promise<null>; getMembership: async (): Promise<LeagueMembership | null> => null,
} = { getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getMembership: async () => null, getJoinRequests: async (): Promise<any[]> => [],
saveMembership: async (): Promise<LeagueMembership> => { throw new Error('Not implemented'); },
removeMembership: async (): Promise<void> => { throw new Error('Not implemented'); },
saveJoinRequest: async (): Promise<any> => { throw new Error('Not implemented'); },
removeJoinRequest: async (): Promise<void> => { throw new Error('Not implemented'); },
}; };
const raceRegistrationRepository: { const raceRegistrationRepository = {
isRegistered: (raceId: string, driverIdParam: string) => Promise<boolean>; isRegistered: async (): Promise<boolean> => false,
} = { getRegisteredDrivers: async (): Promise<string[]> => [],
isRegistered: async () => false, getRegistrationCount: async (): Promise<number> => 0,
register: async (): Promise<void> => { throw new Error('Not implemented'); },
withdraw: async (): Promise<void> => { throw new Error('Not implemented'); },
getDriverRegistrations: async (): Promise<string[]> => [],
clearRaceRegistrations: async (): Promise<void> => { throw new Error('Not implemented'); },
}; };
const feedRepository: { const feedRepository = {
getFeedForDriver: (driverIdParam: string) => Promise<DashboardFeedItemSummaryViewModel[]>; getFeedForDriver: async (): Promise<FeedItem[]> => [],
} = { getGlobalFeed: async (): Promise<FeedItem[]> => [],
getFeedForDriver: async () => [],
}; };
const socialRepository: { const socialRepository = {
getFriends: (driverIdParam: string) => Promise<Array<{ id: string }>>; getFriends: async (): Promise<Driver[]> => [],
} = { getFriendIds: async (): Promise<string[]> => [],
getFriends: async () => [], getSuggestedFriends: async (): Promise<Driver[]> => [],
}; };
const imageService = createTestImageService(); const imageService = createTestImageService();

View File

@@ -2,10 +2,10 @@ import { describe, it, expect, beforeEach } from 'vitest';
import { JoinLeagueUseCase } from '@gridpilot/racing/application/use-cases/JoinLeagueUseCase'; import { JoinLeagueUseCase } from '@gridpilot/racing/application/use-cases/JoinLeagueUseCase';
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository'; import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
import type { import {
LeagueMembership, LeagueMembership,
MembershipRole, type MembershipRole,
MembershipStatus, type MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership'; } from '@gridpilot/racing/domain/entities/LeagueMembership';
class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository { class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
@@ -109,13 +109,13 @@ describe('Membership use-cases', () => {
const leagueId = 'league-1'; const leagueId = 'league-1';
const driverId = 'driver-1'; const driverId = 'driver-1';
repository.seedMembership({ repository.seedMembership(LeagueMembership.create({
leagueId, leagueId,
driverId, driverId,
role: 'member', role: 'member',
status: 'active', status: 'active',
joinedAt: new Date('2024-01-01'), joinedAt: new Date('2024-01-01'),
}); }));
await expect( await expect(
useCase.execute({ leagueId, driverId }), useCase.execute({ leagueId, driverId }),

View File

@@ -4,6 +4,8 @@ import { Race } from '@gridpilot/racing/domain/entities/Race';
import { League } from '@gridpilot/racing/domain/entities/League'; import { League } from '@gridpilot/racing/domain/entities/League';
import { Result } from '@gridpilot/racing/domain/entities/Result'; import { Result } from '@gridpilot/racing/domain/entities/Result';
import { Penalty } from '@gridpilot/racing/domain/entities/Penalty'; 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 { GetRaceResultsDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceResultsDetailUseCase';
import { ImportRaceResultsUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsUseCase'; import { ImportRaceResultsUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsUseCase';
@@ -77,37 +79,64 @@ describe('ImportRaceResultsUseCase', () => {
let existsByRaceIdCalled = false; let existsByRaceIdCalled = false;
const recalcCalls: string[] = []; const recalcCalls: string[] = [];
const raceRepository: { const raceRepository = {
findById: (id: string) => Promise<Race | null>; findById: async (id: string): Promise<Race | null> => races.get(id) ?? null,
} = { findAll: async (): Promise<Race[]> => [],
findById: async (id: string) => races.get(id) ?? null, findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const leagueRepository: { const leagueRepository = {
findById: (id: string) => Promise<League | null>; findById: async (id: string): Promise<League | null> => leagues.get(id) ?? null,
} = { findAll: async (): Promise<League[]> => [],
findById: async (id: string) => leagues.get(id) ?? null, findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
}; };
const resultRepository: { const resultRepository = {
existsByRaceId: (raceId: string) => Promise<boolean>; findById: async (): Promise<Result | null> => null,
createMany: (results: Result[]) => Promise<Result[]>; findAll: async (): Promise<Result[]> => [],
} = { findByRaceId: async (): Promise<Result[]> => [],
existsByRaceId: async (raceId: string) => { findByDriverId: async (): Promise<Result[]> => [],
existsByRaceIdCalled = true; findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
return storedResults.some((r) => r.raceId === raceId); create: async (): Promise<Result> => { throw new Error('Not implemented'); },
}, createMany: async (results: Result[]): Promise<Result[]> => {
createMany: async (results: Result[]) => {
storedResults.push(...results); storedResults.push(...results);
return results; return results;
}, },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (raceId: string): Promise<boolean> => {
existsByRaceIdCalled = true;
return storedResults.some((r) => r.raceId === raceId);
},
}; };
const standingRepository: { const standingRepository = {
recalculate: (leagueId: string) => Promise<void>; findByLeagueId: async (): Promise<Standing[]> => [],
} = { findByDriverIdAndLeagueId: async (): Promise<Standing | null> => null,
recalculate: async (leagueId: string) => { findAll: async (): Promise<Standing[]> => [],
save: async (): Promise<Standing> => { throw new Error('Not implemented'); },
saveMany: async (): Promise<Standing[]> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByLeagueId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
recalculate: async (leagueId: string): Promise<Standing[]> => {
recalcCalls.push(leagueId); recalcCalls.push(leagueId);
return [];
}, },
}; };
@@ -196,34 +225,60 @@ describe('ImportRaceResultsUseCase', () => {
}), }),
]; ];
const raceRepository: { const raceRepository = {
findById: (id: string) => Promise<Race | null>; findById: async (id: string): Promise<Race | null> => races.get(id) ?? null,
} = { findAll: async (): Promise<Race[]> => [],
findById: async (id: string) => races.get(id) ?? null, findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const leagueRepository: { const leagueRepository = {
findById: (id: string) => Promise<League | null>; findById: async (id: string): Promise<League | null> => leagues.get(id) ?? null,
} = { findAll: async (): Promise<League[]> => [],
findById: async (id: string) => leagues.get(id) ?? null, findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
}; };
const resultRepository: { const resultRepository = {
existsByRaceId: (raceId: string) => Promise<boolean>; findById: async (): Promise<Result | null> => null,
createMany: (results: Result[]) => Promise<Result[]>; findAll: async (): Promise<Result[]> => [],
} = { findByRaceId: async (): Promise<Result[]> => [],
existsByRaceId: async (raceId: string) => { findByDriverId: async (): Promise<Result[]> => [],
return storedResults.some((r) => r.raceId === raceId); findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
}, create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (_results: Result[]) => { createMany: async (_results: Result[]): Promise<Result[]> => {
throw new Error('Should not be called when results already exist'); throw new Error('Should not be called when results already exist');
}, },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (raceId: string): Promise<boolean> => {
return storedResults.some((r) => r.raceId === raceId);
},
}; };
const standingRepository: { const standingRepository = {
recalculate: (leagueId: string) => Promise<void>; findByLeagueId: async (): Promise<Standing[]> => [],
} = { findByDriverIdAndLeagueId: async (): Promise<Standing | null> => null,
recalculate: async (_leagueId: string) => { findAll: async (): Promise<Standing[]> => [],
save: async (): Promise<Standing> => { throw new Error('Not implemented'); },
saveMany: async (): Promise<Standing[]> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByLeagueId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
recalculate: async (_leagueId: string): Promise<Standing[]> => {
throw new Error('Should not be called when results already exist'); throw new Error('Should not be called when results already exist');
}, },
}; };
@@ -315,35 +370,68 @@ describe('GetRaceResultsDetailUseCase', () => {
const results = [result1, result2]; const results = [result1, result2];
const drivers = [driver1, driver2]; const drivers = [driver1, driver2];
const raceRepository: { const raceRepository = {
findById: (id: string) => Promise<Race | null>; findById: async (id: string): Promise<Race | null> => races.get(id) ?? null,
} = { findAll: async (): Promise<Race[]> => [],
findById: async (id: string) => races.get(id) ?? null, findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const leagueRepository: { const leagueRepository = {
findById: (id: string) => Promise<League | null>; findById: async (id: string): Promise<League | null> => leagues.get(id) ?? null,
} = { findAll: async (): Promise<League[]> => [],
findById: async (id: string) => leagues.get(id) ?? null, findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
}; };
const resultRepository: { const resultRepository = {
findByRaceId: (raceId: string) => Promise<Result[]>; findById: async (): Promise<Result | null> => null,
} = { findAll: async (): Promise<Result[]> => [],
findByRaceId: async (raceId: string) => findByRaceId: async (raceId: string): Promise<Result[]> =>
results.filter((r) => r.raceId === raceId), results.filter((r) => r.raceId === raceId),
findByDriverId: async (): Promise<Result[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (): Promise<Result[]> => { throw new Error('Not implemented'); },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (): Promise<boolean> => false,
}; };
const driverRepository: { const driverRepository = {
findAll: () => Promise<Array<{ id: string; name: string; country: string }>>; findById: async (): Promise<Driver | null> => null,
} = { findByIRacingId: async (): Promise<Driver | null> => null,
findAll: async () => drivers, findAll: async (): Promise<Driver[]> => drivers.map(d => Driver.create({ id: d.id, iracingId: '123', name: d.name, country: d.country })),
create: async (): Promise<Driver> => { throw new Error('Not implemented'); },
update: async (): Promise<Driver> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByIRacingId: async (): Promise<boolean> => false,
}; };
const penaltyRepository: { const penaltyRepository = {
findByRaceId: (raceId: string) => Promise<Penalty[]>; findById: async (): Promise<Penalty | null> => null,
} = { findByRaceId: async (): Promise<Penalty[]> => [] as Penalty[],
findByRaceId: async () => [] as Penalty[], findByDriverId: async (): Promise<Penalty[]> => [],
findByProtestId: async (): Promise<Penalty[]> => [],
findPending: async (): Promise<Penalty[]> => [],
findIssuedBy: async (): Promise<Penalty[]> => [],
create: async (): Promise<void> => { throw new Error('Not implemented'); },
update: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const presenter = new FakeRaceResultsDetailPresenter(); const presenter = new FakeRaceResultsDetailPresenter();
@@ -354,7 +442,6 @@ describe('GetRaceResultsDetailUseCase', () => {
resultRepository, resultRepository,
driverRepository, driverRepository,
penaltyRepository, penaltyRepository,
presenter,
); );
// When executing the query // When executing the query
@@ -364,8 +451,8 @@ describe('GetRaceResultsDetailUseCase', () => {
expect(viewModel).not.toBeNull(); expect(viewModel).not.toBeNull();
// Then points system matches the default F1-style configuration // Then points system matches the default F1-style configuration
expect(viewModel!.pointsSystem[1]).toBe(25); expect(viewModel!.pointsSystem?.[1]).toBe(25);
expect(viewModel!.pointsSystem[2]).toBe(18); expect(viewModel!.pointsSystem?.[2]).toBe(18);
// And fastest lap is identified correctly // And fastest lap is identified correctly
expect(viewModel!.fastestLapTime).toBeCloseTo(88.456, 3); expect(viewModel!.fastestLapTime).toBeCloseTo(88.456, 3);
@@ -408,6 +495,7 @@ describe('GetRaceResultsDetailUseCase', () => {
const penalty = Penalty.create({ const penalty = Penalty.create({
id: 'pen-1', id: 'pen-1',
leagueId: league.id,
raceId: race.id, raceId: race.id,
driverId: driver.id, driverId: driver.id,
type: 'points_deduction', type: 'points_deduction',
@@ -424,36 +512,69 @@ describe('GetRaceResultsDetailUseCase', () => {
const drivers = [driver]; const drivers = [driver];
const penalties = [penalty]; const penalties = [penalty];
const raceRepository: { const raceRepository = {
findById: (id: string) => Promise<Race | null>; findById: async (id: string): Promise<Race | null> => races.get(id) ?? null,
} = { findAll: async (): Promise<Race[]> => [],
findById: async (id: string) => races.get(id) ?? null, findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const leagueRepository: { const leagueRepository = {
findById: (id: string) => Promise<League | null>; findById: async (id: string): Promise<League | null> => leagues.get(id) ?? null,
} = { findAll: async (): Promise<League[]> => [],
findById: async (id: string) => leagues.get(id) ?? null, findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
}; };
const resultRepository: { const resultRepository = {
findByRaceId: (raceId: string) => Promise<Result[]>; findById: async (): Promise<Result | null> => null,
} = { findAll: async (): Promise<Result[]> => [],
findByRaceId: async (raceId: string) => findByRaceId: async (raceId: string): Promise<Result[]> =>
results.filter((r) => r.raceId === raceId), results.filter((r) => r.raceId === raceId),
findByDriverId: async (): Promise<Result[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (): Promise<Result[]> => { throw new Error('Not implemented'); },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (): Promise<boolean> => false,
}; };
const driverRepository: { const driverRepository = {
findAll: () => Promise<Array<{ id: string; name: string; country: string }>>; findById: async (): Promise<Driver | null> => null,
} = { findByIRacingId: async (): Promise<Driver | null> => null,
findAll: async () => drivers, findAll: async (): Promise<Driver[]> => drivers.map(d => Driver.create({ id: d.id, iracingId: '123', name: d.name, country: d.country })),
create: async (): Promise<Driver> => { throw new Error('Not implemented'); },
update: async (): Promise<Driver> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByIRacingId: async (): Promise<boolean> => false,
}; };
const penaltyRepository: { const penaltyRepository = {
findByRaceId: (raceId: string) => Promise<Penalty[]>; findById: async (): Promise<Penalty | null> => null,
} = { findByRaceId: async (raceId: string): Promise<Penalty[]> =>
findByRaceId: async (raceId: string) =>
penalties.filter((p) => p.raceId === raceId), penalties.filter((p) => p.raceId === raceId),
findByDriverId: async (): Promise<Penalty[]> => [],
findByProtestId: async (): Promise<Penalty[]> => [],
findPending: async (): Promise<Penalty[]> => [],
findIssuedBy: async (): Promise<Penalty[]> => [],
create: async (): Promise<void> => { throw new Error('Not implemented'); },
update: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const presenter = new FakeRaceResultsDetailPresenter(); const presenter = new FakeRaceResultsDetailPresenter();
@@ -464,7 +585,6 @@ describe('GetRaceResultsDetailUseCase', () => {
resultRepository, resultRepository,
driverRepository, driverRepository,
penaltyRepository, penaltyRepository,
presenter,
); );
// When // When
@@ -491,34 +611,67 @@ describe('GetRaceResultsDetailUseCase', () => {
it('presents an error when race does not exist', async () => { it('presents an error when race does not exist', async () => {
// Given repositories without the requested race // Given repositories without the requested race
const raceRepository: { const raceRepository = {
findById: (id: string) => Promise<Race | null>; findById: async (): Promise<Race | null> => null,
} = { findAll: async (): Promise<Race[]> => [],
findById: async () => null, findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const leagueRepository: { const leagueRepository = {
findById: (id: string) => Promise<League | null>; findById: async (): Promise<League | null> => null,
} = { findAll: async (): Promise<League[]> => [],
findById: async () => null, findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
}; };
const resultRepository: { const resultRepository = {
findByRaceId: (raceId: string) => Promise<Result[]>; findById: async (): Promise<Result | null> => null,
} = { findAll: async (): Promise<Result[]> => [],
findByRaceId: async () => [] as Result[], findByRaceId: async (): Promise<Result[]> => [] as Result[],
findByDriverId: async (): Promise<Result[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (): Promise<Result[]> => { throw new Error('Not implemented'); },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (): Promise<boolean> => false,
}; };
const driverRepository: { const driverRepository = {
findAll: () => Promise<Array<{ id: string; name: string; country: string }>>; findById: async (): Promise<Driver | null> => null,
} = { findByIRacingId: async (): Promise<Driver | null> => null,
findAll: async () => [], findAll: async (): Promise<Driver[]> => [],
create: async (): Promise<Driver> => { throw new Error('Not implemented'); },
update: async (): Promise<Driver> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByIRacingId: async (): Promise<boolean> => false,
}; };
const penaltyRepository: { const penaltyRepository = {
findByRaceId: (raceId: string) => Promise<Penalty[]>; findById: async (): Promise<Penalty | null> => null,
} = { findByRaceId: async (): Promise<Penalty[]> => [] as Penalty[],
findByRaceId: async () => [] as Penalty[], findByDriverId: async (): Promise<Penalty[]> => [],
findByProtestId: async (): Promise<Penalty[]> => [],
findPending: async (): Promise<Penalty[]> => [],
findIssuedBy: async (): Promise<Penalty[]> => [],
create: async (): Promise<void> => { throw new Error('Not implemented'); },
update: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
}; };
const presenter = new FakeRaceResultsDetailPresenter(); const presenter = new FakeRaceResultsDetailPresenter();
@@ -529,7 +682,6 @@ describe('GetRaceResultsDetailUseCase', () => {
resultRepository, resultRepository,
driverRepository, driverRepository,
penaltyRepository, penaltyRepository,
presenter,
); );
// When // When

View File

@@ -9,13 +9,14 @@ import {
LeagueMembership, LeagueMembership,
type MembershipStatus, type MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership'; } from '@gridpilot/racing/domain/entities/LeagueMembership';
import { Team } from '@gridpilot/racing/domain/entities/Team';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type { import type {
Team,
TeamMembership, TeamMembership,
TeamMembershipStatus, TeamMembershipStatus,
TeamRole, TeamRole,
TeamJoinRequest, TeamJoinRequest,
} from '@gridpilot/racing/domain/entities/Team'; } from '@gridpilot/racing/domain/types/TeamMembership';
import { RegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/RegisterForRaceUseCase'; import { RegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/RegisterForRaceUseCase';
import { WithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/WithdrawFromRaceUseCase'; import { WithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/WithdrawFromRaceUseCase';
@@ -169,10 +170,23 @@ class TestDriverRegistrationStatusPresenter implements IDriverRegistrationStatus
raceId: string | null = null; raceId: string | null = null;
driverId: 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.isRegistered = isRegistered;
this.raceId = raceId; this.raceId = raceId;
this.driverId = driverId; 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 = []; this.driverIds = [];
} }
present(input: RaceRegistrationsResultDTO): void { present(input: RaceRegistrationsResultDTO) {
this.driverIds = input.registeredDriverIds; this.driverIds = input.registeredDriverIds;
this.raceId = null; 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; let getDriverTeamUseCase: GetDriverTeamUseCase;
class FakeDriverRepository { class FakeDriverRepository {
async findById(driverId: string): Promise<{ id: string; name: string } | null> { async findById(driverId: string): Promise<Driver | null> {
return { id: driverId, name: `Driver ${driverId}` }; return Driver.create({ id: driverId, iracingId: '123', name: `Driver ${driverId}`, country: 'US' });
}
async findByIRacingId(id: string): Promise<Driver | null> {
return null;
}
async findAll(): Promise<Driver[]> {
return [];
}
async create(driver: Driver): Promise<Driver> {
return driver;
}
async update(driver: Driver): Promise<Driver> {
return driver;
}
async delete(id: string): Promise<void> {
}
async exists(id: string): Promise<boolean> {
return false;
}
async existsByIRacingId(iracingId: string): Promise<boolean> {
return false;
}
async findByLeagueId(leagueId: string): Promise<Driver[]> {
return [];
}
async findByTeamId(teamId: string): Promise<Driver[]> {
return [];
} }
} }
@@ -420,6 +480,18 @@ describe('Racing application use-cases - teams', () => {
getDriverAvatar(driverId: string): string { getDriverAvatar(driverId: string): string {
return `https://example.com/avatar/${driverId}.png`; 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 { class TestAllTeamsPresenter implements IAllTeamsPresenter {
@@ -438,9 +510,9 @@ describe('Racing application use-cases - teams', () => {
description: team.description, description: team.description,
memberCount: team.memberCount, memberCount: team.memberCount,
leagues: team.leagues, leagues: team.leagues,
specialization: team.specialization, specialization: (team as any).specialization,
region: team.region, region: (team as any).region,
languages: team.languages, languages: (team as any).languages,
})), })),
totalCount: input.teams.length, totalCount: input.teams.length,
}; };
@@ -487,7 +559,7 @@ describe('Racing application use-cases - teams', () => {
return { return {
driverId, driverId,
driverName, 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(), joinedAt: membership.joinedAt.toISOString(),
isActive: membership.status === 'active', isActive: membership.status === 'active',
avatarUrl, avatarUrl,
@@ -496,7 +568,7 @@ describe('Racing application use-cases - teams', () => {
const ownerCount = members.filter((m) => m.role === 'owner').length; const ownerCount = members.filter((m) => m.role === 'owner').length;
const managerCount = members.filter((m) => m.role === 'manager').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 = { this.viewModel = {
members, members,
@@ -534,7 +606,7 @@ describe('Racing application use-cases - teams', () => {
driverId, driverId,
driverName, driverName,
teamId: request.teamId, teamId: request.teamId,
status: 'pending', status: 'pending' as const,
requestedAt: request.requestedAt.toISOString(), requestedAt: request.requestedAt.toISOString(),
avatarUrl, avatarUrl,
}; };
@@ -559,7 +631,7 @@ describe('Racing application use-cases - teams', () => {
} }
class TestDriverTeamPresenter implements IDriverTeamPresenter { class TestDriverTeamPresenter implements IDriverTeamPresenter {
private viewModel: DriverTeamViewModel | null = null; viewModel: DriverTeamViewModel | null = null;
reset(): void { reset(): void {
this.viewModel = null; this.viewModel = null;
@@ -579,12 +651,9 @@ describe('Racing application use-cases - teams', () => {
description: team.description, description: team.description,
ownerId: team.ownerId, ownerId: team.ownerId,
leagues: team.leagues, leagues: team.leagues,
specialization: team.specialization,
region: team.region,
languages: team.languages,
}, },
membership: { membership: {
role: membership.role, role: (membership.role === 'owner' || membership.role === 'manager') ? membership.role : 'member' as "owner" | "member" | "manager",
joinedAt: membership.joinedAt.toISOString(), joinedAt: membership.joinedAt.toISOString(),
isActive: membership.status === 'active', isActive: membership.status === 'active',
}, },
@@ -619,14 +688,12 @@ describe('Racing application use-cases - teams', () => {
getAllTeamsUseCase = new GetAllTeamsUseCase( getAllTeamsUseCase = new GetAllTeamsUseCase(
teamRepo, teamRepo,
membershipRepo, membershipRepo,
allTeamsPresenter,
); );
teamDetailsPresenter = new TestTeamDetailsPresenter(); teamDetailsPresenter = new TestTeamDetailsPresenter();
getTeamDetailsUseCase = new GetTeamDetailsUseCase( getTeamDetailsUseCase = new GetTeamDetailsUseCase(
teamRepo, teamRepo,
membershipRepo, membershipRepo,
teamDetailsPresenter,
); );
const driverRepository = new FakeDriverRepository(); const driverRepository = new FakeDriverRepository();

View File

@@ -75,13 +75,13 @@ function extractImportModule(line: string): string | null {
// Handle: import ... from 'x'; // Handle: import ... from 'x';
const fromMatch = trimmed.match(/from\s+['"](.*)['"]/); const fromMatch = trimmed.match(/from\s+['"](.*)['"]/);
if (fromMatch) { if (fromMatch) {
return fromMatch[1]; return fromMatch[1] || null;
} }
// Handle: import 'x'; // Handle: import 'x';
const sideEffectMatch = trimmed.match(/^import\s+['"](.*)['"]\s*;?$/); const sideEffectMatch = trimmed.match(/^import\s+['"](.*)['"]\s*;?$/);
if (sideEffectMatch) { if (sideEffectMatch) {
return sideEffectMatch[1]; return sideEffectMatch[1] || null;
} }
return null; return null;

View File

@@ -45,8 +45,8 @@ vi.mock('../../../apps/website/lib/auth/AuthContext', () => {
refreshSession: async () => {}, refreshSession: async () => {},
}); });
const AuthProvider = ({ value, children }: { value: any; children: React.ReactNode }) => ( const AuthProvider = ({ initialSession, children }: { initialSession?: any; children: React.ReactNode }) => (
<AuthContext.Provider value={value}>{children}</AuthContext.Provider> <AuthContext.Provider value={{ session: initialSession, loading: false, login: () => {}, logout: async () => {}, refreshSession: async () => {} }}>{children}</AuthContext.Provider>
); );
const useAuth = () => React.useContext(AuthContext); const useAuth = () => React.useContext(AuthContext);
@@ -65,13 +65,7 @@ describe('AlphaNav', () => {
it('hides Dashboard link and uses Home when unauthenticated', () => { it('hides Dashboard link and uses Home when unauthenticated', () => {
render( render(
<AuthProvider <AuthProvider
value={{ initialSession={null}
session: null,
loading: false,
login: () => {},
logout: async () => {},
refreshSession: async () => {},
}}
> >
<AlphaNav /> <AlphaNav />
</AuthProvider>, </AuthProvider>,
@@ -87,14 +81,11 @@ describe('AlphaNav', () => {
it('shows Dashboard link and hides Home when authenticated', () => { it('shows Dashboard link and hides Home when authenticated', () => {
render( render(
<AuthProvider <AuthProvider
value={{ initialSession={{
session: { user: { id: 'user-1', displayName: 'Test User' },
user: { id: 'user-1' }, issuedAt: Date.now(),
}, expiresAt: Date.now() + 3600000,
loading: false, token: 'fake-token',
login: () => {},
logout: async () => {},
refreshSession: async () => {},
}} }}
> >
<AlphaNav /> <AlphaNav />

View File

@@ -35,7 +35,9 @@ describe('iRacing auth route handlers', () => {
expect(location).toMatch(/state=/); expect(location).toMatch(/state=/);
expect(cookieStore.set).toHaveBeenCalled(); 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'); expect(name).toBe('gp_demo_auth_state');
}); });
@@ -58,7 +60,9 @@ describe('iRacing auth route handlers', () => {
expect(location).toBe('http://localhost/dashboard'); expect(location).toBe('http://localhost/dashboard');
expect(cookieStore.set).toHaveBeenCalled(); 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(sessionName).toBe('gp_demo_session');
expect(typeof sessionValue).toBe('string'); expect(typeof sessionValue).toBe('string');

View File

@@ -44,7 +44,7 @@ describe('getAppMode', () => {
it('falls back to "pre-launch" and logs when NEXT_PUBLIC_GRIDPILOT_MODE is invalid in production', () => { it('falls back to "pre-launch" and logs when NEXT_PUBLIC_GRIDPILOT_MODE is invalid in production', () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); 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(); const mode = getAppMode();
@@ -56,7 +56,7 @@ describe('getAppMode', () => {
it('throws in development when NEXT_PUBLIC_GRIDPILOT_MODE is invalid', () => { it('throws in development when NEXT_PUBLIC_GRIDPILOT_MODE is invalid', () => {
(process.env as any).NODE_ENV = 'development'; (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/); expect(() => getAppMode()).toThrowError(/Invalid NEXT_PUBLIC_GRIDPILOT_MODE/);
}); });

View File

@@ -103,7 +103,9 @@ describe('CreateLeaguePage - URL-bound wizard steps', () => {
fireEvent.click(backButton); fireEvent.click(backButton);
expect(routerInstance.push).toHaveBeenCalledTimes(1); 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('/leagues/create');
expect(callArg).toContain('step=structure'); expect(callArg).toContain('step=structure');
}); });

View File

@@ -6,12 +6,12 @@ type RateLimitResult = {
resetAt: number; resetAt: number;
}; };
const mockCheckRateLimit = vi.fn<[], Promise<RateLimitResult>>(); const mockCheckRateLimit = vi.fn(() => Promise.resolve({ allowed: true, remaining: 4, resetAt: 0 }));
const mockGetClientIp = vi.fn<[], string>(); const mockGetClientIp = vi.fn(() => '127.0.0.1');
vi.mock('../../../apps/website/lib/rate-limit', () => ({ vi.mock('../../../apps/website/lib/rate-limit', () => ({
checkRateLimit: (...args: unknown[]) => mockCheckRateLimit(...(args as [])), checkRateLimit: mockCheckRateLimit,
getClientIp: (..._args: unknown[]) => mockGetClientIp(), getClientIp: mockGetClientIp,
})); }));
async function getPostHandler() { async function getPostHandler() {