tests cleanup
This commit is contained in:
841
tests/integration/racing/RegistrationAndTeamUseCases.test.ts
Normal file
841
tests/integration/racing/RegistrationAndTeamUseCases.test.ts
Normal file
@@ -0,0 +1,841 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
|
||||
|
||||
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import type {
|
||||
TeamMembership,
|
||||
TeamMembershipStatus,
|
||||
TeamRole,
|
||||
TeamJoinRequest,
|
||||
} from '@core/racing/domain/types/TeamMembership';
|
||||
|
||||
import { RegisterForRaceUseCase } from '@core/racing/application/use-cases/RegisterForRaceUseCase';
|
||||
import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import { GetRaceRegistrationsUseCase } from '@core/racing/application/use-cases/GetRaceRegistrationsUseCase';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import type { IRaceRegistrationsPresenter } from '@core/racing/application/presenters/IRaceRegistrationsPresenter';
|
||||
import type { GetAllTeamsOutputPort } from '@core/racing/application/ports/output/GetAllTeamsOutputPort';
|
||||
import type { ITeamDetailsPresenter } from '@core/racing/application/presenters/ITeamDetailsPresenter';
|
||||
import type {
|
||||
ITeamMembersPresenter,
|
||||
TeamMembersResultDTO,
|
||||
TeamMembersViewModel,
|
||||
} from '@core/racing/application/presenters/ITeamMembersPresenter';
|
||||
import type {
|
||||
ITeamJoinRequestsPresenter,
|
||||
TeamJoinRequestsResultDTO,
|
||||
TeamJoinRequestsViewModel,
|
||||
} from '@core/racing/application/presenters/ITeamJoinRequestsPresenter';
|
||||
import type {
|
||||
IDriverTeamPresenter,
|
||||
DriverTeamResultDTO,
|
||||
DriverTeamViewModel,
|
||||
} from '@core/racing/application/presenters/IDriverTeamPresenter';
|
||||
import type { RaceRegistrationsResultDTO } from '@core/racing/application/presenters/IRaceRegistrationsPresenter';
|
||||
|
||||
/**
|
||||
* Simple in-memory fakes mirroring current alpha behavior.
|
||||
*/
|
||||
|
||||
class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository {
|
||||
private registrations = new Map<string, Set<string>>(); // raceId -> driverIds
|
||||
|
||||
async isRegistered(raceId: string, driverId: string): Promise<boolean> {
|
||||
const set = this.registrations.get(raceId);
|
||||
return set ? set.has(driverId) : false;
|
||||
}
|
||||
|
||||
async getRegisteredDrivers(raceId: string): Promise<string[]> {
|
||||
const set = this.registrations.get(raceId);
|
||||
return set ? Array.from(set) : [];
|
||||
}
|
||||
|
||||
async getRegistrationCount(raceId: string): Promise<number> {
|
||||
const set = this.registrations.get(raceId);
|
||||
return set ? set.size : 0;
|
||||
}
|
||||
|
||||
async register(registration: RaceRegistration): 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> {
|
||||
const set = this.registrations.get(raceId);
|
||||
if (!set || !set.has(driverId)) {
|
||||
throw new Error('Not registered for this race');
|
||||
}
|
||||
set.delete(driverId);
|
||||
if (set.size === 0) {
|
||||
this.registrations.delete(raceId);
|
||||
}
|
||||
}
|
||||
|
||||
async getDriverRegistrations(driverId: string): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
for (const [raceId, set] of this.registrations.entries()) {
|
||||
if (set.has(driverId)) {
|
||||
result.push(raceId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async clearRaceRegistrations(raceId: string): Promise<void> {
|
||||
this.registrations.delete(raceId);
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryLeagueMembershipRepositoryForRegistrations implements ILeagueMembershipRepository {
|
||||
private memberships: LeagueMembership[] = [];
|
||||
|
||||
async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||
return (
|
||||
this.memberships.find(
|
||||
(m) => m.leagueId === leagueId && m.leagueId === leagueId && m.driverId === driverId,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
|
||||
return this.memberships.filter(
|
||||
(m) => m.leagueId === leagueId && m.status === 'active',
|
||||
);
|
||||
}
|
||||
|
||||
async getJoinRequests(): Promise<never> {
|
||||
throw new Error('Not needed for registration tests');
|
||||
}
|
||||
|
||||
async saveMembership(membership: LeagueMembership): Promise<LeagueMembership> {
|
||||
this.memberships.push(membership);
|
||||
return membership;
|
||||
}
|
||||
|
||||
async removeMembership(): Promise<void> {
|
||||
throw new Error('Not needed for registration tests');
|
||||
}
|
||||
|
||||
async saveJoinRequest(): Promise<never> {
|
||||
throw new Error('Not needed for registration tests');
|
||||
}
|
||||
|
||||
async removeJoinRequest(): Promise<never> {
|
||||
throw new Error('Not needed for registration tests');
|
||||
}
|
||||
|
||||
seedActiveMembership(leagueId: string, driverId: string): void {
|
||||
this.memberships.push(
|
||||
LeagueMembership.create({
|
||||
leagueId,
|
||||
driverId,
|
||||
role: 'member',
|
||||
status: 'active' as MembershipStatus,
|
||||
joinedAt: new Date('2024-01-01'),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestRaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
|
||||
raceId: string | null = null;
|
||||
driverIds: string[] = [];
|
||||
|
||||
reset(): void {
|
||||
this.raceId = null;
|
||||
this.driverIds = [];
|
||||
}
|
||||
|
||||
present(input: RaceRegistrationsResultDTO) {
|
||||
this.driverIds = input.registeredDriverIds;
|
||||
this.raceId = null;
|
||||
return {
|
||||
registeredDriverIds: input.registeredDriverIds,
|
||||
count: input.registeredDriverIds.length,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel() {
|
||||
return {
|
||||
registeredDriverIds: this.driverIds,
|
||||
count: this.driverIds.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryTeamRepository implements ITeamRepository {
|
||||
private teams: Team[] = [];
|
||||
|
||||
async findById(id: string): Promise<Team | null> {
|
||||
return this.teams.find((t) => t.id === id) || null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Team[]> {
|
||||
return [...this.teams];
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<Team[]> {
|
||||
return this.teams.filter((t) => t.leagues.includes(leagueId));
|
||||
}
|
||||
|
||||
async create(team: Team): Promise<Team> {
|
||||
this.teams.push(team);
|
||||
return team;
|
||||
}
|
||||
|
||||
async update(team: Team): Promise<Team> {
|
||||
const index = this.teams.findIndex((t) => t.id === team.id);
|
||||
if (index >= 0) {
|
||||
this.teams[index] = team;
|
||||
} else {
|
||||
this.teams.push(team);
|
||||
}
|
||||
return team;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.teams = this.teams.filter((t) => t.id !== id);
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
return this.teams.some((t) => t.id === id);
|
||||
}
|
||||
|
||||
seedTeam(team: Team): void {
|
||||
this.teams.push(team);
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryTeamMembershipRepository implements ITeamMembershipRepository {
|
||||
private memberships: TeamMembership[] = [];
|
||||
private joinRequests: TeamJoinRequest[] = [];
|
||||
|
||||
async getMembership(teamId: string, driverId: string): Promise<TeamMembership | null> {
|
||||
return (
|
||||
this.memberships.find(
|
||||
(m) => m.teamId === teamId && m.driverId === driverId,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
async getActiveMembershipForDriver(driverId: string): Promise<TeamMembership | null> {
|
||||
return (
|
||||
this.memberships.find(
|
||||
(m) => m.driverId === driverId && m.status === 'active',
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
async getTeamMembers(teamId: string): Promise<TeamMembership[]> {
|
||||
return this.memberships.filter(
|
||||
(m) => m.teamId === teamId && m.status === 'active',
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
if (index >= 0) {
|
||||
this.memberships[index] = membership;
|
||||
} else {
|
||||
this.memberships.push(membership);
|
||||
}
|
||||
return membership;
|
||||
}
|
||||
|
||||
async removeMembership(teamId: string, driverId: string): Promise<void> {
|
||||
this.memberships = this.memberships.filter(
|
||||
(m) => !(m.teamId === teamId && m.driverId === driverId),
|
||||
);
|
||||
}
|
||||
|
||||
async getJoinRequests(teamId: string): Promise<TeamJoinRequest[]> {
|
||||
// For these tests we ignore teamId and return all,
|
||||
// allowing use-cases to look up by request ID only.
|
||||
return [...this.joinRequests];
|
||||
}
|
||||
|
||||
async saveJoinRequest(request: TeamJoinRequest): Promise<TeamJoinRequest> {
|
||||
const index = this.joinRequests.findIndex((r) => r.id === request.id);
|
||||
if (index >= 0) {
|
||||
this.joinRequests[index] = request;
|
||||
} else {
|
||||
this.joinRequests.push(request);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
async removeJoinRequest(requestId: string): Promise<void> {
|
||||
this.joinRequests = this.joinRequests.filter((r) => r.id !== requestId);
|
||||
}
|
||||
|
||||
seedMembership(membership: TeamMembership): void {
|
||||
this.memberships.push(membership);
|
||||
}
|
||||
|
||||
seedJoinRequest(request: TeamJoinRequest): void {
|
||||
this.joinRequests.push(request);
|
||||
}
|
||||
|
||||
getAllMemberships(): TeamMembership[] {
|
||||
return [...this.memberships];
|
||||
}
|
||||
|
||||
getAllJoinRequests(): TeamJoinRequest[] {
|
||||
return [...this.joinRequests];
|
||||
}
|
||||
|
||||
async countByTeamId(teamId: string): Promise<number> {
|
||||
return this.memberships.filter((m) => m.teamId === teamId).length;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Racing application use-cases - registrations', () => {
|
||||
let registrationRepo: InMemoryRaceRegistrationRepository;
|
||||
let membershipRepo: InMemoryLeagueMembershipRepositoryForRegistrations;
|
||||
let registerForRace: RegisterForRaceUseCase;
|
||||
let withdrawFromRace: WithdrawFromRaceUseCase;
|
||||
let isDriverRegistered: IsDriverRegisteredForRaceUseCase;
|
||||
let getRaceRegistrations: GetRaceRegistrationsUseCase;
|
||||
let logger: Logger;
|
||||
let raceRegistrationsPresenter: TestRaceRegistrationsPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
registrationRepo = new InMemoryRaceRegistrationRepository();
|
||||
membershipRepo = new InMemoryLeagueMembershipRepositoryForRegistrations();
|
||||
|
||||
registerForRace = new RegisterForRaceUseCase(registrationRepo, membershipRepo);
|
||||
withdrawFromRace = new WithdrawFromRaceUseCase(registrationRepo);
|
||||
logger = { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() };
|
||||
isDriverRegistered = new IsDriverRegisteredForRaceUseCase(
|
||||
registrationRepo,
|
||||
logger,
|
||||
);
|
||||
raceRegistrationsPresenter = new TestRaceRegistrationsPresenter();
|
||||
getRaceRegistrations = new GetRaceRegistrationsUseCase(registrationRepo);
|
||||
});
|
||||
|
||||
it('registers an active league member for a race and tracks registration', async () => {
|
||||
const raceId = 'race-1';
|
||||
const leagueId = 'league-1';
|
||||
const driverId = 'driver-1';
|
||||
|
||||
membershipRepo.seedActiveMembership(leagueId, driverId);
|
||||
|
||||
await registerForRace.execute({ raceId, leagueId, driverId });
|
||||
|
||||
const result = await isDriverRegistered.execute({ raceId, driverId });
|
||||
expect(result.isOk()).toBe(true);
|
||||
const status = result.unwrap();
|
||||
expect(status.isRegistered).toBe(true);
|
||||
expect(status.raceId).toBe(raceId);
|
||||
expect(status.driverId).toBe(driverId);
|
||||
|
||||
await getRaceRegistrations.execute({ raceId }, raceRegistrationsPresenter);
|
||||
expect(raceRegistrationsPresenter.driverIds).toContain(driverId);
|
||||
});
|
||||
|
||||
it('throws when registering a non-member for a race', async () => {
|
||||
const raceId = 'race-1';
|
||||
const leagueId = 'league-1';
|
||||
const driverId = 'driver-1';
|
||||
|
||||
await expect(
|
||||
registerForRace.execute({ raceId, leagueId, driverId }),
|
||||
).rejects.toThrow('Must be an active league member to register for races');
|
||||
});
|
||||
|
||||
it('withdraws a registration and reflects state in queries', async () => {
|
||||
const raceId = 'race-1';
|
||||
const leagueId = 'league-1';
|
||||
const driverId = 'driver-1';
|
||||
|
||||
membershipRepo.seedActiveMembership(leagueId, driverId);
|
||||
await registerForRace.execute({ raceId, leagueId, driverId });
|
||||
|
||||
await withdrawFromRace.execute({ raceId, driverId });
|
||||
|
||||
const result = await isDriverRegistered.execute({ raceId, driverId });
|
||||
expect(result.isOk()).toBe(true);
|
||||
const status = result.unwrap();
|
||||
expect(status.isRegistered).toBe(false);
|
||||
|
||||
await getRaceRegistrations.execute({ raceId }, raceRegistrationsPresenter);
|
||||
expect(raceRegistrationsPresenter.driverIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Racing application use-cases - teams', () => {
|
||||
let teamRepo: InMemoryTeamRepository;
|
||||
let membershipRepo: InMemoryTeamMembershipRepository;
|
||||
|
||||
let createTeam: CreateTeamUseCase;
|
||||
let joinTeam: JoinTeamUseCase;
|
||||
let leaveTeam: LeaveTeamUseCase;
|
||||
let approveJoin: ApproveTeamJoinRequestUseCase;
|
||||
let rejectJoin: RejectTeamJoinRequestUseCase;
|
||||
let updateTeamUseCase: UpdateTeamUseCase;
|
||||
let getAllTeamsUseCase: GetAllTeamsUseCase;
|
||||
let getTeamDetailsUseCase: GetTeamDetailsUseCase;
|
||||
let getTeamMembersUseCase: GetTeamMembersUseCase;
|
||||
let getTeamJoinRequestsUseCase: GetTeamJoinRequestsUseCase;
|
||||
let getDriverTeamUseCase: GetDriverTeamUseCase;
|
||||
|
||||
class FakeDriverRepository {
|
||||
async findById(driverId: string): Promise<Driver | null> {
|
||||
return Driver.create({ id: driverId, iracingId: '123', name: `Driver ${driverId}`, country: 'US' });
|
||||
}
|
||||
|
||||
async findByIRacingId(id: string): Promise<Driver | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Driver[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async create(driver: Driver): Promise<Driver> {
|
||||
return driver;
|
||||
}
|
||||
|
||||
async update(driver: Driver): Promise<Driver> {
|
||||
return driver;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async existsByIRacingId(iracingId: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<Driver[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByTeamId(teamId: string): Promise<Driver[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class FakeImageService {
|
||||
getDriverAvatar(driverId: string): string {
|
||||
return `https://example.com/avatar/${driverId}.png`;
|
||||
}
|
||||
|
||||
getTeamLogo(teamId: string): string {
|
||||
return `https://example.com/logo/${teamId}.png`;
|
||||
}
|
||||
|
||||
getLeagueCover(leagueId: string): string {
|
||||
return `https://example.com/cover/${leagueId}.png`;
|
||||
}
|
||||
|
||||
getLeagueLogo(leagueId: string): string {
|
||||
return `https://example.com/logo/${leagueId}.png`;
|
||||
}
|
||||
}
|
||||
|
||||
class TestAllTeamsPresenter implements IAllTeamsPresenter {
|
||||
private viewModel: AllTeamsViewModel | null = null;
|
||||
|
||||
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 as unknown).specialization,
|
||||
region: (team as unknown).region,
|
||||
languages: (team as unknown).languages,
|
||||
})),
|
||||
totalCount: input.teams.length,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): AllTeamsViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
get teams(): unknown[] {
|
||||
return this.viewModel?.teams ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestTeamDetailsPresenter implements ITeamDetailsPresenter {
|
||||
viewModel: any = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: any): void {
|
||||
this.viewModel = input;
|
||||
}
|
||||
|
||||
getViewModel(): any {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
class TestTeamMembersPresenter implements ITeamMembersPresenter {
|
||||
private viewModel: TeamMembersViewModel | null = null;
|
||||
|
||||
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 as unknown) === 'owner' ? 'owner' : (membership.role as unknown) === 'member' ? 'member' : (membership.role as unknown) === 'manager' ? 'manager' : (membership.role as unknown) === 'driver' ? 'member' : 'member') as "owner" | "member" | "manager",
|
||||
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 as unknown) === 'member').length;
|
||||
|
||||
this.viewModel = {
|
||||
members,
|
||||
totalCount: members.length,
|
||||
ownerCount,
|
||||
managerCount,
|
||||
memberCount,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): TeamMembersViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
get members(): unknown[] {
|
||||
return this.viewModel?.members ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestTeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
|
||||
private viewModel: TeamJoinRequestsViewModel | null = null;
|
||||
|
||||
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' as const,
|
||||
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(): unknown[] {
|
||||
return this.viewModel?.requests ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestDriverTeamPresenter implements IDriverTeamPresenter {
|
||||
viewModel: DriverTeamViewModel | null = null;
|
||||
|
||||
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,
|
||||
},
|
||||
membership: {
|
||||
role: (membership.role === 'owner' || membership.role === 'manager') ? membership.role : 'member' as "owner" | "member" | "manager",
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
isActive: membership.status === 'active',
|
||||
},
|
||||
isOwner,
|
||||
canManage,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): DriverTeamViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
let allTeamsPresenter: TestAllTeamsPresenter;
|
||||
let teamDetailsPresenter: TestTeamDetailsPresenter;
|
||||
let teamMembersPresenter: TestTeamMembersPresenter;
|
||||
let teamJoinRequestsPresenter: TestTeamJoinRequestsPresenter;
|
||||
let driverTeamPresenter: TestDriverTeamPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
teamRepo = new InMemoryTeamRepository();
|
||||
membershipRepo = new InMemoryTeamMembershipRepository();
|
||||
|
||||
createTeam = new CreateTeamUseCase(teamRepo, membershipRepo);
|
||||
joinTeam = new JoinTeamUseCase(teamRepo, membershipRepo);
|
||||
leaveTeam = new LeaveTeamUseCase(membershipRepo);
|
||||
approveJoin = new ApproveTeamJoinRequestUseCase(membershipRepo);
|
||||
rejectJoin = new RejectTeamJoinRequestUseCase(membershipRepo);
|
||||
updateTeamUseCase = new UpdateTeamUseCase(teamRepo, membershipRepo);
|
||||
|
||||
allTeamsPresenter = new TestAllTeamsPresenter();
|
||||
getAllTeamsUseCase = new GetAllTeamsUseCase(
|
||||
teamRepo,
|
||||
membershipRepo,
|
||||
);
|
||||
|
||||
teamDetailsPresenter = new TestTeamDetailsPresenter();
|
||||
getTeamDetailsUseCase = new GetTeamDetailsUseCase(
|
||||
teamRepo,
|
||||
membershipRepo,
|
||||
);
|
||||
|
||||
const driverRepository = new FakeDriverRepository();
|
||||
const imageService = new FakeImageService();
|
||||
|
||||
teamMembersPresenter = new TestTeamMembersPresenter();
|
||||
getTeamMembersUseCase = new GetTeamMembersUseCase(
|
||||
membershipRepo,
|
||||
driverRepository,
|
||||
imageService,
|
||||
teamMembersPresenter,
|
||||
);
|
||||
|
||||
teamJoinRequestsPresenter = new TestTeamJoinRequestsPresenter();
|
||||
getTeamJoinRequestsUseCase = new GetTeamJoinRequestsUseCase(
|
||||
membershipRepo,
|
||||
driverRepository,
|
||||
imageService,
|
||||
teamJoinRequestsPresenter,
|
||||
);
|
||||
|
||||
driverTeamPresenter = new TestDriverTeamPresenter();
|
||||
getDriverTeamUseCase = new GetDriverTeamUseCase(
|
||||
teamRepo,
|
||||
membershipRepo,
|
||||
driverTeamPresenter,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a team and assigns creator as active owner', async () => {
|
||||
const ownerId = 'driver-1';
|
||||
|
||||
const result = await createTeam.execute({
|
||||
name: 'Apex Racing',
|
||||
tag: 'APEX',
|
||||
description: 'Professional GT3 racing',
|
||||
ownerId,
|
||||
leagues: ['league-1'],
|
||||
});
|
||||
|
||||
expect(result.team.id).toBeDefined();
|
||||
expect(result.team.ownerId).toBe(ownerId);
|
||||
|
||||
const membership = await membershipRepo.getActiveMembershipForDriver(ownerId);
|
||||
expect(membership?.teamId).toBe(result.team.id);
|
||||
expect(membership?.role as TeamRole).toBe('owner');
|
||||
expect(membership?.status as TeamMembershipStatus).toBe('active');
|
||||
});
|
||||
|
||||
it('prevents driver from joining multiple teams and mirrors legacy error message', async () => {
|
||||
const ownerId = 'driver-1';
|
||||
const otherTeamId = 'team-2';
|
||||
|
||||
// Seed an existing active membership
|
||||
membershipRepo.seedMembership({
|
||||
teamId: otherTeamId,
|
||||
driverId: ownerId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date('2024-02-01'),
|
||||
});
|
||||
|
||||
await expect(
|
||||
joinTeam.execute({ teamId: 'team-1', driverId: ownerId }),
|
||||
).rejects.toThrow('Driver already belongs to a team');
|
||||
});
|
||||
|
||||
it('approves a join request and moves it into active membership', async () => {
|
||||
const teamId = 'team-1';
|
||||
const driverId = 'driver-2';
|
||||
|
||||
const request: TeamJoinRequest = {
|
||||
id: 'req-1',
|
||||
teamId,
|
||||
driverId,
|
||||
requestedAt: new Date('2024-03-01'),
|
||||
message: 'Let me in',
|
||||
};
|
||||
membershipRepo.seedJoinRequest(request);
|
||||
|
||||
await approveJoin.execute({ requestId: request.id });
|
||||
|
||||
const membership = await membershipRepo.getMembership(teamId, driverId);
|
||||
expect(membership).not.toBeNull();
|
||||
expect(membership?.status as TeamMembershipStatus).toBe('active');
|
||||
|
||||
const remainingRequests = await membershipRepo.getJoinRequests(teamId);
|
||||
expect(remainingRequests.find((r) => r.id === request.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects a join request and removes it', async () => {
|
||||
const teamId = 'team-1';
|
||||
const driverId = 'driver-2';
|
||||
|
||||
const request: TeamJoinRequest = {
|
||||
id: 'req-2',
|
||||
teamId,
|
||||
driverId,
|
||||
requestedAt: new Date('2024-03-02'),
|
||||
message: 'Please?',
|
||||
};
|
||||
membershipRepo.seedJoinRequest(request);
|
||||
|
||||
await rejectJoin.execute({ requestId: request.id });
|
||||
|
||||
const remainingRequests = await membershipRepo.getJoinRequests(teamId);
|
||||
expect(remainingRequests.find((r) => r.id === request.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updates team details when performed by owner or manager and reflects in queries', async () => {
|
||||
const ownerId = 'driver-1';
|
||||
const created = await createTeam.execute({
|
||||
name: 'Original Name',
|
||||
tag: 'ORIG',
|
||||
description: 'Original description',
|
||||
ownerId,
|
||||
leagues: [],
|
||||
});
|
||||
|
||||
await updateTeamUseCase.execute({
|
||||
teamId: created.team.id,
|
||||
updates: { name: 'Updated Name', description: 'Updated description' },
|
||||
updatedBy: ownerId,
|
||||
});
|
||||
|
||||
await getTeamDetailsUseCase.execute({ teamId: created.team.id, driverId: ownerId }, teamDetailsPresenter);
|
||||
|
||||
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 () => {
|
||||
const ownerId = 'driver-1';
|
||||
|
||||
const { team } = await createTeam.execute({
|
||||
name: 'Apex Racing',
|
||||
tag: 'APEX',
|
||||
description: 'Professional GT3 racing',
|
||||
ownerId,
|
||||
leagues: [],
|
||||
});
|
||||
|
||||
await getDriverTeamUseCase.execute({ driverId: ownerId }, driverTeamPresenter);
|
||||
const result = driverTeamPresenter.viewModel;
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.team.id).toBe(team.id);
|
||||
expect(result?.membership.isActive).toBe(true);
|
||||
expect(result?.isOwner).toBe(true);
|
||||
});
|
||||
|
||||
it('lists all teams and members via queries after multiple operations', async () => {
|
||||
const ownerId = 'driver-1';
|
||||
const otherDriverId = 'driver-2';
|
||||
|
||||
const { team } = await createTeam.execute({
|
||||
name: 'Apex Racing',
|
||||
tag: 'APEX',
|
||||
description: 'Professional GT3 racing',
|
||||
ownerId,
|
||||
leagues: [],
|
||||
});
|
||||
|
||||
await joinTeam.execute({ teamId: team.id, driverId: otherDriverId });
|
||||
|
||||
await getAllTeamsUseCase.execute(undefined as void, allTeamsPresenter);
|
||||
expect(allTeamsPresenter.teams.length).toBe(1);
|
||||
|
||||
await getTeamMembersUseCase.execute({ teamId: team.id }, teamMembersPresenter);
|
||||
const memberIds = teamMembersPresenter.members.map((m) => m.driverId).sort();
|
||||
expect(memberIds).toEqual([ownerId, otherDriverId].sort());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user