This commit is contained in:
2025-12-11 00:57:32 +01:00
parent 1303a14493
commit 6a427eab57
112 changed files with 6148 additions and 2272 deletions

View File

@@ -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());
});
});