refactor
This commit is contained in:
716
core/racing/application/RaceResultsUseCases.test.ts
Normal file
716
core/racing/application/RaceResultsUseCases.test.ts
Normal file
@@ -0,0 +1,716 @@
|
||||
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 { Driver } from '@core/racing/domain/entities/Driver';
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user