Files
gridpilot.gg/core/racing/application/RaceResultsUseCases.test.ts
2025-12-16 13:53:23 +01:00

716 lines
28 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { Race } from '@core/racing/domain/entities/Race';
import { League } from '@core/racing/domain/entities/League';
import { Result } from '@core/racing/domain/entities/Result';
import { Penalty } from '@core/racing/domain/entities/Penalty';
import { Standing } from '@core/racing/domain/entities/Standing';
import { GetRaceResultsDetailUseCase } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase';
import { ImportRaceResultsUseCase } from '@core/racing/application/use-cases/ImportRaceResultsUseCase';
import type {
IRaceResultsDetailPresenter,
RaceResultsDetailViewModel,
} from '@core/racing/application/presenters/IRaceResultsDetailPresenter';
import type {
IImportRaceResultsPresenter,
ImportRaceResultsSummaryViewModel,
} from '@core/racing/application/presenters/IImportRaceResultsPresenter';
class FakeRaceResultsDetailPresenter implements IRaceResultsDetailPresenter {
viewModel: RaceResultsDetailViewModel | null = null;
reset(): void {
this.viewModel = null;
}
present(viewModel: RaceResultsDetailViewModel): RaceResultsDetailViewModel {
this.viewModel = viewModel;
return viewModel;
}
getViewModel(): RaceResultsDetailViewModel | null {
return this.viewModel;
}
}
class FakeImportRaceResultsPresenter implements IImportRaceResultsPresenter {
viewModel: ImportRaceResultsSummaryViewModel | null = null;
present(viewModel: ImportRaceResultsSummaryViewModel): ImportRaceResultsSummaryViewModel {
this.viewModel = viewModel;
return viewModel;
}
getViewModel(): ImportRaceResultsSummaryViewModel | null {
return this.viewModel;
}
}
describe('ImportRaceResultsUseCase', () => {
it('imports results and triggers standings recalculation for the league', async () => {
// Given a league, a race, empty results, and a standing repository
const league = League.create({
id: 'league-1',
name: 'Import League',
description: 'League for import tests',
ownerId: 'owner-1',
});
const race = Race.create({
id: 'race-1',
leagueId: league.id,
scheduledAt: new Date(),
track: 'Import Circuit',
car: 'GT3',
sessionType: 'race',
status: 'completed',
});
const races = new Map<string, typeof race>();
races.set(race.id, race);
const leagues = new Map<string, typeof league>();
leagues.set(league.id, league);
const storedResults: Result[] = [];
let existsByRaceIdCalled = false;
const recalcCalls: string[] = [];
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: 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 = {
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 = {
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 [];
},
};
const presenter = new FakeImportRaceResultsPresenter();
const useCase = new ImportRaceResultsUseCase(
raceRepository,
leagueRepository,
resultRepository,
standingRepository,
presenter,
);
const importedResults = [
Result.create({
id: 'result-1',
raceId: race.id,
driverId: 'driver-1',
position: 1,
fastestLap: 90.123,
incidents: 0,
startPosition: 3,
}),
Result.create({
id: 'result-2',
raceId: race.id,
driverId: 'driver-2',
position: 2,
fastestLap: 91.456,
incidents: 2,
startPosition: 1,
}),
];
// When executing the import
await useCase.execute({
raceId: race.id,
results: importedResults,
});
// Then new Result entries are persisted
expect(existsByRaceIdCalled).toBe(true);
expect(storedResults.length).toBe(2);
expect(storedResults.map((r) => r.id)).toEqual(['result-1', 'result-2']);
// And standings are recalculated exactly once for the correct league
expect(recalcCalls).toEqual([league.id]);
// And the presenter receives a summary
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
expect(viewModel!.importedCount).toBe(2);
expect(viewModel!.standingsRecalculated).toBe(true);
});
it('rejects import when results already exist for the race', async () => {
const league = League.create({
id: 'league-2',
name: 'Existing Results League',
description: 'League with existing results',
ownerId: 'owner-2',
});
const race = Race.create({
id: 'race-2',
leagueId: league.id,
scheduledAt: new Date(),
track: 'Existing Circuit',
car: 'GT4',
sessionType: 'race',
status: 'completed',
});
const races = new Map<string, typeof race>([[race.id, race]]);
const leagues = new Map<string, typeof league>([[league.id, league]]);
const storedResults: Result[] = [
Result.create({
id: 'existing',
raceId: race.id,
driverId: 'driver-x',
position: 1,
fastestLap: 90.0,
incidents: 1,
startPosition: 1,
}),
];
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: 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 = {
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 = {
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');
},
};
const presenter = new FakeImportRaceResultsPresenter();
const driverRepository = {
findById: async (): Promise<Driver | null> => null,
findByIRacingId: async (iracingId: string): Promise<Driver | null> => {
// Mock finding driver by iracingId
if (iracingId === 'driver-1') {
return Driver.create({ id: 'driver-1', iracingId: 'driver-1', name: 'Driver One', country: 'US' });
}
if (iracingId === 'driver-2') {
return Driver.create({ id: 'driver-2', iracingId: 'driver-2', name: 'Driver Two', country: 'GB' });
}
return 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 useCase = new ImportRaceResultsUseCase(
raceRepository,
leagueRepository,
resultRepository,
driverRepository,
standingRepository,
presenter,
);
const importedResults = [
Result.create({
id: 'new-result',
raceId: race.id,
driverId: 'driver-1',
position: 2,
fastestLap: 91.0,
incidents: 0,
startPosition: 2,
}),
];
await expect(
useCase.execute({
raceId: race.id,
results: importedResults,
}),
).rejects.toThrow('Results already exist for this race');
});
});
describe('GetRaceResultsDetailUseCase', () => {
it('computes points system from league settings and identifies fastest lap', async () => {
// Given a league with default scoring configuration and two results
const league = League.create({
id: 'league-scoring',
name: 'Scoring League',
description: 'League with scoring settings',
ownerId: 'owner-scoring',
});
const race = Race.create({
id: 'race-scoring',
leagueId: league.id,
scheduledAt: new Date(),
track: 'Scoring Circuit',
car: 'Prototype',
sessionType: 'race',
status: 'completed',
});
const driver1: { id: string; name: string; country: string } = {
id: 'driver-a',
name: 'Driver A',
country: 'US',
};
const driver2: { id: string; name: string; country: string } = {
id: 'driver-b',
name: 'Driver B',
country: 'GB',
};
const result1 = Result.create({
id: 'r1',
raceId: race.id,
driverId: driver1.id,
position: 1,
fastestLap: 90.123,
incidents: 0,
startPosition: 3,
});
const result2 = Result.create({
id: 'r2',
raceId: race.id,
driverId: driver2.id,
position: 2,
fastestLap: 88.456,
incidents: 2,
startPosition: 1,
});
const races = new Map<string, typeof race>([[race.id, race]]);
const leagues = new Map<string, typeof league>([[league.id, league]]);
const results = [result1, result2];
const drivers = [driver1, driver2];
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: 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 = {
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 = {
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 = {
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();
const useCase = new GetRaceResultsDetailUseCase(
raceRepository,
leagueRepository,
resultRepository,
driverRepository,
penaltyRepository,
);
// When executing the query
await useCase.execute({ raceId: race.id }, presenter);
const viewModel = presenter.getViewModel();
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);
// And fastest lap is identified correctly
expect(viewModel!.fastestLapTime).toBeCloseTo(88.456, 3);
});
it('builds race results view model including penalties', async () => {
// Given a race with one result and one applied penalty
const league = League.create({
id: 'league-penalties',
name: 'Penalty League',
description: 'League with penalties',
ownerId: 'owner-penalties',
});
const race = Race.create({
id: 'race-penalties',
leagueId: league.id,
scheduledAt: new Date(),
track: 'Penalty Circuit',
car: 'Touring',
sessionType: 'race',
status: 'completed',
});
const driver: { id: string; name: string; country: string } = {
id: 'driver-pen',
name: 'Penalty Driver',
country: 'DE',
};
const result = Result.create({
id: 'res-pen',
raceId: race.id,
driverId: driver.id,
position: 3,
fastestLap: 95.0,
incidents: 4,
startPosition: 5,
});
const penalty = Penalty.create({
id: 'pen-1',
leagueId: league.id,
raceId: race.id,
driverId: driver.id,
type: 'points_deduction',
value: 3,
reason: 'Track limits',
issuedBy: 'steward-1',
status: 'applied',
issuedAt: new Date(),
});
const races = new Map<string, typeof race>([[race.id, race]]);
const leagues = new Map<string, typeof league>([[league.id, league]]);
const results = [result];
const drivers = [driver];
const penalties = [penalty];
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: 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 = {
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 = {
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 = {
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();
const useCase = new GetRaceResultsDetailUseCase(
raceRepository,
leagueRepository,
resultRepository,
driverRepository,
penaltyRepository,
);
// When
await useCase.execute({ raceId: race.id }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
// Then header and league info are present
expect(viewModel!.race).not.toBeNull();
expect(viewModel!.race!.id).toBe(race.id);
expect(viewModel!.league).not.toBeNull();
expect(viewModel!.league!.id).toBe(league.id);
// And classification and penalties match the underlying data
expect(viewModel!.results.length).toBe(1);
expect(viewModel!.results[0]!.id).toBe(result.id);
expect(viewModel!.penalties.length).toBe(1);
expect(viewModel!.penalties[0]!.driverId).toBe(driver.id);
expect(viewModel!.penalties[0]!.type).toBe('points_deduction');
expect(viewModel!.penalties[0]!.value).toBe(3);
});
it('presents an error when race does not exist', async () => {
// Given repositories without the requested race
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: 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 = {
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 = {
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 = {
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();
const useCase = new GetRaceResultsDetailUseCase(
raceRepository,
leagueRepository,
resultRepository,
driverRepository,
penaltyRepository,
);
// When
await useCase.execute({ raceId: 'missing-race' }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
expect(viewModel!.race).toBeNull();
expect(viewModel!.error).toBe('Race not found');
});
});