801 lines
26 KiB
TypeScript
801 lines
26 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
|
|
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
|
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
|
import type { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
|
import type { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
|
|
import type { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
|
import {
|
|
LeagueMembership,
|
|
type MembershipStatus,
|
|
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
|
import type {
|
|
Team,
|
|
TeamMembership,
|
|
TeamMembershipStatus,
|
|
TeamRole,
|
|
TeamJoinRequest,
|
|
} from '@gridpilot/racing/domain/entities/Team';
|
|
|
|
import { RegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/RegisterForRaceUseCase';
|
|
import { WithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/WithdrawFromRaceUseCase';
|
|
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';
|
|
import { LeaveTeamUseCase } from '@gridpilot/racing/application/use-cases/LeaveTeamUseCase';
|
|
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 { 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,
|
|
AllTeamsResultDTO,
|
|
AllTeamsViewModel,
|
|
} from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
|
|
import type { ITeamDetailsPresenter } from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
|
|
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';
|
|
import type { RaceRegistrationsResultDTO } from '@gridpilot/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 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[] = [];
|
|
|
|
reset(): void {
|
|
this.raceId = null;
|
|
this.driverIds = [];
|
|
}
|
|
|
|
present(input: RaceRegistrationsResultDTO): void {
|
|
this.driverIds = input.registeredDriverIds;
|
|
this.raceId = null;
|
|
}
|
|
}
|
|
|
|
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 driverRegistrationPresenter: TestDriverRegistrationStatusPresenter;
|
|
let raceRegistrationsPresenter: TestRaceRegistrationsPresenter;
|
|
|
|
beforeEach(() => {
|
|
registrationRepo = new InMemoryRaceRegistrationRepository();
|
|
membershipRepo = new InMemoryLeagueMembershipRepositoryForRegistrations();
|
|
|
|
registerForRace = new RegisterForRaceUseCase(registrationRepo, membershipRepo);
|
|
withdrawFromRace = new WithdrawFromRaceUseCase(registrationRepo);
|
|
driverRegistrationPresenter = new TestDriverRegistrationStatusPresenter();
|
|
isDriverRegistered = new IsDriverRegisteredForRaceUseCase(
|
|
registrationRepo,
|
|
driverRegistrationPresenter,
|
|
);
|
|
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 });
|
|
|
|
await isDriverRegistered.execute({ raceId, driverId });
|
|
expect(driverRegistrationPresenter.isRegistered).toBe(true);
|
|
expect(driverRegistrationPresenter.raceId).toBe(raceId);
|
|
expect(driverRegistrationPresenter.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 });
|
|
|
|
await isDriverRegistered.execute({ raceId, driverId });
|
|
expect(driverRegistrationPresenter.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<{ 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 {
|
|
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.specialization,
|
|
region: team.region,
|
|
languages: team.languages,
|
|
})),
|
|
totalCount: input.teams.length,
|
|
};
|
|
}
|
|
|
|
getViewModel(): AllTeamsViewModel | null {
|
|
return this.viewModel;
|
|
}
|
|
|
|
get teams(): any[] {
|
|
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,
|
|
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 {
|
|
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',
|
|
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 {
|
|
private 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,
|
|
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;
|
|
}
|
|
}
|
|
|
|
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,
|
|
allTeamsPresenter,
|
|
);
|
|
|
|
teamDetailsPresenter = new TestTeamDetailsPresenter();
|
|
getTeamDetailsUseCase = new GetTeamDetailsUseCase(
|
|
teamRepo,
|
|
membershipRepo,
|
|
teamDetailsPresenter,
|
|
);
|
|
|
|
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());
|
|
});
|
|
}); |