This commit is contained in:
2025-12-11 21:06:25 +01:00
parent c49ea2598d
commit ec3ddc3a5c
227 changed files with 3496 additions and 2083 deletions

View File

@@ -19,10 +19,14 @@ class FakeDashboardOverviewPresenter implements IDashboardOverviewPresenter {
}
}
function createTestImageService() {
interface TestImageService {
getDriverAvatar(driverId: string): string;
}
function createTestImageService(): TestImageService {
return {
getDriverAvatar: (driverId: string) => `avatar-${driverId}`,
} as any;
};
}
describe('GetDashboardOverviewUseCase', () => {
@@ -74,7 +78,7 @@ describe('GetDashboardOverviewUseCase', () => {
},
];
const results: any[] = [];
const results: unknown[] = [];
const memberships = [
{
@@ -92,29 +96,53 @@ describe('GetDashboardOverviewUseCase', () => {
const registeredRaceIds = new Set<string>(['race-1', 'race-3']);
const feedItems: DashboardFeedItemSummaryViewModel[] = [];
const friends: any[] = [];
const driverRepository = {
const friends: Array<{ id: string }> = [];
const driverRepository: {
findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>;
} = {
findById: async (id: string) => (id === driver.id ? driver : null),
} as any;
const raceRepository = {
};
const raceRepository: {
findAll: () => Promise<
Array<{
id: string;
leagueId: string;
track: string;
car: string;
scheduledAt: Date;
status: 'scheduled';
}>
>;
} = {
findAll: async () => races,
} as any;
const resultRepository = {
};
const resultRepository: {
findAll: () => Promise<unknown[]>;
} = {
findAll: async () => results,
} as any;
const leagueRepository = {
};
const leagueRepository: {
findAll: () => Promise<Array<{ id: string; name: string }>>;
} = {
findAll: async () => leagues,
} as any;
const standingRepository = {
};
const standingRepository: {
findByLeagueId: (leagueId: string) => Promise<unknown[]>;
} = {
findByLeagueId: async () => [],
} as any;
const leagueMembershipRepository = {
};
const leagueMembershipRepository: {
getMembership: (
leagueId: string,
driverIdParam: string,
) => Promise<{ leagueId: string; driverId: string; status: string } | null>;
} = {
getMembership: async (leagueId: string, driverIdParam: string) => {
return (
memberships.find(
@@ -122,22 +150,28 @@ describe('GetDashboardOverviewUseCase', () => {
) ?? null
);
},
} as any;
const raceRegistrationRepository = {
};
const raceRegistrationRepository: {
isRegistered: (raceId: string, driverIdParam: string) => Promise<boolean>;
} = {
isRegistered: async (raceId: string, driverIdParam: string) => {
if (driverIdParam !== driverId) return false;
return registeredRaceIds.has(raceId);
},
} as any;
const feedRepository = {
};
const feedRepository: {
getFeedForDriver: (driverIdParam: string) => Promise<DashboardFeedItemSummaryViewModel[]>;
} = {
getFeedForDriver: async () => feedItems,
} as any;
const socialRepository = {
};
const socialRepository: {
getFriends: (driverIdParam: string) => Promise<Array<{ id: string }>>;
} = {
getFriends: async () => friends,
} as any;
};
const imageService = createTestImageService();
@@ -250,7 +284,10 @@ describe('GetDashboardOverviewUseCase', () => {
},
];
const standingsByLeague = new Map<string, any[]>();
const standingsByLeague = new Map<
string,
Array<{ leagueId: string; driverId: string; position: number; points: number }>
>();
standingsByLeague.set('league-A', [
{ leagueId: 'league-A', driverId, position: 3, points: 50 },
{ leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 },
@@ -260,28 +297,43 @@ describe('GetDashboardOverviewUseCase', () => {
{ leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 },
]);
const driverRepository = {
const driverRepository: {
findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>;
} = {
findById: async (id: string) => (id === driver.id ? driver : null),
} as any;
const raceRepository = {
};
const raceRepository: {
findAll: () => Promise<typeof races>;
} = {
findAll: async () => races,
} as any;
const resultRepository = {
};
const resultRepository: {
findAll: () => Promise<typeof results>;
} = {
findAll: async () => results,
} as any;
const leagueRepository = {
};
const leagueRepository: {
findAll: () => Promise<typeof leagues>;
} = {
findAll: async () => leagues,
} as any;
const standingRepository = {
};
const standingRepository: {
findByLeagueId: (leagueId: string) => Promise<Array<{ leagueId: string; driverId: string; position: number; points: number }>>;
} = {
findByLeagueId: async (leagueId: string) =>
standingsByLeague.get(leagueId) ?? [],
} as any;
const leagueMembershipRepository = {
};
const leagueMembershipRepository: {
getMembership: (
leagueId: string,
driverIdParam: string,
) => Promise<{ leagueId: string; driverId: string; status: string } | null>;
} = {
getMembership: async (leagueId: string, driverIdParam: string) => {
return (
memberships.find(
@@ -289,19 +341,25 @@ describe('GetDashboardOverviewUseCase', () => {
) ?? null
);
},
} as any;
const raceRegistrationRepository = {
};
const raceRegistrationRepository: {
isRegistered: (raceId: string, driverIdParam: string) => Promise<boolean>;
} = {
isRegistered: async () => false,
} as any;
const feedRepository = {
};
const feedRepository: {
getFeedForDriver: (driverIdParam: string) => Promise<DashboardFeedItemSummaryViewModel[]>;
} = {
getFeedForDriver: async () => [],
} as any;
const socialRepository = {
};
const socialRepository: {
getFriends: (driverIdParam: string) => Promise<Array<{ id: string }>>;
} = {
getFriends: async () => [],
} as any;
};
const imageService = createTestImageService();
@@ -372,41 +430,53 @@ describe('GetDashboardOverviewUseCase', () => {
const driver = { id: driverId, name: 'New Racer', country: 'FR' };
const driverRepository = {
const driverRepository: {
findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>;
} = {
findById: async (id: string) => (id === driver.id ? driver : null),
} as any;
const raceRepository = {
};
const raceRepository: { findAll: () => Promise<never[]> } = {
findAll: async () => [],
} as any;
const resultRepository = {
};
const resultRepository: { findAll: () => Promise<never[]> } = {
findAll: async () => [],
} as any;
const leagueRepository = {
};
const leagueRepository: { findAll: () => Promise<never[]> } = {
findAll: async () => [],
} as any;
const standingRepository = {
};
const standingRepository: {
findByLeagueId: (leagueId: string) => Promise<never[]>;
} = {
findByLeagueId: async () => [],
} as any;
const leagueMembershipRepository = {
};
const leagueMembershipRepository: {
getMembership: (leagueId: string, driverIdParam: string) => Promise<null>;
} = {
getMembership: async () => null,
} as any;
const raceRegistrationRepository = {
};
const raceRegistrationRepository: {
isRegistered: (raceId: string, driverIdParam: string) => Promise<boolean>;
} = {
isRegistered: async () => false,
} as any;
const feedRepository = {
};
const feedRepository: {
getFeedForDriver: (driverIdParam: string) => Promise<DashboardFeedItemSummaryViewModel[]>;
} = {
getFeedForDriver: async () => [],
} as any;
const socialRepository = {
};
const socialRepository: {
getFriends: (driverIdParam: string) => Promise<Array<{ id: string }>>;
} = {
getFriends: async () => [],
} as any;
};
const imageService = createTestImageService();

View File

@@ -121,28 +121,28 @@ class InMemoryLeagueRepository implements ILeagueRepository {
}
class InMemoryDriverRepository implements IDriverRepository {
private drivers = new Map<string, any>();
private drivers = new Map<string, { id: string; name: string; country: string }>();
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> {
async findById(id: string): Promise<{ id: string; name: string; country: string } | null> {
return this.drivers.get(id) ?? null;
}
async findAll(): Promise<any[]> {
async findAll(): Promise<Array<{ id: string; name: string; country: string }>> {
return [...this.drivers.values()];
}
async findByIds(ids: string[]): Promise<any[]> {
async findByIds(ids: string[]): Promise<Array<{ id: string; name: string; country: string }>> {
return ids
.map(id => this.drivers.get(id))
.filter((d): d is any => !!d);
.filter((d): d is { id: string; name: string; country: string } => !!d);
}
async create(): Promise<any> {

View File

@@ -73,15 +73,22 @@ describe('ImportRaceResultsUseCase', () => {
let existsByRaceIdCalled = false;
const recalcCalls: string[] = [];
const raceRepository = {
const raceRepository: {
findById: (id: string) => Promise<Race | null>;
} = {
findById: async (id: string) => races.get(id) ?? null,
} as unknown as any;
const leagueRepository = {
};
const leagueRepository: {
findById: (id: string) => Promise<League | null>;
} = {
findById: async (id: string) => leagues.get(id) ?? null,
} as unknown as any;
const resultRepository = {
};
const resultRepository: {
existsByRaceId: (raceId: string) => Promise<boolean>;
createMany: (results: Result[]) => Promise<Result[]>;
} = {
existsByRaceId: async (raceId: string) => {
existsByRaceIdCalled = true;
return storedResults.some((r) => r.raceId === raceId);
@@ -90,13 +97,15 @@ describe('ImportRaceResultsUseCase', () => {
storedResults.push(...results);
return results;
},
} as unknown as any;
const standingRepository = {
};
const standingRepository: {
recalculate: (leagueId: string) => Promise<void>;
} = {
recalculate: async (leagueId: string) => {
recalcCalls.push(leagueId);
},
} as unknown as any;
};
const presenter = new FakeImportRaceResultsPresenter();
@@ -183,28 +192,37 @@ describe('ImportRaceResultsUseCase', () => {
}),
];
const raceRepository = {
const raceRepository: {
findById: (id: string) => Promise<Race | null>;
} = {
findById: async (id: string) => races.get(id) ?? null,
} as unknown as any;
const leagueRepository = {
};
const leagueRepository: {
findById: (id: string) => Promise<League | null>;
} = {
findById: async (id: string) => leagues.get(id) ?? null,
} as unknown as any;
const resultRepository = {
};
const resultRepository: {
existsByRaceId: (raceId: string) => Promise<boolean>;
createMany: (results: Result[]) => Promise<Result[]>;
} = {
existsByRaceId: async (raceId: string) => {
return storedResults.some((r) => r.raceId === raceId);
},
createMany: async (_results: Result[]) => {
throw new Error('Should not be called when results already exist');
},
} as unknown as any;
const standingRepository = {
};
const standingRepository: {
recalculate: (leagueId: string) => Promise<void>;
} = {
recalculate: async (_leagueId: string) => {
throw new Error('Should not be called when results already exist');
},
} as unknown as any;
};
const presenter = new FakeImportRaceResultsPresenter();
@@ -257,8 +275,16 @@ describe('GetRaceResultsDetailUseCase', () => {
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 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',
@@ -285,26 +311,36 @@ describe('GetRaceResultsDetailUseCase', () => {
const results = [result1, result2];
const drivers = [driver1, driver2];
const raceRepository = {
const raceRepository: {
findById: (id: string) => Promise<Race | null>;
} = {
findById: async (id: string) => races.get(id) ?? null,
} as unknown as any;
const leagueRepository = {
};
const leagueRepository: {
findById: (id: string) => Promise<League | null>;
} = {
findById: async (id: string) => leagues.get(id) ?? null,
} as unknown as any;
const resultRepository = {
};
const resultRepository: {
findByRaceId: (raceId: string) => Promise<Result[]>;
} = {
findByRaceId: async (raceId: string) =>
results.filter((r) => r.raceId === raceId),
} as unknown as any;
const driverRepository = {
};
const driverRepository: {
findAll: () => Promise<Array<{ id: string; name: string; country: string }>>;
} = {
findAll: async () => drivers,
} as unknown as any;
const penaltyRepository = {
};
const penaltyRepository: {
findByRaceId: (raceId: string) => Promise<Penalty[]>;
} = {
findByRaceId: async () => [] as Penalty[],
} as unknown as any;
};
const presenter = new FakeRaceResultsDetailPresenter();
@@ -350,7 +386,11 @@ describe('GetRaceResultsDetailUseCase', () => {
status: 'completed',
});
const driver = { id: 'driver-pen', name: 'Penalty Driver', country: 'DE' } as any;
const driver: { id: string; name: string; country: string } = {
id: 'driver-pen',
name: 'Penalty Driver',
country: 'DE',
};
const result = Result.create({
id: 'res-pen',
@@ -380,27 +420,37 @@ describe('GetRaceResultsDetailUseCase', () => {
const drivers = [driver];
const penalties = [penalty];
const raceRepository = {
const raceRepository: {
findById: (id: string) => Promise<Race | null>;
} = {
findById: async (id: string) => races.get(id) ?? null,
} as unknown as any;
const leagueRepository = {
};
const leagueRepository: {
findById: (id: string) => Promise<League | null>;
} = {
findById: async (id: string) => leagues.get(id) ?? null,
} as unknown as any;
const resultRepository = {
};
const resultRepository: {
findByRaceId: (raceId: string) => Promise<Result[]>;
} = {
findByRaceId: async (raceId: string) =>
results.filter((r) => r.raceId === raceId),
} as unknown as any;
const driverRepository = {
};
const driverRepository: {
findAll: () => Promise<Array<{ id: string; name: string; country: string }>>;
} = {
findAll: async () => drivers,
} as unknown as any;
const penaltyRepository = {
};
const penaltyRepository: {
findByRaceId: (raceId: string) => Promise<Penalty[]>;
} = {
findByRaceId: async (raceId: string) =>
penalties.filter((p) => p.raceId === raceId),
} as unknown as any;
};
const presenter = new FakeRaceResultsDetailPresenter();
@@ -437,28 +487,38 @@ describe('GetRaceResultsDetailUseCase', () => {
it('presents an error when race does not exist', async () => {
// Given repositories without the requested race
const raceRepository = {
const raceRepository: {
findById: (id: string) => Promise<Race | null>;
} = {
findById: async () => null,
} as unknown as any;
const leagueRepository = {
};
const leagueRepository: {
findById: (id: string) => Promise<League | null>;
} = {
findById: async () => null,
} as unknown as any;
const resultRepository = {
};
const resultRepository: {
findByRaceId: (raceId: string) => Promise<Result[]>;
} = {
findByRaceId: async () => [] as Result[],
} as unknown as any;
const driverRepository = {
findAll: async () => [] as any[],
} as unknown as any;
const penaltyRepository = {
};
const driverRepository: {
findAll: () => Promise<Array<{ id: string; name: string; country: string }>>;
} = {
findAll: async () => [],
};
const penaltyRepository: {
findByRaceId: (raceId: string) => Promise<Penalty[]>;
} = {
findByRaceId: async () => [] as Penalty[],
} as unknown as any;
};
const presenter = new FakeRaceResultsDetailPresenter();
const useCase = new GetRaceResultsDetailUseCase(
raceRepository,
leagueRepository,
@@ -467,10 +527,10 @@ describe('GetRaceResultsDetailUseCase', () => {
penaltyRepository,
presenter,
);
// When
await useCase.execute({ raceId: 'missing-race' });
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
expect(viewModel!.race).toBeNull();

View File

@@ -35,11 +35,27 @@ import { GetTeamJoinRequestsUseCase } from '@gridpilot/racing/application/use-ca
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 {
IAllTeamsPresenter,
AllTeamsResultDTO,
AllTeamsViewModel,
} 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';
import type {
ITeamMembersPresenter,
TeamMembersResultDTO,
TeamMembersViewModel,
} from '@gridpilot/racing/application/presenters/ITeamMembersPresenter';
import type {
ITeamJoinRequestsPresenter,
TeamJoinRequestsResultDTO,
TeamJoinRequestsViewModel,
} from '@gridpilot/racing/application/presenters/ITeamJoinRequestsPresenter';
import type {
IDriverTeamPresenter,
DriverTeamResultDTO,
DriverTeamViewModel,
} from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
/**
* Simple in-memory fakes mirroring current alpha behavior.
@@ -407,10 +423,35 @@ describe('Racing application use-cases - teams', () => {
}
class TestAllTeamsPresenter implements IAllTeamsPresenter {
teams: any[] = [];
private viewModel: AllTeamsViewModel | null = null;
present(teams: any[]): void {
this.teams = teams;
reset(): void {
this.viewModel = null;
}
present(input: AllTeamsResultDTO): void {
this.viewModel = {
teams: input.teams.map((team) => ({
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
memberCount: team.memberCount,
leagues: team.leagues,
specialization: team.specialization,
region: team.region,
languages: team.languages,
})),
totalCount: input.teams.length,
};
}
getViewModel(): AllTeamsViewModel | null {
return this.viewModel;
}
get teams(): any[] {
return this.viewModel?.teams ?? [];
}
}
@@ -423,26 +464,129 @@ describe('Racing application use-cases - teams', () => {
}
class TestTeamMembersPresenter implements ITeamMembersPresenter {
members: any[] = [];
private viewModel: TeamMembersViewModel | null = null;
present(members: any[]): void {
this.members = members;
reset(): void {
this.viewModel = null;
}
present(input: TeamMembersResultDTO): void {
const members = input.memberships.map((membership) => {
const driverId = membership.driverId;
const driverName = input.driverNames[driverId] ?? driverId;
const avatarUrl = input.avatarUrls[driverId] ?? '';
return {
driverId,
driverName,
role: membership.role,
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.status === 'active',
avatarUrl,
};
});
const ownerCount = members.filter((m) => m.role === 'owner').length;
const managerCount = members.filter((m) => m.role === 'manager').length;
const memberCount = members.filter((m) => m.role === 'member').length;
this.viewModel = {
members,
totalCount: members.length,
ownerCount,
managerCount,
memberCount,
};
}
getViewModel(): TeamMembersViewModel | null {
return this.viewModel;
}
get members(): any[] {
return this.viewModel?.members ?? [];
}
}
class TestTeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
requests: any[] = [];
private viewModel: TeamJoinRequestsViewModel | null = null;
present(requests: any[]): void {
this.requests = requests;
reset(): void {
this.viewModel = null;
}
present(input: TeamJoinRequestsResultDTO): void {
const requests = input.requests.map((request) => {
const driverId = request.driverId;
const driverName = input.driverNames[driverId] ?? driverId;
const avatarUrl = input.avatarUrls[driverId] ?? '';
return {
requestId: request.id,
driverId,
driverName,
teamId: request.teamId,
status: 'pending',
requestedAt: request.requestedAt.toISOString(),
avatarUrl,
};
});
const pendingCount = requests.filter((r) => r.status === 'pending').length;
this.viewModel = {
requests,
pendingCount,
totalCount: requests.length,
};
}
getViewModel(): TeamJoinRequestsViewModel | null {
return this.viewModel;
}
get requests(): any[] {
return this.viewModel?.requests ?? [];
}
}
class TestDriverTeamPresenter implements IDriverTeamPresenter {
viewModel: any = null;
private viewModel: DriverTeamViewModel | null = null;
present(team: any, membership: any, driverId: string): void {
this.viewModel = { team, membership, driverId };
reset(): void {
this.viewModel = null;
}
present(input: DriverTeamResultDTO): void {
const { team, membership, driverId } = input;
const isOwner = team.ownerId === driverId;
const canManage = membership.role === 'owner' || membership.role === 'manager';
this.viewModel = {
team: {
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
leagues: team.leagues,
specialization: team.specialization,
region: team.region,
languages: team.languages,
},
membership: {
role: membership.role,
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.status === 'active',
},
isOwner,
canManage,
};
}
getViewModel(): DriverTeamViewModel | null {
return this.viewModel;
}
}
@@ -477,19 +621,22 @@ describe('Racing application use-cases - teams', () => {
teamDetailsPresenter,
);
const driverRepository = new FakeDriverRepository();
const imageService = new FakeImageService();
teamMembersPresenter = new TestTeamMembersPresenter();
getTeamMembersUseCase = new GetTeamMembersUseCase(
membershipRepo,
new FakeDriverRepository() as any,
new FakeImageService() as any,
driverRepository,
imageService,
teamMembersPresenter,
);
teamJoinRequestsPresenter = new TestTeamJoinRequestsPresenter();
getTeamJoinRequestsUseCase = new GetTeamJoinRequestsUseCase(
membershipRepo,
new FakeDriverRepository() as any,
new FakeImageService() as any,
driverRepository,
imageService,
teamJoinRequestsPresenter,
);
@@ -614,11 +761,12 @@ describe('Racing application use-cases - teams', () => {
leagues: [],
});
await getDriverTeamUseCase.execute(ownerId);
await getDriverTeamUseCase.execute({ driverId: ownerId }, driverTeamPresenter);
const result = driverTeamPresenter.viewModel;
expect(result).not.toBeNull();
expect(result?.team.id).toBe(team.id);
expect(result?.membership.driverId).toBe(ownerId);
expect(result?.membership.isActive).toBe(true);
expect(result?.isOwner).toBe(true);
});
it('lists all teams and members via queries after multiple operations', async () => {
@@ -635,10 +783,10 @@ describe('Racing application use-cases - teams', () => {
await joinTeam.execute({ teamId: team.id, driverId: otherDriverId });
await getAllTeamsUseCase.execute();
await getAllTeamsUseCase.execute(undefined as void, allTeamsPresenter);
expect(allTeamsPresenter.teams.length).toBe(1);
await getTeamMembersUseCase.execute(team.id);
await getTeamMembersUseCase.execute({ teamId: team.id }, teamMembersPresenter);
const memberIds = teamMembersPresenter.members.map((m) => m.driverId).sort();
expect(memberIds).toEqual([ownerId, otherDriverId].sort());
});