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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { OverlayAction } from '../../../../packages/automation/application/ports/IOverlaySyncPort'
import { OverlayAction } from '../../../../packages/automation/application/ports/OverlaySyncPort'
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../../packages/automation/infrastructure/adapters/IAutomationLifecycleEmitter'
import { OverlaySyncService } from '../../../../packages/automation/application/services/OverlaySyncService'
@@ -11,7 +11,7 @@ class MockLifecycleEmitter implements IAutomationLifecycleEmitter {
offLifecycle(cb: LifecycleCallback): void {
this.callbacks.delete(cb)
}
async emit(event: { type: string; actionId: string; timestamp: number }) {
async emit(event: { type: 'panel-attached'|'modal-opened'|'action-started'|'action-complete'|'action-failed'|'panel-missing'; actionId: string; timestamp: number }) {
for (const cb of Array.from(this.callbacks)) {
cb(event)
}

View File

@@ -56,7 +56,7 @@ describe('CompleteRaceCreationUseCase', () => {
const state = CheckoutState.ready();
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');

View File

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

View File

@@ -3,7 +3,7 @@ import { Result } from '../../../../packages/shared/result/Result';
import { ConfirmCheckoutUseCase } from '../../../../packages/automation/application/use-cases/ConfirmCheckoutUseCase';
import type { CheckoutServicePort } from '../../../../packages/automation/application/ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '../../../../packages/automation/application/ports/CheckoutConfirmationPort';
import type { CheckoutInfoDTODTO } from '../../../../packages/automation/application/dto/CheckoutInfoDTODTO';
import type { CheckoutInfoDTO } from '../../../../packages/automation/application/dto/CheckoutInfoDTO';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState, CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';

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

View File

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

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { VerifyAuthenticatedPageUseCase } from '../../../../packages/automation/application/use-cases/VerifyAuthenticatedPageUseCase';
import { IAuthenticationService } from '../../../../packages/automation/application/ports/IAuthenticationService';
import { AuthenticationServicePort as IAuthenticationService } from '../../../../packages/automation/application/ports/AuthenticationServicePort';
import { Result } from '../../../../packages/shared/result/Result';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';

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

View File

@@ -75,9 +75,9 @@ describe('ScheduleCalculator', () => {
expect(date.getDay()).toBe(6); // Saturday
});
// First race should be Jan 6
expect(result.raceDates[0].toISOString().split('T')[0]).toBe('2024-01-06');
expect(result.raceDates[0]!.toISOString().split('T')[0]).toBe('2024-01-06');
// Last race should be 7 weeks later (Feb 24)
expect(result.raceDates[7].toISOString().split('T')[0]).toBe('2024-02-24');
expect(result.raceDates[7]!.toISOString().split('T')[0]).toBe('2024-02-24');
});
it('should schedule races on multiple weekdays', () => {
@@ -138,13 +138,13 @@ describe('ScheduleCalculator', () => {
// Then
expect(result.raceDates.length).toBe(4);
// First race Jan 6
expect(result.raceDates[0].toISOString().split('T')[0]).toBe('2024-01-06');
expect(result.raceDates[0]!.toISOString().split('T')[0]).toBe('2024-01-06');
// Second race 2 weeks later (Jan 20)
expect(result.raceDates[1].toISOString().split('T')[0]).toBe('2024-01-20');
expect(result.raceDates[1]!.toISOString().split('T')[0]).toBe('2024-01-20');
// Third race 2 weeks later (Feb 3)
expect(result.raceDates[2].toISOString().split('T')[0]).toBe('2024-02-03');
expect(result.raceDates[2]!.toISOString().split('T')[0]).toBe('2024-02-03');
// Fourth race 2 weeks later (Feb 17)
expect(result.raceDates[3].toISOString().split('T')[0]).toBe('2024-02-17');
expect(result.raceDates[3]!.toISOString().split('T')[0]).toBe('2024-02-17');
});
});
@@ -165,7 +165,7 @@ describe('ScheduleCalculator', () => {
// Then
expect(result.raceDates.length).toBe(8);
// First race should be at or near start
expect(result.raceDates[0].toISOString().split('T')[0]).toBe('2024-01-06');
expect(result.raceDates[0]!.toISOString().split('T')[0]).toBe('2024-01-06');
// Races should be spread across the range, not consecutive weeks
});

View File

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

View File

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

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 { SessionCookieStore } from '../../../../packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore';
import type { IPlaywrightAuthFlow } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthFlow';
import type { ILogger } from '../../../../packages/automation/application/ports/ILogger';
import type { LoggerPort as ILogger } from '../../../../packages/automation/application/ports/LoggerPort';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { Result } from '../../../../packages/shared/result/Result';
@@ -25,6 +25,9 @@ describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', ()
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
fatal: vi.fn(),
child: vi.fn(),
flush: vi.fn().mockResolvedValue(undefined),
};
mockPage = {
@@ -107,7 +110,7 @@ describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', ()
}),
);
const calledUrl = (mockPage.goto as unknown as ReturnType<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');
});

View File

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

View File

@@ -197,7 +197,7 @@ describe('SessionCookieStore - Cookie Validation', () => {
const validCookies = cookieStore.getValidCookiesForUrl(targetUrl);
expect(validCookies).toHaveLength(1);
expect(validCookies[0].name).toBe('valid_cookie');
expect(validCookies[0]!.name).toBe('valid_cookie');
});
test('should filter out cookies with mismatched domains', async () => {
@@ -228,7 +228,7 @@ describe('SessionCookieStore - Cookie Validation', () => {
const validCookies = cookieStore.getValidCookiesForUrl(targetUrl);
expect(validCookies).toHaveLength(1);
expect(validCookies[0].name).toBe('cookie1');
expect(validCookies[0]!.name).toBe('cookie1');
});
test('should filter out cookies with invalid paths', async () => {
@@ -259,7 +259,7 @@ describe('SessionCookieStore - Cookie Validation', () => {
const validCookies = cookieStore.getValidCookiesForUrl(targetUrl);
expect(validCookies).toHaveLength(1);
expect(validCookies[0].name).toBe('valid_path_cookie');
expect(validCookies[0]!.name).toBe('valid_path_cookie');
});
test('should return empty array when no cookies are valid', async () => {

View File

@@ -1,6 +1,13 @@
import { describe, it, expect } from 'vitest';
import { GetDashboardOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetDashboardOverviewUseCase';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import { Result } from '@gridpilot/racing/domain/entities/Result';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
import { LeagueMembership } from '@gridpilot/racing/domain/entities/LeagueMembership';
import type { FeedItem } from '@gridpilot/social/domain/types/FeedItem';
import type {
IDashboardOverviewPresenter,
DashboardOverviewViewModel,
@@ -25,11 +32,17 @@ class FakeDashboardOverviewPresenter implements IDashboardOverviewPresenter {
interface TestImageService {
getDriverAvatar(driverId: string): string;
getTeamLogo(teamId: string): string;
getLeagueCover(leagueId: string): string;
getLeagueLogo(leagueId: string): string;
}
function createTestImageService(): TestImageService {
return {
getDriverAvatar: (driverId: string) => `avatar-${driverId}`,
getTeamLogo: (teamId: string) => `team-logo-${teamId}`,
getLeagueCover: (leagueId: string) => `league-cover-${leagueId}`,
getLeagueLogo: (leagueId: string) => `league-logo-${leagueId}`,
};
}
@@ -38,143 +51,173 @@ describe('GetDashboardOverviewUseCase', () => {
// Given a driver with memberships in two leagues and future races with mixed registration
const driverId = 'driver-1';
const driver = { id: driverId, name: 'Alice Racer', country: 'US' };
const driver = Driver.create({ id: driverId, iracingId: '12345', name: 'Alice Racer', country: 'US' });
const leagues = [
{ id: 'league-1', name: 'Alpha League' },
{ id: 'league-2', name: 'Beta League' },
League.create({ id: 'league-1', name: 'Alpha League', description: 'First league', ownerId: 'owner-1' }),
League.create({ id: 'league-2', name: 'Beta League', description: 'Second league', ownerId: 'owner-2' }),
];
const now = Date.now();
const races = [
{
Race.create({
id: 'race-1',
leagueId: 'league-1',
track: 'Monza',
car: 'GT3',
scheduledAt: new Date(now + 60 * 60 * 1000),
status: 'scheduled' as const,
},
{
status: 'scheduled',
}),
Race.create({
id: 'race-2',
leagueId: 'league-1',
track: 'Spa',
car: 'GT3',
scheduledAt: new Date(now + 2 * 60 * 60 * 1000),
status: 'scheduled' as const,
},
{
status: 'scheduled',
}),
Race.create({
id: 'race-3',
leagueId: 'league-2',
track: 'Silverstone',
car: 'GT4',
scheduledAt: new Date(now + 3 * 60 * 60 * 1000),
status: 'scheduled' as const,
},
{
status: 'scheduled',
}),
Race.create({
id: 'race-4',
leagueId: 'league-2',
track: 'Imola',
car: 'GT4',
scheduledAt: new Date(now + 4 * 60 * 60 * 1000),
status: 'scheduled' as const,
},
status: 'scheduled',
}),
];
const results: unknown[] = [];
const results: Result[] = [];
const memberships = [
{
LeagueMembership.create({
leagueId: 'league-1',
driverId,
role: 'member',
status: 'active',
},
{
}),
LeagueMembership.create({
leagueId: 'league-2',
driverId,
role: 'member',
status: 'active',
},
}),
];
const registeredRaceIds = new Set<string>(['race-1', 'race-3']);
const feedItems: DashboardFeedItemSummaryViewModel[] = [];
const friends: Array<{ id: string }> = [];
const feedItems: FeedItem[] = [];
const friends: Driver[] = [];
const driverRepository: {
findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>;
} = {
findById: async (id: string) => (id === driver.id ? driver : null),
const driverRepository = {
findById: async (id: string): Promise<Driver | null> => (id === driver.id ? driver : null),
findByIRacingId: async (): Promise<Driver | null> => 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<
Array<{
id: string;
leagueId: string;
track: string;
car: string;
scheduledAt: Date;
status: 'scheduled';
}>
>;
} = {
findAll: async () => races,
const raceRepository = {
findById: async (): Promise<Race | null> => null,
findAll: async (): Promise<Race[]> => 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: {
findAll: () => Promise<unknown[]>;
} = {
findAll: async () => results,
const resultRepository = {
findById: async (): Promise<Result | null> => null,
findAll: async (): Promise<Result[]> => 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: {
findAll: () => Promise<Array<{ id: string; name: string }>>;
} = {
findAll: async () => leagues,
const leagueRepository = {
findById: async (): Promise<League | null> => null,
findAll: async (): Promise<League[]> => 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: {
findByLeagueId: (leagueId: string) => Promise<unknown[]>;
} = {
findByLeagueId: async () => [],
const standingRepository = {
findByLeagueId: async (): Promise<Standing[]> => [],
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: {
getMembership: (
leagueId: string,
driverIdParam: string,
) => Promise<{ leagueId: string; driverId: string; status: string } | null>;
} = {
getMembership: async (leagueId: string, driverIdParam: string) => {
const leagueMembershipRepository = {
getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => {
return (
memberships.find(
(m) => m.leagueId === leagueId && m.driverId === driverIdParam,
) ?? 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: {
isRegistered: (raceId: string, driverIdParam: string) => Promise<boolean>;
} = {
isRegistered: async (raceId: string, driverIdParam: string) => {
const raceRegistrationRepository = {
isRegistered: async (raceId: string, driverIdParam: string): Promise<boolean> => {
if (driverIdParam !== driverId) return false;
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: {
getFeedForDriver: (driverIdParam: string) => Promise<DashboardFeedItemSummaryViewModel[]>;
} = {
getFeedForDriver: async () => feedItems,
const feedRepository = {
getFeedForDriver: async (): Promise<FeedItem[]> => feedItems,
getGlobalFeed: async (): Promise<FeedItem[]> => [],
};
const socialRepository: {
getFriends: (driverIdParam: string) => Promise<Array<{ id: string }>>;
} = {
getFriends: async () => friends,
const socialRepository = {
getFriends: async (): Promise<Driver[]> => friends,
getFriendIds: async (): Promise<string[]> => [],
getSuggestedFriends: async (): Promise<Driver[]> => [],
};
const imageService = createTestImageService();
@@ -230,138 +273,181 @@ describe('GetDashboardOverviewUseCase', () => {
// Given completed races with results and standings
const driverId = 'driver-2';
const driver = { id: driverId, name: 'Result Driver', country: 'DE' };
const driver = Driver.create({ id: driverId, iracingId: '67890', name: 'Result Driver', country: 'DE' });
const leagues = [
{ id: 'league-A', name: 'Results League A' },
{ id: 'league-B', name: 'Results League B' },
League.create({ id: 'league-A', name: 'Results League A', description: 'League A', ownerId: 'owner-A' }),
League.create({ id: 'league-B', name: 'Results League B', description: 'League B', ownerId: 'owner-B' }),
];
const raceOld = {
const raceOld = Race.create({
id: 'race-old',
leagueId: 'league-A',
track: 'Old Circuit',
car: 'GT3',
scheduledAt: new Date('2024-01-01T10:00:00Z'),
status: 'completed' as const,
};
status: 'completed',
});
const raceNew = {
const raceNew = Race.create({
id: 'race-new',
leagueId: 'league-B',
track: 'New Circuit',
car: 'GT4',
scheduledAt: new Date('2024-02-01T10:00:00Z'),
status: 'completed' as const,
};
status: 'completed',
});
const races = [raceOld, raceNew];
const results = [
{
Result.create({
id: 'result-old',
raceId: raceOld.id,
driverId,
position: 5,
fastestLap: 120,
incidents: 3,
},
{
startPosition: 5,
}),
Result.create({
id: 'result-new',
raceId: raceNew.id,
driverId,
position: 2,
fastestLap: 115,
incidents: 1,
},
startPosition: 2,
}),
];
const memberships = [
{
LeagueMembership.create({
leagueId: 'league-A',
driverId,
role: 'member',
status: 'active',
},
{
}),
LeagueMembership.create({
leagueId: 'league-B',
driverId,
role: 'member',
status: 'active',
},
}),
];
const standingsByLeague = new Map<
string,
Array<{ leagueId: string; driverId: string; position: number; points: number }>
Standing[]
>();
standingsByLeague.set('league-A', [
{ leagueId: 'league-A', driverId, position: 3, points: 50 },
{ leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 },
Standing.create({ leagueId: 'league-A', driverId, position: 3, points: 50 }),
Standing.create({ leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 }),
]);
standingsByLeague.set('league-B', [
{ leagueId: 'league-B', driverId, position: 1, points: 100 },
{ leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 },
Standing.create({ leagueId: 'league-B', driverId, position: 1, points: 100 }),
Standing.create({ leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 }),
]);
const driverRepository: {
findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>;
} = {
findById: async (id: string) => (id === driver.id ? driver : null),
const driverRepository = {
findById: async (id: string): Promise<Driver | null> => (id === driver.id ? driver : null),
findByIRacingId: async (): Promise<Driver | null> => 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<typeof races>;
} = {
findAll: async () => races,
const raceRepository = {
findById: async (): Promise<Race | null> => null,
findAll: async (): Promise<Race[]> => 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: {
findAll: () => Promise<typeof results>;
} = {
findAll: async () => results,
const resultRepository = {
findById: async (): Promise<Result | null> => null,
findAll: async (): Promise<Result[]> => 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: {
findAll: () => Promise<typeof leagues>;
} = {
findAll: async () => leagues,
const leagueRepository = {
findById: async (): Promise<League | null> => null,
findAll: async (): Promise<League[]> => 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: {
findByLeagueId: (leagueId: string) => Promise<Array<{ leagueId: string; driverId: string; position: number; points: number }>>;
} = {
findByLeagueId: async (leagueId: string) =>
const standingRepository = {
findByLeagueId: async (leagueId: string): Promise<Standing[]> =>
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: {
getMembership: (
leagueId: string,
driverIdParam: string,
) => Promise<{ leagueId: string; driverId: string; status: string } | null>;
} = {
getMembership: async (leagueId: string, driverIdParam: string) => {
const leagueMembershipRepository = {
getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => {
return (
memberships.find(
(m) => m.leagueId === leagueId && m.driverId === driverIdParam,
) ?? 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: {
isRegistered: (raceId: string, driverIdParam: string) => Promise<boolean>;
} = {
isRegistered: async () => false,
const raceRegistrationRepository = {
isRegistered: async (): Promise<boolean> => false,
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: {
getFeedForDriver: (driverIdParam: string) => Promise<DashboardFeedItemSummaryViewModel[]>;
} = {
getFeedForDriver: async () => [],
const feedRepository = {
getFeedForDriver: async (): Promise<FeedItem[]> => [],
getGlobalFeed: async (): Promise<FeedItem[]> => [],
};
const socialRepository: {
getFriends: (driverIdParam: string) => Promise<Array<{ id: string }>>;
} = {
getFriends: async () => [],
const socialRepository = {
getFriends: async (): Promise<Driver[]> => [],
getFriendIds: async (): Promise<string[]> => [],
getSuggestedFriends: async (): Promise<Driver[]> => [],
};
const imageService = createTestImageService();
@@ -430,54 +516,100 @@ describe('GetDashboardOverviewUseCase', () => {
// Given a driver with no related data
const driverId = 'driver-empty';
const driver = { id: driverId, name: 'New Racer', country: 'FR' };
const driver = Driver.create({ id: driverId, iracingId: '11111', name: 'New Racer', country: 'FR' });
const driverRepository: {
findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>;
} = {
findById: async (id: string) => (id === driver.id ? driver : null),
const driverRepository = {
findById: async (id: string): Promise<Driver | null> => (id === driver.id ? driver : null),
findByIRacingId: async (): Promise<Driver | null> => 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[]> } = {
findAll: async () => [],
const raceRepository = {
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[]> } = {
findAll: async () => [],
const resultRepository = {
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[]> } = {
findAll: async () => [],
const leagueRepository = {
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: {
findByLeagueId: (leagueId: string) => Promise<never[]>;
} = {
findByLeagueId: async () => [],
const standingRepository = {
findByLeagueId: async (): Promise<Standing[]> => [],
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: {
getMembership: (leagueId: string, driverIdParam: string) => Promise<null>;
} = {
getMembership: async () => null,
const leagueMembershipRepository = {
getMembership: async (): Promise<LeagueMembership | 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: {
isRegistered: (raceId: string, driverIdParam: string) => Promise<boolean>;
} = {
isRegistered: async () => false,
const raceRegistrationRepository = {
isRegistered: async (): Promise<boolean> => false,
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: {
getFeedForDriver: (driverIdParam: string) => Promise<DashboardFeedItemSummaryViewModel[]>;
} = {
getFeedForDriver: async () => [],
const feedRepository = {
getFeedForDriver: async (): Promise<FeedItem[]> => [],
getGlobalFeed: async (): Promise<FeedItem[]> => [],
};
const socialRepository: {
getFriends: (driverIdParam: string) => Promise<Array<{ id: string }>>;
} = {
getFriends: async () => [],
const socialRepository = {
getFriends: async (): Promise<Driver[]> => [],
getFriendIds: async (): Promise<string[]> => [],
getSuggestedFriends: async (): Promise<Driver[]> => [],
};
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 type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
import type {
import {
LeagueMembership,
MembershipRole,
MembershipStatus,
type MembershipRole,
type MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership';
class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
@@ -109,13 +109,13 @@ describe('Membership use-cases', () => {
const leagueId = 'league-1';
const driverId = 'driver-1';
repository.seedMembership({
repository.seedMembership(LeagueMembership.create({
leagueId,
driverId,
role: 'member',
status: 'active',
joinedAt: new Date('2024-01-01'),
});
}));
await expect(
useCase.execute({ leagueId, driverId }),

View File

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

View File

@@ -9,13 +9,14 @@ import {
LeagueMembership,
type MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership';
import { Team } from '@gridpilot/racing/domain/entities/Team';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type {
Team,
TeamMembership,
TeamMembershipStatus,
TeamRole,
TeamJoinRequest,
} from '@gridpilot/racing/domain/entities/Team';
} from '@gridpilot/racing/domain/types/TeamMembership';
import { RegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/RegisterForRaceUseCase';
import { WithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/WithdrawFromRaceUseCase';
@@ -169,10 +170,23 @@ class TestDriverRegistrationStatusPresenter implements IDriverRegistrationStatus
raceId: string | null = null;
driverId: string | null = null;
present(isRegistered: boolean, raceId: string, driverId: string): void {
present(isRegistered: boolean, raceId: string, driverId: string) {
this.isRegistered = isRegistered;
this.raceId = raceId;
this.driverId = driverId;
return {
isRegistered,
raceId,
driverId,
};
}
getViewModel() {
return {
isRegistered: this.isRegistered!,
raceId: this.raceId!,
driverId: this.driverId!,
};
}
}
@@ -185,9 +199,20 @@ class TestRaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
this.driverIds = [];
}
present(input: RaceRegistrationsResultDTO): void {
present(input: RaceRegistrationsResultDTO) {
this.driverIds = input.registeredDriverIds;
this.raceId = null;
return {
registeredDriverIds: input.registeredDriverIds,
count: input.registeredDriverIds.length,
};
}
getViewModel() {
return {
registeredDriverIds: this.driverIds,
count: this.driverIds.length,
};
}
}
@@ -411,8 +436,43 @@ describe('Racing application use-cases - teams', () => {
let getDriverTeamUseCase: GetDriverTeamUseCase;
class FakeDriverRepository {
async findById(driverId: string): Promise<{ id: string; name: string } | null> {
return { id: driverId, name: `Driver ${driverId}` };
async findById(driverId: string): Promise<Driver | null> {
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 {
return `https://example.com/avatar/${driverId}.png`;
}
getTeamLogo(teamId: string): string {
return `https://example.com/logo/${teamId}.png`;
}
getLeagueCover(leagueId: string): string {
return `https://example.com/cover/${leagueId}.png`;
}
getLeagueLogo(leagueId: string): string {
return `https://example.com/logo/${leagueId}.png`;
}
}
class TestAllTeamsPresenter implements IAllTeamsPresenter {
@@ -438,9 +510,9 @@ describe('Racing application use-cases - teams', () => {
description: team.description,
memberCount: team.memberCount,
leagues: team.leagues,
specialization: team.specialization,
region: team.region,
languages: team.languages,
specialization: (team as any).specialization,
region: (team as any).region,
languages: (team as any).languages,
})),
totalCount: input.teams.length,
};
@@ -487,7 +559,7 @@ describe('Racing application use-cases - teams', () => {
return {
driverId,
driverName,
role: membership.role,
role: ((membership.role as any) === 'owner' ? 'owner' : (membership.role as any) === 'member' ? 'member' : (membership.role as any) === 'manager' ? 'manager' : (membership.role as any) === 'driver' ? 'member' : 'member') as "owner" | "member" | "manager",
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.status === 'active',
avatarUrl,
@@ -496,7 +568,7 @@ describe('Racing application use-cases - teams', () => {
const ownerCount = members.filter((m) => m.role === 'owner').length;
const managerCount = members.filter((m) => m.role === 'manager').length;
const memberCount = members.filter((m) => m.role === 'member').length;
const memberCount = members.filter((m) => (m.role as any) === 'member').length;
this.viewModel = {
members,
@@ -534,7 +606,7 @@ describe('Racing application use-cases - teams', () => {
driverId,
driverName,
teamId: request.teamId,
status: 'pending',
status: 'pending' as const,
requestedAt: request.requestedAt.toISOString(),
avatarUrl,
};
@@ -559,7 +631,7 @@ describe('Racing application use-cases - teams', () => {
}
class TestDriverTeamPresenter implements IDriverTeamPresenter {
private viewModel: DriverTeamViewModel | null = null;
viewModel: DriverTeamViewModel | null = null;
reset(): void {
this.viewModel = null;
@@ -579,12 +651,9 @@ describe('Racing application use-cases - teams', () => {
description: team.description,
ownerId: team.ownerId,
leagues: team.leagues,
specialization: team.specialization,
region: team.region,
languages: team.languages,
},
membership: {
role: membership.role,
role: (membership.role === 'owner' || membership.role === 'manager') ? membership.role : 'member' as "owner" | "member" | "manager",
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.status === 'active',
},
@@ -619,19 +688,17 @@ describe('Racing application use-cases - teams', () => {
getAllTeamsUseCase = new GetAllTeamsUseCase(
teamRepo,
membershipRepo,
allTeamsPresenter,
);
teamDetailsPresenter = new TestTeamDetailsPresenter();
getTeamDetailsUseCase = new GetTeamDetailsUseCase(
teamRepo,
membershipRepo,
teamDetailsPresenter,
);
const driverRepository = new FakeDriverRepository();
const imageService = new FakeImageService();
teamMembersPresenter = new TestTeamMembersPresenter();
getTeamMembersUseCase = new GetTeamMembersUseCase(
membershipRepo,

View File

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

View File

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

View File

@@ -35,7 +35,9 @@ describe('iRacing auth route handlers', () => {
expect(location).toMatch(/state=/);
expect(cookieStore.set).toHaveBeenCalled();
const [name] = cookieStore.set.mock.calls[0];
const call = cookieStore.set.mock.calls[0];
expect(call).toBeDefined();
const [name] = call as [string, string];
expect(name).toBe('gp_demo_auth_state');
});
@@ -58,7 +60,9 @@ describe('iRacing auth route handlers', () => {
expect(location).toBe('http://localhost/dashboard');
expect(cookieStore.set).toHaveBeenCalled();
const [sessionName, sessionValue] = cookieStore.set.mock.calls[0];
const call = cookieStore.set.mock.calls[0];
expect(call).toBeDefined();
const [sessionName, sessionValue] = call as [string, string];
expect(sessionName).toBe('gp_demo_session');
expect(typeof sessionValue).toBe('string');

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

View File

@@ -103,7 +103,9 @@ describe('CreateLeaguePage - URL-bound wizard steps', () => {
fireEvent.click(backButton);
expect(routerInstance.push).toHaveBeenCalledTimes(1);
const callArg = routerInstance.push.mock.calls[0][0] as string;
const call = routerInstance.push.mock.calls[0];
expect(call).toBeDefined();
const callArg = (call as [string])[0];
expect(callArg).toContain('/leagues/create');
expect(callArg).toContain('step=structure');
});

View File

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