716 lines
28 KiB
TypeScript
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');
|
|
});
|
|
}); |