wip
This commit is contained in:
450
tests/unit/racing-application/DashboardOverviewUseCase.test.ts
Normal file
450
tests/unit/racing-application/DashboardOverviewUseCase.test.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { GetDashboardOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetDashboardOverviewUseCase';
|
||||
import type {
|
||||
IDashboardOverviewPresenter,
|
||||
DashboardOverviewViewModel,
|
||||
DashboardFeedItemSummaryViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IDashboardOverviewPresenter';
|
||||
|
||||
class FakeDashboardOverviewPresenter implements IDashboardOverviewPresenter {
|
||||
viewModel: DashboardOverviewViewModel | null = null;
|
||||
|
||||
present(viewModel: DashboardOverviewViewModel): void {
|
||||
this.viewModel = viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): DashboardOverviewViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
function createTestImageService() {
|
||||
return {
|
||||
getDriverAvatar: (driverId: string) => `avatar-${driverId}`,
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('GetDashboardOverviewUseCase', () => {
|
||||
it('partitions upcoming races into myUpcomingRaces and otherUpcomingRaces and selects nextRace from myUpcomingRaces', async () => {
|
||||
// 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 leagues = [
|
||||
{ id: 'league-1', name: 'Alpha League' },
|
||||
{ id: 'league-2', name: 'Beta League' },
|
||||
];
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const races = [
|
||||
{
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
track: 'Monza',
|
||||
car: 'GT3',
|
||||
scheduledAt: new Date(now + 60 * 60 * 1000),
|
||||
status: 'scheduled' as const,
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
leagueId: 'league-1',
|
||||
track: 'Spa',
|
||||
car: 'GT3',
|
||||
scheduledAt: new Date(now + 2 * 60 * 60 * 1000),
|
||||
status: 'scheduled' as const,
|
||||
},
|
||||
{
|
||||
id: 'race-3',
|
||||
leagueId: 'league-2',
|
||||
track: 'Silverstone',
|
||||
car: 'GT4',
|
||||
scheduledAt: new Date(now + 3 * 60 * 60 * 1000),
|
||||
status: 'scheduled' as const,
|
||||
},
|
||||
{
|
||||
id: 'race-4',
|
||||
leagueId: 'league-2',
|
||||
track: 'Imola',
|
||||
car: 'GT4',
|
||||
scheduledAt: new Date(now + 4 * 60 * 60 * 1000),
|
||||
status: 'scheduled' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const results: any[] = [];
|
||||
|
||||
const memberships = [
|
||||
{
|
||||
leagueId: 'league-1',
|
||||
driverId,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
leagueId: 'league-2',
|
||||
driverId,
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
const registeredRaceIds = new Set<string>(['race-1', 'race-3']);
|
||||
|
||||
const feedItems: DashboardFeedItemSummaryViewModel[] = [];
|
||||
const friends: any[] = [];
|
||||
|
||||
const driverRepository = {
|
||||
findById: async (id: string) => (id === driver.id ? driver : null),
|
||||
} as any;
|
||||
|
||||
const raceRepository = {
|
||||
findAll: async () => races,
|
||||
} as any;
|
||||
|
||||
const resultRepository = {
|
||||
findAll: async () => results,
|
||||
} as any;
|
||||
|
||||
const leagueRepository = {
|
||||
findAll: async () => leagues,
|
||||
} as any;
|
||||
|
||||
const standingRepository = {
|
||||
findByLeagueId: async () => [],
|
||||
} as any;
|
||||
|
||||
const leagueMembershipRepository = {
|
||||
getMembership: async (leagueId: string, driverIdParam: string) => {
|
||||
return (
|
||||
memberships.find(
|
||||
(m) => m.leagueId === leagueId && m.driverId === driverIdParam,
|
||||
) ?? null
|
||||
);
|
||||
},
|
||||
} as any;
|
||||
|
||||
const raceRegistrationRepository = {
|
||||
isRegistered: async (raceId: string, driverIdParam: string) => {
|
||||
if (driverIdParam !== driverId) return false;
|
||||
return registeredRaceIds.has(raceId);
|
||||
},
|
||||
} as any;
|
||||
|
||||
const feedRepository = {
|
||||
getFeedForDriver: async () => feedItems,
|
||||
} as any;
|
||||
|
||||
const socialRepository = {
|
||||
getFriends: async () => friends,
|
||||
} as any;
|
||||
|
||||
const imageService = createTestImageService();
|
||||
|
||||
const getDriverStats = (id: string) =>
|
||||
id === driverId
|
||||
? {
|
||||
rating: 1600,
|
||||
wins: 5,
|
||||
podiums: 12,
|
||||
totalRaces: 40,
|
||||
overallRank: 42,
|
||||
consistency: 88,
|
||||
}
|
||||
: null;
|
||||
|
||||
const presenter = new FakeDashboardOverviewPresenter();
|
||||
|
||||
const useCase = new GetDashboardOverviewUseCase(
|
||||
driverRepository,
|
||||
raceRepository,
|
||||
resultRepository,
|
||||
leagueRepository,
|
||||
standingRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRegistrationRepository,
|
||||
feedRepository,
|
||||
socialRepository,
|
||||
imageService,
|
||||
getDriverStats,
|
||||
presenter,
|
||||
);
|
||||
|
||||
// When
|
||||
await useCase.execute({ driverId });
|
||||
|
||||
const viewModel = presenter.getViewModel();
|
||||
expect(viewModel).not.toBeNull();
|
||||
|
||||
const vm = viewModel!;
|
||||
|
||||
// Then myUpcomingRaces only contains registered races from the driver's leagues
|
||||
expect(vm.myUpcomingRaces.map((r) => r.id)).toEqual(['race-1', 'race-3']);
|
||||
|
||||
// And otherUpcomingRaces contains the other upcoming races in those leagues
|
||||
expect(vm.otherUpcomingRaces.map((r) => r.id)).toEqual(['race-2', 'race-4']);
|
||||
|
||||
// And nextRace is the earliest upcoming race from myUpcomingRaces
|
||||
expect(vm.nextRace).not.toBeNull();
|
||||
expect(vm.nextRace!.id).toBe('race-1');
|
||||
});
|
||||
|
||||
it('builds recentResults sorted by date descending and leagueStandingsSummaries from standings', async () => {
|
||||
// Given completed races with results and standings
|
||||
const driverId = 'driver-2';
|
||||
|
||||
const driver = { id: driverId, name: 'Result Driver', country: 'DE' };
|
||||
|
||||
const leagues = [
|
||||
{ id: 'league-A', name: 'Results League A' },
|
||||
{ id: 'league-B', name: 'Results League B' },
|
||||
];
|
||||
|
||||
const raceOld = {
|
||||
id: 'race-old',
|
||||
leagueId: 'league-A',
|
||||
track: 'Old Circuit',
|
||||
car: 'GT3',
|
||||
scheduledAt: new Date('2024-01-01T10:00:00Z'),
|
||||
status: 'completed' as const,
|
||||
};
|
||||
|
||||
const raceNew = {
|
||||
id: 'race-new',
|
||||
leagueId: 'league-B',
|
||||
track: 'New Circuit',
|
||||
car: 'GT4',
|
||||
scheduledAt: new Date('2024-02-01T10:00:00Z'),
|
||||
status: 'completed' as const,
|
||||
};
|
||||
|
||||
const races = [raceOld, raceNew];
|
||||
|
||||
const results = [
|
||||
{
|
||||
id: 'result-old',
|
||||
raceId: raceOld.id,
|
||||
driverId,
|
||||
position: 5,
|
||||
incidents: 3,
|
||||
},
|
||||
{
|
||||
id: 'result-new',
|
||||
raceId: raceNew.id,
|
||||
driverId,
|
||||
position: 2,
|
||||
incidents: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const memberships = [
|
||||
{
|
||||
leagueId: 'league-A',
|
||||
driverId,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
leagueId: 'league-B',
|
||||
driverId,
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
const standingsByLeague = new Map<string, any[]>();
|
||||
standingsByLeague.set('league-A', [
|
||||
{ leagueId: 'league-A', driverId, position: 3, points: 50 },
|
||||
{ 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 },
|
||||
]);
|
||||
|
||||
const driverRepository = {
|
||||
findById: async (id: string) => (id === driver.id ? driver : null),
|
||||
} as any;
|
||||
|
||||
const raceRepository = {
|
||||
findAll: async () => races,
|
||||
} as any;
|
||||
|
||||
const resultRepository = {
|
||||
findAll: async () => results,
|
||||
} as any;
|
||||
|
||||
const leagueRepository = {
|
||||
findAll: async () => leagues,
|
||||
} as any;
|
||||
|
||||
const standingRepository = {
|
||||
findByLeagueId: async (leagueId: string) =>
|
||||
standingsByLeague.get(leagueId) ?? [],
|
||||
} as any;
|
||||
|
||||
const leagueMembershipRepository = {
|
||||
getMembership: async (leagueId: string, driverIdParam: string) => {
|
||||
return (
|
||||
memberships.find(
|
||||
(m) => m.leagueId === leagueId && m.driverId === driverIdParam,
|
||||
) ?? null
|
||||
);
|
||||
},
|
||||
} as any;
|
||||
|
||||
const raceRegistrationRepository = {
|
||||
isRegistered: async () => false,
|
||||
} as any;
|
||||
|
||||
const feedRepository = {
|
||||
getFeedForDriver: async () => [],
|
||||
} as any;
|
||||
|
||||
const socialRepository = {
|
||||
getFriends: async () => [],
|
||||
} as any;
|
||||
|
||||
const imageService = createTestImageService();
|
||||
|
||||
const getDriverStats = (id: string) =>
|
||||
id === driverId
|
||||
? {
|
||||
rating: 1800,
|
||||
wins: 3,
|
||||
podiums: 7,
|
||||
totalRaces: 20,
|
||||
overallRank: 10,
|
||||
consistency: 92,
|
||||
}
|
||||
: null;
|
||||
|
||||
const presenter = new FakeDashboardOverviewPresenter();
|
||||
|
||||
const useCase = new GetDashboardOverviewUseCase(
|
||||
driverRepository,
|
||||
raceRepository,
|
||||
resultRepository,
|
||||
leagueRepository,
|
||||
standingRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRegistrationRepository,
|
||||
feedRepository,
|
||||
socialRepository,
|
||||
imageService,
|
||||
getDriverStats,
|
||||
presenter,
|
||||
);
|
||||
|
||||
// When
|
||||
await useCase.execute({ driverId });
|
||||
|
||||
const viewModel = presenter.getViewModel();
|
||||
expect(viewModel).not.toBeNull();
|
||||
|
||||
const vm = viewModel!;
|
||||
|
||||
// Then recentResults are sorted by finishedAt descending (newest first)
|
||||
expect(vm.recentResults.length).toBe(2);
|
||||
expect(vm.recentResults[0]!.raceId).toBe('race-new');
|
||||
expect(vm.recentResults[1]!.raceId).toBe('race-old');
|
||||
|
||||
// And leagueStandingsSummaries reflect the driver's position and points per league
|
||||
const summariesByLeague = new Map(
|
||||
vm.leagueStandingsSummaries.map((s) => [s.leagueId, s]),
|
||||
);
|
||||
|
||||
const summaryA = summariesByLeague.get('league-A');
|
||||
const summaryB = summariesByLeague.get('league-B');
|
||||
|
||||
expect(summaryA).toBeDefined();
|
||||
expect(summaryA!.position).toBe(3);
|
||||
expect(summaryA!.points).toBe(50);
|
||||
expect(summaryA!.totalDrivers).toBe(2);
|
||||
|
||||
expect(summaryB).toBeDefined();
|
||||
expect(summaryB!.position).toBe(1);
|
||||
expect(summaryB!.points).toBe(100);
|
||||
expect(summaryB!.totalDrivers).toBe(2);
|
||||
});
|
||||
|
||||
it('returns empty collections and safe defaults when driver has no races or standings', async () => {
|
||||
// Given a driver with no related data
|
||||
const driverId = 'driver-empty';
|
||||
|
||||
const driver = { id: driverId, name: 'New Racer', country: 'FR' };
|
||||
|
||||
const driverRepository = {
|
||||
findById: async (id: string) => (id === driver.id ? driver : null),
|
||||
} as any;
|
||||
|
||||
const raceRepository = {
|
||||
findAll: async () => [],
|
||||
} as any;
|
||||
|
||||
const resultRepository = {
|
||||
findAll: async () => [],
|
||||
} as any;
|
||||
|
||||
const leagueRepository = {
|
||||
findAll: async () => [],
|
||||
} as any;
|
||||
|
||||
const standingRepository = {
|
||||
findByLeagueId: async () => [],
|
||||
} as any;
|
||||
|
||||
const leagueMembershipRepository = {
|
||||
getMembership: async () => null,
|
||||
} as any;
|
||||
|
||||
const raceRegistrationRepository = {
|
||||
isRegistered: async () => false,
|
||||
} as any;
|
||||
|
||||
const feedRepository = {
|
||||
getFeedForDriver: async () => [],
|
||||
} as any;
|
||||
|
||||
const socialRepository = {
|
||||
getFriends: async () => [],
|
||||
} as any;
|
||||
|
||||
const imageService = createTestImageService();
|
||||
|
||||
const getDriverStats = () => null;
|
||||
|
||||
const presenter = new FakeDashboardOverviewPresenter();
|
||||
|
||||
const useCase = new GetDashboardOverviewUseCase(
|
||||
driverRepository,
|
||||
raceRepository,
|
||||
resultRepository,
|
||||
leagueRepository,
|
||||
standingRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRegistrationRepository,
|
||||
feedRepository,
|
||||
socialRepository,
|
||||
imageService,
|
||||
getDriverStats,
|
||||
presenter,
|
||||
);
|
||||
|
||||
// When
|
||||
await useCase.execute({ driverId });
|
||||
|
||||
const viewModel = presenter.getViewModel();
|
||||
expect(viewModel).not.toBeNull();
|
||||
|
||||
const vm = viewModel!;
|
||||
|
||||
// Then collections are empty and no errors are thrown
|
||||
expect(vm.myUpcomingRaces).toEqual([]);
|
||||
expect(vm.otherUpcomingRaces).toEqual([]);
|
||||
expect(vm.nextRace).toBeNull();
|
||||
expect(vm.recentResults).toEqual([]);
|
||||
expect(vm.leagueStandingsSummaries).toEqual([]);
|
||||
expect(vm.friends).toEqual([]);
|
||||
expect(vm.feedSummary.notificationCount).toBe(0);
|
||||
expect(vm.feedSummary.items).toEqual([]);
|
||||
});
|
||||
});
|
||||
618
tests/unit/racing-application/RaceDetailUseCases.test.ts
Normal file
618
tests/unit/racing-application/RaceDetailUseCases.test.ts
Normal file
@@ -0,0 +1,618 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
|
||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { LeagueMembership } from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
import type { DriverRatingProvider } from '@gridpilot/racing/application/ports/DriverRatingProvider';
|
||||
import type { IImageService } from '@gridpilot/racing/domain/services/IImageService';
|
||||
import type {
|
||||
IRaceDetailPresenter,
|
||||
RaceDetailViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRaceDetailPresenter';
|
||||
|
||||
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 { GetRaceDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceDetailUseCase';
|
||||
import { CancelRaceUseCase } from '@gridpilot/racing/application/use-cases/CancelRaceUseCase';
|
||||
|
||||
class InMemoryRaceRepository implements IRaceRepository {
|
||||
private races = new Map<string, Race>();
|
||||
|
||||
constructor(races: Race[]) {
|
||||
for (const race of races) {
|
||||
this.races.set(race.id, race);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Race | null> {
|
||||
return this.races.get(id) ?? null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Race[]> {
|
||||
return [...this.races.values()];
|
||||
}
|
||||
|
||||
async findByLeagueId(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findUpcomingByLeagueId(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findCompletedByLeagueId(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByStatus(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByDateRange(): Promise<Race[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async create(race: Race): Promise<Race> {
|
||||
this.races.set(race.id, race);
|
||||
return race;
|
||||
}
|
||||
|
||||
async update(race: Race): Promise<Race> {
|
||||
this.races.set(race.id, race);
|
||||
return race;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.races.delete(id);
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
return this.races.has(id);
|
||||
}
|
||||
|
||||
getStored(id: string): Race | null {
|
||||
return this.races.get(id) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryLeagueRepository implements ILeagueRepository {
|
||||
private leagues = new Map<string, League>();
|
||||
|
||||
constructor(leagues: League[]) {
|
||||
for (const league of leagues) {
|
||||
this.leagues.set(league.id, league);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<League | null> {
|
||||
return this.leagues.get(id) ?? null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<League[]> {
|
||||
return [...this.leagues.values()];
|
||||
}
|
||||
|
||||
async findByOwnerId(): Promise<League[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async create(league: League): Promise<League> {
|
||||
this.leagues.set(league.id, league);
|
||||
return league;
|
||||
}
|
||||
|
||||
async update(league: League): Promise<League> {
|
||||
this.leagues.set(league.id, league);
|
||||
return league;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.leagues.delete(id);
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
return this.leagues.has(id);
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryDriverRepository implements IDriverRepository {
|
||||
private drivers = new Map<string, any>();
|
||||
|
||||
constructor(drivers: Array<{ id: string; name: string; country: string }>) {
|
||||
for (const driver of drivers) {
|
||||
this.drivers.set(driver.id, {
|
||||
...driver,
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<any | null> {
|
||||
return this.drivers.get(id) ?? null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<any[]> {
|
||||
return [...this.drivers.values()];
|
||||
}
|
||||
|
||||
async findByIds(ids: string[]): Promise<any[]> {
|
||||
return ids
|
||||
.map(id => this.drivers.get(id))
|
||||
.filter((d): d is any => !!d);
|
||||
}
|
||||
|
||||
async create(): Promise<any> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async update(): Promise<any> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async exists(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository {
|
||||
private registrations = new Map<string, Set<string>>();
|
||||
|
||||
constructor(seed: Array<{ raceId: string; driverId: string }> = []) {
|
||||
for (const { raceId, driverId } of seed) {
|
||||
if (!this.registrations.has(raceId)) {
|
||||
this.registrations.set(raceId, new Set());
|
||||
}
|
||||
this.registrations.get(raceId)!.add(driverId);
|
||||
}
|
||||
}
|
||||
|
||||
async isRegistered(raceId: string, driverId: string): Promise<boolean> {
|
||||
return this.registrations.get(raceId)?.has(driverId) ?? false;
|
||||
}
|
||||
|
||||
async getRegisteredDrivers(raceId: string): Promise<string[]> {
|
||||
return Array.from(this.registrations.get(raceId) ?? []);
|
||||
}
|
||||
|
||||
async getRegistrationCount(raceId: string): Promise<number> {
|
||||
return this.registrations.get(raceId)?.size ?? 0;
|
||||
}
|
||||
|
||||
async register(registration: { raceId: string; driverId: string }): Promise<void> {
|
||||
if (!this.registrations.has(registration.raceId)) {
|
||||
this.registrations.set(registration.raceId, new Set());
|
||||
}
|
||||
this.registrations.get(registration.raceId)!.add(registration.driverId);
|
||||
}
|
||||
|
||||
async withdraw(raceId: string, driverId: string): Promise<void> {
|
||||
this.registrations.get(raceId)?.delete(driverId);
|
||||
}
|
||||
|
||||
async getDriverRegistrations(): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async clearRaceRegistrations(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryResultRepository implements IResultRepository {
|
||||
private results = new Map<string, Result[]>();
|
||||
|
||||
constructor(results: Result[]) {
|
||||
for (const result of results) {
|
||||
const list = this.results.get(result.raceId) ?? [];
|
||||
list.push(result);
|
||||
this.results.set(result.raceId, list);
|
||||
}
|
||||
}
|
||||
|
||||
async findByRaceId(raceId: string): Promise<Result[]> {
|
||||
return this.results.get(raceId) ?? [];
|
||||
}
|
||||
|
||||
async findById(): Promise<Result | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Result[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByDriverId(): Promise<Result[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByDriverIdAndLeagueId(): Promise<Result[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async create(result: Result): Promise<Result> {
|
||||
const list = this.results.get(result.raceId) ?? [];
|
||||
list.push(result);
|
||||
this.results.set(result.raceId, list);
|
||||
return result;
|
||||
}
|
||||
|
||||
async createMany(results: Result[]): Promise<Result[]> {
|
||||
for (const result of results) {
|
||||
await this.create(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async update(): Promise<Result> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async deleteByRaceId(): Promise<void> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async exists(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async existsByRaceId(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
|
||||
private memberships: LeagueMembership[] = [];
|
||||
|
||||
seedMembership(membership: LeagueMembership): void {
|
||||
this.memberships.push(membership);
|
||||
}
|
||||
|
||||
async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||
return (
|
||||
this.memberships.find(
|
||||
m => m.leagueId === leagueId && m.driverId === driverId,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
async getLeagueMembers(): Promise<LeagueMembership[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getJoinRequests(): Promise<never> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async saveMembership(membership: LeagueMembership): Promise<LeagueMembership> {
|
||||
this.memberships.push(membership);
|
||||
return membership;
|
||||
}
|
||||
|
||||
async removeMembership(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async saveJoinRequest(): Promise<never> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
|
||||
async removeJoinRequest(): Promise<never> {
|
||||
throw new Error('Not needed for these tests');
|
||||
}
|
||||
}
|
||||
|
||||
class TestDriverRatingProvider implements DriverRatingProvider {
|
||||
private ratings = new Map<string, number>();
|
||||
|
||||
seed(driverId: string, rating: number): void {
|
||||
this.ratings.set(driverId, rating);
|
||||
}
|
||||
|
||||
getRating(driverId: string): number | null {
|
||||
return this.ratings.get(driverId) ?? null;
|
||||
}
|
||||
|
||||
getRatings(driverIds: string[]): Map<string, number> {
|
||||
const map = new Map<string, number>();
|
||||
for (const id of driverIds) {
|
||||
const rating = this.ratings.get(id);
|
||||
if (rating != null) {
|
||||
map.set(id, rating);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class TestImageService implements IImageService {
|
||||
getDriverAvatar(driverId: string): string {
|
||||
return `avatar-${driverId}`;
|
||||
}
|
||||
|
||||
getTeamLogo(teamId: string): string {
|
||||
return `team-logo-${teamId}`;
|
||||
}
|
||||
|
||||
getLeagueCover(leagueId: string): string {
|
||||
return `league-cover-${leagueId}`;
|
||||
}
|
||||
|
||||
getLeagueLogo(leagueId: string): string {
|
||||
return `league-logo-${leagueId}`;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeRaceDetailPresenter implements IRaceDetailPresenter {
|
||||
viewModel: RaceDetailViewModel | null = null;
|
||||
|
||||
present(viewModel: RaceDetailViewModel): RaceDetailViewModel {
|
||||
this.viewModel = viewModel;
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): RaceDetailViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
describe('GetRaceDetailUseCase', () => {
|
||||
it('builds entry list and registration flags for an upcoming race', async () => {
|
||||
// Given (arrange a scheduled race with one registered driver)
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'League for testing',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
|
||||
const race = Race.create({
|
||||
id: 'race-1',
|
||||
leagueId: league.id,
|
||||
scheduledAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||
track: 'Test Track',
|
||||
car: 'GT3',
|
||||
sessionType: 'race',
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
const driverId = 'driver-1';
|
||||
const otherDriverId = 'driver-2';
|
||||
|
||||
const raceRepo = new InMemoryRaceRepository([race]);
|
||||
const leagueRepo = new InMemoryLeagueRepository([league]);
|
||||
const driverRepo = new InMemoryDriverRepository([
|
||||
{ id: driverId, name: 'Alice Racer', country: 'US' },
|
||||
{ id: otherDriverId, name: 'Bob Driver', country: 'GB' },
|
||||
]);
|
||||
|
||||
const registrationRepo = new InMemoryRaceRegistrationRepository([
|
||||
{ raceId: race.id, driverId },
|
||||
{ raceId: race.id, driverId: otherDriverId },
|
||||
]);
|
||||
|
||||
const resultRepo = new InMemoryResultRepository([]);
|
||||
|
||||
const membershipRepo = new InMemoryLeagueMembershipRepository();
|
||||
membershipRepo.seedMembership({
|
||||
leagueId: league.id,
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-01-01'),
|
||||
});
|
||||
|
||||
const ratingProvider = new TestDriverRatingProvider();
|
||||
ratingProvider.seed(driverId, 1500);
|
||||
ratingProvider.seed(otherDriverId, 1600);
|
||||
|
||||
const imageService = new TestImageService();
|
||||
const presenter = new FakeRaceDetailPresenter();
|
||||
|
||||
const useCase = new GetRaceDetailUseCase(
|
||||
raceRepo,
|
||||
leagueRepo,
|
||||
driverRepo,
|
||||
registrationRepo,
|
||||
resultRepo,
|
||||
membershipRepo,
|
||||
ratingProvider,
|
||||
imageService,
|
||||
presenter,
|
||||
);
|
||||
|
||||
// When (execute the query for the current driver)
|
||||
await useCase.execute({ raceId: race.id, driverId });
|
||||
|
||||
const viewModel = presenter.getViewModel();
|
||||
expect(viewModel).not.toBeNull();
|
||||
|
||||
// Then (verify race, league and registration flags)
|
||||
expect(viewModel!.race?.id).toBe(race.id);
|
||||
expect(viewModel!.league?.id).toBe(league.id);
|
||||
expect(viewModel!.registration.isUserRegistered).toBe(true);
|
||||
expect(viewModel!.registration.canRegister).toBe(true);
|
||||
|
||||
// Then (entry list contains both drivers with rating and avatar)
|
||||
expect(viewModel!.entryList.length).toBe(2);
|
||||
const currentDriver = viewModel!.entryList.find(e => e.id === driverId);
|
||||
const otherDriver = viewModel!.entryList.find(e => e.id === otherDriverId);
|
||||
|
||||
expect(currentDriver).toBeDefined();
|
||||
expect(currentDriver!.isCurrentUser).toBe(true);
|
||||
expect(currentDriver!.rating).toBe(1500);
|
||||
expect(currentDriver!.avatarUrl).toBe(`avatar-${driverId}`);
|
||||
|
||||
expect(otherDriver).toBeDefined();
|
||||
expect(otherDriver!.isCurrentUser).toBe(false);
|
||||
expect(otherDriver!.rating).toBe(1600);
|
||||
});
|
||||
|
||||
it('computes rating change for a completed race result using legacy formula', async () => {
|
||||
// Given (a completed race with a result for the current driver)
|
||||
const league = League.create({
|
||||
id: 'league-2',
|
||||
name: 'Results League',
|
||||
description: 'League with results',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
|
||||
const race = Race.create({
|
||||
id: 'race-2',
|
||||
leagueId: league.id,
|
||||
scheduledAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
||||
track: 'Historic Circuit',
|
||||
car: 'LMP2',
|
||||
sessionType: 'race',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
const driverId = 'driver-results';
|
||||
|
||||
const raceRepo = new InMemoryRaceRepository([race]);
|
||||
const leagueRepo = new InMemoryLeagueRepository([league]);
|
||||
const driverRepo = new InMemoryDriverRepository([
|
||||
{ id: driverId, name: 'Result Hero', country: 'DE' },
|
||||
]);
|
||||
|
||||
const registrationRepo = new InMemoryRaceRegistrationRepository([
|
||||
{ raceId: race.id, driverId },
|
||||
]);
|
||||
|
||||
const resultEntity = Result.create({
|
||||
id: 'result-1',
|
||||
raceId: race.id,
|
||||
driverId,
|
||||
position: 1,
|
||||
fastestLap: 90.123,
|
||||
incidents: 0,
|
||||
startPosition: 3,
|
||||
});
|
||||
|
||||
const resultRepo = new InMemoryResultRepository([resultEntity]);
|
||||
const membershipRepo = new InMemoryLeagueMembershipRepository();
|
||||
membershipRepo.seedMembership({
|
||||
leagueId: league.id,
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-01-01'),
|
||||
});
|
||||
|
||||
const ratingProvider = new TestDriverRatingProvider();
|
||||
ratingProvider.seed(driverId, 2000);
|
||||
|
||||
const imageService = new TestImageService();
|
||||
const presenter = new FakeRaceDetailPresenter();
|
||||
|
||||
const useCase = new GetRaceDetailUseCase(
|
||||
raceRepo,
|
||||
leagueRepo,
|
||||
driverRepo,
|
||||
registrationRepo,
|
||||
resultRepo,
|
||||
membershipRepo,
|
||||
ratingProvider,
|
||||
imageService,
|
||||
presenter,
|
||||
);
|
||||
|
||||
// When (executing the query for the completed race)
|
||||
await useCase.execute({ raceId: race.id, driverId });
|
||||
|
||||
const viewModel = presenter.getViewModel();
|
||||
expect(viewModel).not.toBeNull();
|
||||
expect(viewModel!.userResult).not.toBeNull();
|
||||
|
||||
// Then (rating change uses the same formula as the legacy UI)
|
||||
// For P1: baseChange = 25, positionBonus = (20 - 1) * 2 = 38, total = 63
|
||||
expect(viewModel!.userResult!.ratingChange).toBe(63);
|
||||
expect(viewModel!.userResult!.position).toBe(1);
|
||||
expect(viewModel!.userResult!.startPosition).toBe(3);
|
||||
expect(viewModel!.userResult!.positionChange).toBe(2);
|
||||
expect(viewModel!.userResult!.isPodium).toBe(true);
|
||||
expect(viewModel!.userResult!.isClean).toBe(true);
|
||||
});
|
||||
|
||||
it('presents an error when race does not exist', async () => {
|
||||
// Given (no race in the repository)
|
||||
const raceRepo = new InMemoryRaceRepository([]);
|
||||
const leagueRepo = new InMemoryLeagueRepository([]);
|
||||
const driverRepo = new InMemoryDriverRepository([]);
|
||||
const registrationRepo = new InMemoryRaceRegistrationRepository();
|
||||
const resultRepo = new InMemoryResultRepository([]);
|
||||
const membershipRepo = new InMemoryLeagueMembershipRepository();
|
||||
const ratingProvider = new TestDriverRatingProvider();
|
||||
const imageService = new TestImageService();
|
||||
const presenter = new FakeRaceDetailPresenter();
|
||||
|
||||
const useCase = new GetRaceDetailUseCase(
|
||||
raceRepo,
|
||||
leagueRepo,
|
||||
driverRepo,
|
||||
registrationRepo,
|
||||
resultRepo,
|
||||
membershipRepo,
|
||||
ratingProvider,
|
||||
imageService,
|
||||
presenter,
|
||||
);
|
||||
|
||||
// When
|
||||
await useCase.execute({ raceId: 'missing-race', driverId: 'driver-x' });
|
||||
|
||||
const viewModel = presenter.getViewModel();
|
||||
// Then
|
||||
expect(viewModel).not.toBeNull();
|
||||
expect(viewModel!.race).toBeNull();
|
||||
expect(viewModel!.error).toBe('Race not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CancelRaceUseCase', () => {
|
||||
it('cancels a scheduled race and persists it via the repository', async () => {
|
||||
// Given (a scheduled race in the repository)
|
||||
const race = Race.create({
|
||||
id: 'cancel-me',
|
||||
leagueId: 'league-cancel',
|
||||
scheduledAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||
track: 'Cancel Circuit',
|
||||
car: 'GT4',
|
||||
sessionType: 'race',
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
const raceRepo = new InMemoryRaceRepository([race]);
|
||||
const useCase = new CancelRaceUseCase(raceRepo);
|
||||
|
||||
// When
|
||||
await useCase.execute({ raceId: race.id });
|
||||
|
||||
// Then (the stored race is now cancelled)
|
||||
const updated = raceRepo.getStored(race.id);
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated!.status).toBe('cancelled');
|
||||
});
|
||||
|
||||
it('throws when trying to cancel a non-existent race', async () => {
|
||||
// Given
|
||||
const raceRepo = new InMemoryRaceRepository([]);
|
||||
const useCase = new CancelRaceUseCase(raceRepo);
|
||||
|
||||
// When / Then
|
||||
await expect(
|
||||
useCase.execute({ raceId: 'does-not-exist' }),
|
||||
).rejects.toThrow('Race not found');
|
||||
});
|
||||
});
|
||||
479
tests/unit/racing-application/RaceResultsUseCases.test.ts
Normal file
479
tests/unit/racing-application/RaceResultsUseCases.test.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
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 { GetRaceResultsDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceResultsDetailUseCase';
|
||||
import { ImportRaceResultsUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsUseCase';
|
||||
|
||||
import type {
|
||||
IRaceResultsDetailPresenter,
|
||||
RaceResultsDetailViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRaceResultsDetailPresenter';
|
||||
import type {
|
||||
IImportRaceResultsPresenter,
|
||||
ImportRaceResultsSummaryViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IImportRaceResultsPresenter';
|
||||
|
||||
class FakeRaceResultsDetailPresenter implements IRaceResultsDetailPresenter {
|
||||
viewModel: RaceResultsDetailViewModel | null = 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) => races.get(id) ?? null,
|
||||
} as unknown as any;
|
||||
|
||||
const leagueRepository = {
|
||||
findById: async (id: string) => leagues.get(id) ?? null,
|
||||
} as unknown as any;
|
||||
|
||||
const resultRepository = {
|
||||
existsByRaceId: async (raceId: string) => {
|
||||
existsByRaceIdCalled = true;
|
||||
return storedResults.some((r) => r.raceId === raceId);
|
||||
},
|
||||
createMany: async (results: Result[]) => {
|
||||
storedResults.push(...results);
|
||||
return results;
|
||||
},
|
||||
} as unknown as any;
|
||||
|
||||
const standingRepository = {
|
||||
recalculate: async (leagueId: string) => {
|
||||
recalcCalls.push(leagueId);
|
||||
},
|
||||
} as unknown as any;
|
||||
|
||||
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) => races.get(id) ?? null,
|
||||
} as unknown as any;
|
||||
|
||||
const leagueRepository = {
|
||||
findById: async (id: string) => leagues.get(id) ?? null,
|
||||
} as unknown as any;
|
||||
|
||||
const resultRepository = {
|
||||
existsByRaceId: async (raceId: string) => {
|
||||
return storedResults.some((r) => r.raceId === raceId);
|
||||
},
|
||||
createMany: async (_results: Result[]) => {
|
||||
throw new Error('Should not be called when results already exist');
|
||||
},
|
||||
} as unknown as any;
|
||||
|
||||
const standingRepository = {
|
||||
recalculate: async (_leagueId: string) => {
|
||||
throw new Error('Should not be called when results already exist');
|
||||
},
|
||||
} as unknown as any;
|
||||
|
||||
const presenter = new FakeImportRaceResultsPresenter();
|
||||
|
||||
const useCase = new ImportRaceResultsUseCase(
|
||||
raceRepository,
|
||||
leagueRepository,
|
||||
resultRepository,
|
||||
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: 'driver-a', name: 'Driver A', country: 'US' } as any;
|
||||
const driver2 = { id: 'driver-b', name: 'Driver B', country: 'GB' } as any;
|
||||
|
||||
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) => races.get(id) ?? null,
|
||||
} as unknown as any;
|
||||
|
||||
const leagueRepository = {
|
||||
findById: async (id: string) => leagues.get(id) ?? null,
|
||||
} as unknown as any;
|
||||
|
||||
const resultRepository = {
|
||||
findByRaceId: async (raceId: string) =>
|
||||
results.filter((r) => r.raceId === raceId),
|
||||
} as unknown as any;
|
||||
|
||||
const driverRepository = {
|
||||
findAll: async () => drivers,
|
||||
} as unknown as any;
|
||||
|
||||
const penaltyRepository = {
|
||||
findByRaceId: async () => [] as Penalty[],
|
||||
} as unknown as any;
|
||||
|
||||
const presenter = new FakeRaceResultsDetailPresenter();
|
||||
|
||||
const useCase = new GetRaceResultsDetailUseCase(
|
||||
raceRepository,
|
||||
leagueRepository,
|
||||
resultRepository,
|
||||
driverRepository,
|
||||
penaltyRepository,
|
||||
presenter,
|
||||
);
|
||||
|
||||
// When executing the query
|
||||
await useCase.execute({ raceId: race.id });
|
||||
|
||||
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: 'driver-pen', name: 'Penalty Driver', country: 'DE' } as any;
|
||||
|
||||
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',
|
||||
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) => races.get(id) ?? null,
|
||||
} as unknown as any;
|
||||
|
||||
const leagueRepository = {
|
||||
findById: async (id: string) => leagues.get(id) ?? null,
|
||||
} as unknown as any;
|
||||
|
||||
const resultRepository = {
|
||||
findByRaceId: async (raceId: string) =>
|
||||
results.filter((r) => r.raceId === raceId),
|
||||
} as unknown as any;
|
||||
|
||||
const driverRepository = {
|
||||
findAll: async () => drivers,
|
||||
} as unknown as any;
|
||||
|
||||
const penaltyRepository = {
|
||||
findByRaceId: async (raceId: string) =>
|
||||
penalties.filter((p) => p.raceId === raceId),
|
||||
} as unknown as any;
|
||||
|
||||
const presenter = new FakeRaceResultsDetailPresenter();
|
||||
|
||||
const useCase = new GetRaceResultsDetailUseCase(
|
||||
raceRepository,
|
||||
leagueRepository,
|
||||
resultRepository,
|
||||
driverRepository,
|
||||
penaltyRepository,
|
||||
presenter,
|
||||
);
|
||||
|
||||
// When
|
||||
await useCase.execute({ raceId: race.id });
|
||||
|
||||
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 () => null,
|
||||
} as unknown as any;
|
||||
|
||||
const leagueRepository = {
|
||||
findById: async () => null,
|
||||
} as unknown as any;
|
||||
|
||||
const resultRepository = {
|
||||
findByRaceId: async () => [] as Result[],
|
||||
} as unknown as any;
|
||||
|
||||
const driverRepository = {
|
||||
findAll: async () => [] as any[],
|
||||
} as unknown as any;
|
||||
|
||||
const penaltyRepository = {
|
||||
findByRaceId: async () => [] as Penalty[],
|
||||
} as unknown as any;
|
||||
|
||||
const presenter = new FakeRaceResultsDetailPresenter();
|
||||
|
||||
const useCase = new GetRaceResultsDetailUseCase(
|
||||
raceRepository,
|
||||
leagueRepository,
|
||||
resultRepository,
|
||||
driverRepository,
|
||||
penaltyRepository,
|
||||
presenter,
|
||||
);
|
||||
|
||||
// When
|
||||
await useCase.execute({ raceId: 'missing-race' });
|
||||
|
||||
const viewModel = presenter.getViewModel();
|
||||
expect(viewModel).not.toBeNull();
|
||||
expect(viewModel!.race).toBeNull();
|
||||
expect(viewModel!.error).toBe('Race not found');
|
||||
});
|
||||
});
|
||||
@@ -19,8 +19,8 @@ import type {
|
||||
|
||||
import { RegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/RegisterForRaceUseCase';
|
||||
import { WithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/WithdrawFromRaceUseCase';
|
||||
import { IsDriverRegisteredForRaceQuery } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceQuery';
|
||||
import { GetRaceRegistrationsQuery } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsQuery';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsUseCase';
|
||||
|
||||
import { CreateTeamUseCase } from '@gridpilot/racing/application/use-cases/CreateTeamUseCase';
|
||||
import { JoinTeamUseCase } from '@gridpilot/racing/application/use-cases/JoinTeamUseCase';
|
||||
@@ -28,11 +28,18 @@ import { LeaveTeamUseCase } from '@gridpilot/racing/application/use-cases/LeaveT
|
||||
import { ApproveTeamJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/ApproveTeamJoinRequestUseCase';
|
||||
import { RejectTeamJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/RejectTeamJoinRequestUseCase';
|
||||
import { UpdateTeamUseCase } from '@gridpilot/racing/application/use-cases/UpdateTeamUseCase';
|
||||
import { GetAllTeamsQuery } from '@gridpilot/racing/application/use-cases/GetAllTeamsQuery';
|
||||
import { GetTeamDetailsQuery } from '@gridpilot/racing/application/use-cases/GetTeamDetailsQuery';
|
||||
import { GetTeamMembersQuery } from '@gridpilot/racing/application/use-cases/GetTeamMembersQuery';
|
||||
import { GetTeamJoinRequestsQuery } from '@gridpilot/racing/application/use-cases/GetTeamJoinRequestsQuery';
|
||||
import { GetDriverTeamQuery } from '@gridpilot/racing/application/use-cases/GetDriverTeamQuery';
|
||||
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
|
||||
import { GetTeamDetailsUseCase } from '@gridpilot/racing/application/use-cases/GetTeamDetailsUseCase';
|
||||
import { GetTeamMembersUseCase } from '@gridpilot/racing/application/use-cases/GetTeamMembersUseCase';
|
||||
import { GetTeamJoinRequestsUseCase } from '@gridpilot/racing/application/use-cases/GetTeamJoinRequestsUseCase';
|
||||
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
|
||||
import type { IDriverRegistrationStatusPresenter } from '@gridpilot/racing/application/presenters/IDriverRegistrationStatusPresenter';
|
||||
import type { IRaceRegistrationsPresenter } from '@gridpilot/racing/application/presenters/IRaceRegistrationsPresenter';
|
||||
import type { IAllTeamsPresenter } from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
|
||||
import type { ITeamDetailsPresenter } from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
|
||||
import type { ITeamMembersPresenter } from '@gridpilot/racing/application/presenters/ITeamMembersPresenter';
|
||||
import type { ITeamJoinRequestsPresenter } from '@gridpilot/racing/application/presenters/ITeamJoinRequestsPresenter';
|
||||
import type { IDriverTeamPresenter } from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
|
||||
|
||||
/**
|
||||
* Simple in-memory fakes mirroring current alpha behavior.
|
||||
@@ -138,6 +145,35 @@ class InMemoryLeagueMembershipRepositoryForRegistrations implements ILeagueMembe
|
||||
}
|
||||
}
|
||||
|
||||
class TestDriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
|
||||
isRegistered: boolean | null = null;
|
||||
raceId: string | null = null;
|
||||
driverId: string | null = null;
|
||||
|
||||
present(isRegistered: boolean, raceId: string, driverId: string): void {
|
||||
this.isRegistered = isRegistered;
|
||||
this.raceId = raceId;
|
||||
this.driverId = driverId;
|
||||
}
|
||||
}
|
||||
|
||||
class TestRaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
|
||||
raceId: string | null = null;
|
||||
driverIds: string[] = [];
|
||||
|
||||
// Accepts either the legacy (raceId, driverIds) shape or the new (driverIds) shape
|
||||
present(raceIdOrDriverIds: string | string[], driverIds?: string[]): void {
|
||||
if (Array.isArray(raceIdOrDriverIds) && driverIds == null) {
|
||||
this.raceId = null;
|
||||
this.driverIds = raceIdOrDriverIds;
|
||||
return;
|
||||
}
|
||||
|
||||
this.raceId = raceIdOrDriverIds as string;
|
||||
this.driverIds = driverIds ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryTeamRepository implements ITeamRepository {
|
||||
private teams: Team[] = [];
|
||||
|
||||
@@ -207,6 +243,10 @@ class InMemoryTeamMembershipRepository implements ITeamMembershipRepository {
|
||||
);
|
||||
}
|
||||
|
||||
async findByTeamId(teamId: string): Promise<TeamMembership[]> {
|
||||
return this.memberships.filter((m) => m.teamId === teamId);
|
||||
}
|
||||
|
||||
async saveMembership(membership: TeamMembership): Promise<TeamMembership> {
|
||||
const index = this.memberships.findIndex(
|
||||
(m) => m.teamId === membership.teamId && m.driverId === membership.driverId,
|
||||
@@ -267,8 +307,10 @@ describe('Racing application use-cases - registrations', () => {
|
||||
let membershipRepo: InMemoryLeagueMembershipRepositoryForRegistrations;
|
||||
let registerForRace: RegisterForRaceUseCase;
|
||||
let withdrawFromRace: WithdrawFromRaceUseCase;
|
||||
let isDriverRegistered: IsDriverRegisteredForRaceQuery;
|
||||
let getRaceRegistrations: GetRaceRegistrationsQuery;
|
||||
let isDriverRegistered: IsDriverRegisteredForRaceUseCase;
|
||||
let getRaceRegistrations: GetRaceRegistrationsUseCase;
|
||||
let driverRegistrationPresenter: TestDriverRegistrationStatusPresenter;
|
||||
let raceRegistrationsPresenter: TestRaceRegistrationsPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
registrationRepo = new InMemoryRaceRegistrationRepository();
|
||||
@@ -276,8 +318,16 @@ describe('Racing application use-cases - registrations', () => {
|
||||
|
||||
registerForRace = new RegisterForRaceUseCase(registrationRepo, membershipRepo);
|
||||
withdrawFromRace = new WithdrawFromRaceUseCase(registrationRepo);
|
||||
isDriverRegistered = new IsDriverRegisteredForRaceQuery(registrationRepo);
|
||||
getRaceRegistrations = new GetRaceRegistrationsQuery(registrationRepo);
|
||||
driverRegistrationPresenter = new TestDriverRegistrationStatusPresenter();
|
||||
isDriverRegistered = new IsDriverRegisteredForRaceUseCase(
|
||||
registrationRepo,
|
||||
driverRegistrationPresenter,
|
||||
);
|
||||
raceRegistrationsPresenter = new TestRaceRegistrationsPresenter();
|
||||
getRaceRegistrations = new GetRaceRegistrationsUseCase(
|
||||
registrationRepo,
|
||||
raceRegistrationsPresenter,
|
||||
);
|
||||
});
|
||||
|
||||
it('registers an active league member for a race and tracks registration', async () => {
|
||||
@@ -289,10 +339,13 @@ describe('Racing application use-cases - registrations', () => {
|
||||
|
||||
await registerForRace.execute({ raceId, leagueId, driverId });
|
||||
|
||||
expect(await isDriverRegistered.execute({ raceId, driverId })).toBe(true);
|
||||
await isDriverRegistered.execute({ raceId, driverId });
|
||||
expect(driverRegistrationPresenter.isRegistered).toBe(true);
|
||||
expect(driverRegistrationPresenter.raceId).toBe(raceId);
|
||||
expect(driverRegistrationPresenter.driverId).toBe(driverId);
|
||||
|
||||
const registeredDrivers = await getRaceRegistrations.execute({ raceId });
|
||||
expect(registeredDrivers).toContain(driverId);
|
||||
await getRaceRegistrations.execute({ raceId });
|
||||
expect(raceRegistrationsPresenter.driverIds).toContain(driverId);
|
||||
});
|
||||
|
||||
it('throws when registering a non-member for a race', async () => {
|
||||
@@ -315,8 +368,11 @@ describe('Racing application use-cases - registrations', () => {
|
||||
|
||||
await withdrawFromRace.execute({ raceId, driverId });
|
||||
|
||||
expect(await isDriverRegistered.execute({ raceId, driverId })).toBe(false);
|
||||
expect(await getRaceRegistrations.execute({ raceId })).toEqual([]);
|
||||
await isDriverRegistered.execute({ raceId, driverId });
|
||||
expect(driverRegistrationPresenter.isRegistered).toBe(false);
|
||||
|
||||
await getRaceRegistrations.execute({ raceId });
|
||||
expect(raceRegistrationsPresenter.driverIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -330,11 +386,69 @@ describe('Racing application use-cases - teams', () => {
|
||||
let approveJoin: ApproveTeamJoinRequestUseCase;
|
||||
let rejectJoin: RejectTeamJoinRequestUseCase;
|
||||
let updateTeamUseCase: UpdateTeamUseCase;
|
||||
let getAllTeamsQuery: GetAllTeamsQuery;
|
||||
let getTeamDetailsQuery: GetTeamDetailsQuery;
|
||||
let getTeamMembersQuery: GetTeamMembersQuery;
|
||||
let getTeamJoinRequestsQuery: GetTeamJoinRequestsQuery;
|
||||
let getDriverTeamQuery: GetDriverTeamQuery;
|
||||
let getAllTeamsUseCase: GetAllTeamsUseCase;
|
||||
let getTeamDetailsUseCase: GetTeamDetailsUseCase;
|
||||
let getTeamMembersUseCase: GetTeamMembersUseCase;
|
||||
let getTeamJoinRequestsUseCase: GetTeamJoinRequestsUseCase;
|
||||
let getDriverTeamUseCase: GetDriverTeamUseCase;
|
||||
|
||||
class FakeDriverRepository {
|
||||
async findById(driverId: string): Promise<{ id: string; name: string } | null> {
|
||||
return { id: driverId, name: `Driver ${driverId}` };
|
||||
}
|
||||
}
|
||||
|
||||
class FakeImageService {
|
||||
getDriverAvatar(driverId: string): string {
|
||||
return `https://example.com/avatar/${driverId}.png`;
|
||||
}
|
||||
}
|
||||
|
||||
class TestAllTeamsPresenter implements IAllTeamsPresenter {
|
||||
teams: any[] = [];
|
||||
|
||||
present(teams: any[]): void {
|
||||
this.teams = teams;
|
||||
}
|
||||
}
|
||||
|
||||
class TestTeamDetailsPresenter implements ITeamDetailsPresenter {
|
||||
viewModel: any = null;
|
||||
|
||||
present(team: any, membership: any, driverId: string): void {
|
||||
this.viewModel = { team, membership, driverId };
|
||||
}
|
||||
}
|
||||
|
||||
class TestTeamMembersPresenter implements ITeamMembersPresenter {
|
||||
members: any[] = [];
|
||||
|
||||
present(members: any[]): void {
|
||||
this.members = members;
|
||||
}
|
||||
}
|
||||
|
||||
class TestTeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
|
||||
requests: any[] = [];
|
||||
|
||||
present(requests: any[]): void {
|
||||
this.requests = requests;
|
||||
}
|
||||
}
|
||||
|
||||
class TestDriverTeamPresenter implements IDriverTeamPresenter {
|
||||
viewModel: any = null;
|
||||
|
||||
present(team: any, membership: any, driverId: string): void {
|
||||
this.viewModel = { team, membership, driverId };
|
||||
}
|
||||
}
|
||||
|
||||
let allTeamsPresenter: TestAllTeamsPresenter;
|
||||
let teamDetailsPresenter: TestTeamDetailsPresenter;
|
||||
let teamMembersPresenter: TestTeamMembersPresenter;
|
||||
let teamJoinRequestsPresenter: TestTeamJoinRequestsPresenter;
|
||||
let driverTeamPresenter: TestDriverTeamPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
teamRepo = new InMemoryTeamRepository();
|
||||
@@ -346,11 +460,43 @@ describe('Racing application use-cases - teams', () => {
|
||||
approveJoin = new ApproveTeamJoinRequestUseCase(membershipRepo);
|
||||
rejectJoin = new RejectTeamJoinRequestUseCase(membershipRepo);
|
||||
updateTeamUseCase = new UpdateTeamUseCase(teamRepo, membershipRepo);
|
||||
getAllTeamsQuery = new GetAllTeamsQuery(teamRepo);
|
||||
getTeamDetailsQuery = new GetTeamDetailsQuery(teamRepo, membershipRepo);
|
||||
getTeamMembersQuery = new GetTeamMembersQuery(membershipRepo);
|
||||
getTeamJoinRequestsQuery = new GetTeamJoinRequestsQuery(membershipRepo);
|
||||
getDriverTeamQuery = new GetDriverTeamQuery(teamRepo, membershipRepo);
|
||||
|
||||
allTeamsPresenter = new TestAllTeamsPresenter();
|
||||
getAllTeamsUseCase = new GetAllTeamsUseCase(
|
||||
teamRepo,
|
||||
membershipRepo,
|
||||
allTeamsPresenter,
|
||||
);
|
||||
|
||||
teamDetailsPresenter = new TestTeamDetailsPresenter();
|
||||
getTeamDetailsUseCase = new GetTeamDetailsUseCase(
|
||||
teamRepo,
|
||||
membershipRepo,
|
||||
teamDetailsPresenter,
|
||||
);
|
||||
|
||||
teamMembersPresenter = new TestTeamMembersPresenter();
|
||||
getTeamMembersUseCase = new GetTeamMembersUseCase(
|
||||
membershipRepo,
|
||||
new FakeDriverRepository() as any,
|
||||
new FakeImageService() as any,
|
||||
teamMembersPresenter,
|
||||
);
|
||||
|
||||
teamJoinRequestsPresenter = new TestTeamJoinRequestsPresenter();
|
||||
getTeamJoinRequestsUseCase = new GetTeamJoinRequestsUseCase(
|
||||
membershipRepo,
|
||||
new FakeDriverRepository() as any,
|
||||
new FakeImageService() as any,
|
||||
teamJoinRequestsPresenter,
|
||||
);
|
||||
|
||||
driverTeamPresenter = new TestDriverTeamPresenter();
|
||||
getDriverTeamUseCase = new GetDriverTeamUseCase(
|
||||
teamRepo,
|
||||
membershipRepo,
|
||||
driverTeamPresenter,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a team and assigns creator as active owner', async () => {
|
||||
@@ -449,13 +595,10 @@ describe('Racing application use-cases - teams', () => {
|
||||
updatedBy: ownerId,
|
||||
});
|
||||
|
||||
const teamDetails = await getTeamDetailsQuery.execute({
|
||||
teamId: created.team.id,
|
||||
driverId: ownerId,
|
||||
});
|
||||
await getTeamDetailsUseCase.execute(created.team.id, ownerId);
|
||||
|
||||
expect(teamDetails.team.name).toBe('Updated Name');
|
||||
expect(teamDetails.team.description).toBe('Updated description');
|
||||
expect(teamDetailsPresenter.viewModel.team.name).toBe('Updated Name');
|
||||
expect(teamDetailsPresenter.viewModel.team.description).toBe('Updated description');
|
||||
});
|
||||
|
||||
it('returns driver team via query matching legacy getDriverTeam behavior', async () => {
|
||||
@@ -469,7 +612,8 @@ describe('Racing application use-cases - teams', () => {
|
||||
leagues: [],
|
||||
});
|
||||
|
||||
const result = await getDriverTeamQuery.execute({ driverId: ownerId });
|
||||
await getDriverTeamUseCase.execute(ownerId);
|
||||
const result = driverTeamPresenter.viewModel;
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.team.id).toBe(team.id);
|
||||
expect(result?.membership.driverId).toBe(ownerId);
|
||||
@@ -489,11 +633,11 @@ describe('Racing application use-cases - teams', () => {
|
||||
|
||||
await joinTeam.execute({ teamId: team.id, driverId: otherDriverId });
|
||||
|
||||
const teams = await getAllTeamsQuery.execute();
|
||||
expect(teams.length).toBe(1);
|
||||
await getAllTeamsUseCase.execute();
|
||||
expect(allTeamsPresenter.teams.length).toBe(1);
|
||||
|
||||
const members = await getTeamMembersQuery.execute({ teamId: team.id });
|
||||
const memberIds = members.map((m) => m.driverId).sort();
|
||||
await getTeamMembersUseCase.execute(team.id);
|
||||
const memberIds = teamMembersPresenter.members.map((m) => m.driverId).sort();
|
||||
expect(memberIds).toEqual([ownerId, otherDriverId].sort());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user