integration tests
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
This commit is contained in:
94
tests/integration/teams/TeamsTestContext.ts
Normal file
94
tests/integration/teams/TeamsTestContext.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryTeamStatsRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamStatsRepository';
|
||||
import { CreateTeamUseCase } from '../../../core/racing/application/use-cases/CreateTeamUseCase';
|
||||
import { JoinTeamUseCase } from '../../../core/racing/application/use-cases/JoinTeamUseCase';
|
||||
import { LeaveTeamUseCase } from '../../../core/racing/application/use-cases/LeaveTeamUseCase';
|
||||
import { GetTeamMembershipUseCase } from '../../../core/racing/application/use-cases/GetTeamMembershipUseCase';
|
||||
import { GetTeamMembersUseCase } from '../../../core/racing/application/use-cases/GetTeamMembersUseCase';
|
||||
import { GetTeamJoinRequestsUseCase } from '../../../core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
|
||||
import { ApproveTeamJoinRequestUseCase } from '../../../core/racing/application/use-cases/ApproveTeamJoinRequestUseCase';
|
||||
import { UpdateTeamUseCase } from '../../../core/racing/application/use-cases/UpdateTeamUseCase';
|
||||
import { GetTeamDetailsUseCase } from '../../../core/racing/application/use-cases/GetTeamDetailsUseCase';
|
||||
import { GetTeamsLeaderboardUseCase } from '../../../core/racing/application/use-cases/GetTeamsLeaderboardUseCase';
|
||||
import { GetAllTeamsUseCase } from '../../../core/racing/application/use-cases/GetAllTeamsUseCase';
|
||||
|
||||
export class TeamsTestContext {
|
||||
public readonly logger: Logger;
|
||||
public readonly teamRepository: InMemoryTeamRepository;
|
||||
public readonly membershipRepository: InMemoryTeamMembershipRepository;
|
||||
public readonly driverRepository: InMemoryDriverRepository;
|
||||
public readonly statsRepository: InMemoryTeamStatsRepository;
|
||||
|
||||
constructor() {
|
||||
this.logger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
this.teamRepository = new InMemoryTeamRepository(this.logger);
|
||||
this.membershipRepository = new InMemoryTeamMembershipRepository(this.logger);
|
||||
this.driverRepository = new InMemoryDriverRepository(this.logger);
|
||||
this.statsRepository = new InMemoryTeamStatsRepository(this.logger);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.teamRepository.clear();
|
||||
this.membershipRepository.clear();
|
||||
this.driverRepository.clear();
|
||||
this.statsRepository.clear();
|
||||
}
|
||||
|
||||
public createCreateTeamUseCase(): CreateTeamUseCase {
|
||||
return new CreateTeamUseCase(this.teamRepository, this.membershipRepository, this.logger);
|
||||
}
|
||||
|
||||
public createJoinTeamUseCase(): JoinTeamUseCase {
|
||||
return new JoinTeamUseCase(this.teamRepository, this.membershipRepository, this.logger);
|
||||
}
|
||||
|
||||
public createLeaveTeamUseCase(): LeaveTeamUseCase {
|
||||
return new LeaveTeamUseCase(this.teamRepository, this.membershipRepository, this.logger);
|
||||
}
|
||||
|
||||
public createGetTeamMembershipUseCase(): GetTeamMembershipUseCase {
|
||||
return new GetTeamMembershipUseCase(this.membershipRepository, this.logger);
|
||||
}
|
||||
|
||||
public createGetTeamMembersUseCase(): GetTeamMembersUseCase {
|
||||
return new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, this.logger);
|
||||
}
|
||||
|
||||
public createGetTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
|
||||
return new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository);
|
||||
}
|
||||
|
||||
public createApproveTeamJoinRequestUseCase(): ApproveTeamJoinRequestUseCase {
|
||||
return new ApproveTeamJoinRequestUseCase(this.membershipRepository);
|
||||
}
|
||||
|
||||
public createUpdateTeamUseCase(): UpdateTeamUseCase {
|
||||
return new UpdateTeamUseCase(this.teamRepository, this.membershipRepository);
|
||||
}
|
||||
|
||||
public createGetTeamDetailsUseCase(): GetTeamDetailsUseCase {
|
||||
return new GetTeamDetailsUseCase(this.teamRepository, this.membershipRepository);
|
||||
}
|
||||
|
||||
public createGetTeamsLeaderboardUseCase(getDriverStats: (driverId: string) => any): GetTeamsLeaderboardUseCase {
|
||||
return new GetTeamsLeaderboardUseCase(
|
||||
this.teamRepository,
|
||||
this.membershipRepository,
|
||||
getDriverStats,
|
||||
this.logger
|
||||
);
|
||||
}
|
||||
|
||||
public createGetAllTeamsUseCase(): GetAllTeamsUseCase {
|
||||
return new GetAllTeamsUseCase(this.teamRepository, this.membershipRepository, this.statsRepository, this.logger);
|
||||
}
|
||||
}
|
||||
103
tests/integration/teams/admin/update-team.test.ts
Normal file
103
tests/integration/teams/admin/update-team.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TeamsTestContext } from '../TeamsTestContext';
|
||||
import { Team } from '../../../../core/racing/domain/entities/Team';
|
||||
|
||||
describe('UpdateTeamUseCase', () => {
|
||||
const context = new TeamsTestContext();
|
||||
const updateTeamUseCase = context.createUpdateTeamUseCase();
|
||||
|
||||
beforeEach(() => {
|
||||
context.clear();
|
||||
});
|
||||
|
||||
describe('Success Path', () => {
|
||||
it('should update team details when called by owner', async () => {
|
||||
const teamId = 't1';
|
||||
const ownerId = 'o1';
|
||||
const team = Team.create({ id: teamId, name: 'Old Name', tag: 'OLD', description: 'Old Desc', ownerId, leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: ownerId,
|
||||
updates: {
|
||||
name: 'New Name',
|
||||
tag: 'NEW',
|
||||
description: 'New Desc'
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: updatedTeam } = result.unwrap();
|
||||
expect(updatedTeam.name.toString()).toBe('New Name');
|
||||
expect(updatedTeam.tag.toString()).toBe('NEW');
|
||||
expect(updatedTeam.description.toString()).toBe('New Desc');
|
||||
|
||||
const savedTeam = await context.teamRepository.findById(teamId);
|
||||
expect(savedTeam?.name.toString()).toBe('New Name');
|
||||
});
|
||||
|
||||
it('should update team details when called by manager', async () => {
|
||||
const teamId = 't2';
|
||||
const managerId = 'm2';
|
||||
const team = Team.create({ id: teamId, name: 'Team 2', tag: 'T2', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: managerId,
|
||||
role: 'manager',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: managerId,
|
||||
updates: {
|
||||
name: 'Updated by Manager'
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: updatedTeam } = result.unwrap();
|
||||
expect(updatedTeam.name.toString()).toBe('Updated by Manager');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should reject update when called by regular member', async () => {
|
||||
const teamId = 't3';
|
||||
const memberId = 'd3';
|
||||
const team = Team.create({ id: teamId, name: 'Team 3', tag: 'T3', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: memberId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: memberId,
|
||||
updates: {
|
||||
name: 'Unauthorized Update'
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('PERMISSION_DENIED');
|
||||
});
|
||||
});
|
||||
});
|
||||
193
tests/integration/teams/creation/create-team.test.ts
Normal file
193
tests/integration/teams/creation/create-team.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TeamsTestContext } from '../TeamsTestContext';
|
||||
import { Team } from '../../../../core/racing/domain/entities/Team';
|
||||
|
||||
describe('CreateTeamUseCase', () => {
|
||||
const context = new TeamsTestContext();
|
||||
const createTeamUseCase = context.createCreateTeamUseCase();
|
||||
|
||||
beforeEach(() => {
|
||||
context.clear();
|
||||
});
|
||||
|
||||
describe('Success Path', () => {
|
||||
it('should create a team with all required fields', async () => {
|
||||
const driverId = 'd1';
|
||||
const leagueId = 'l1';
|
||||
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
|
||||
expect(team.name.toString()).toBe('Test Team');
|
||||
expect(team.tag.toString()).toBe('TT');
|
||||
expect(team.description.toString()).toBe('A test team');
|
||||
expect(team.ownerId.toString()).toBe(driverId);
|
||||
expect(team.leagues.map(l => l.toString())).toContain(leagueId);
|
||||
|
||||
const savedTeam = await context.teamRepository.findById(team.id.toString());
|
||||
expect(savedTeam).toBeDefined();
|
||||
expect(savedTeam?.name.toString()).toBe('Test Team');
|
||||
|
||||
const membership = await context.membershipRepository.getMembership(team.id.toString(), driverId);
|
||||
expect(membership).toBeDefined();
|
||||
expect(membership?.role).toBe('owner');
|
||||
expect(membership?.status).toBe('active');
|
||||
});
|
||||
|
||||
it('should create a team with optional description', async () => {
|
||||
const driverId = 'd2';
|
||||
const leagueId = 'l2';
|
||||
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Team With Description',
|
||||
tag: 'TWD',
|
||||
description: 'This team has a detailed description',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.description.toString()).toBe('This team has a detailed description');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should reject team creation with empty team name', async () => {
|
||||
const driverId = 'd4';
|
||||
const leagueId = 'l4';
|
||||
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: '',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('REPOSITORY_ERROR');
|
||||
});
|
||||
|
||||
it('should reject team creation with empty description', async () => {
|
||||
const driverId = 'd3';
|
||||
const leagueId = 'l3';
|
||||
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Minimal Team',
|
||||
tag: 'MT',
|
||||
description: '',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('REPOSITORY_ERROR');
|
||||
});
|
||||
|
||||
it('should reject team creation when driver already belongs to a team', async () => {
|
||||
const driverId = 'd6';
|
||||
const leagueId = 'l6';
|
||||
|
||||
const existingTeam = Team.create({ id: 'existing', name: 'Existing Team', tag: 'ET', description: 'Existing', ownerId: driverId, leagues: [] });
|
||||
await context.teamRepository.create(existingTeam);
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId: 'existing',
|
||||
driverId: driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'New Team',
|
||||
tag: 'NT',
|
||||
description: 'A new team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
expect(error.details.message).toContain('already belongs to a team');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Business Logic', () => {
|
||||
it('should set the creating driver as team captain', async () => {
|
||||
const driverId = 'd10';
|
||||
const leagueId = 'l10';
|
||||
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Captain Team',
|
||||
tag: 'CT',
|
||||
description: 'A team with captain',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
|
||||
const membership = await context.membershipRepository.getMembership(team.id.toString(), driverId);
|
||||
expect(membership).toBeDefined();
|
||||
expect(membership?.role).toBe('owner');
|
||||
});
|
||||
|
||||
it('should generate unique team ID', async () => {
|
||||
const driverId = 'd11';
|
||||
const leagueId = 'l11';
|
||||
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Unique Team',
|
||||
tag: 'UT',
|
||||
description: 'A unique team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.id.toString()).toBeDefined();
|
||||
expect(team.id.toString().length).toBeGreaterThan(0);
|
||||
|
||||
const existingTeam = await context.teamRepository.findById(team.id.toString());
|
||||
expect(existingTeam).toBeDefined();
|
||||
expect(existingTeam?.id.toString()).toBe(team.id.toString());
|
||||
});
|
||||
|
||||
it('should set creation timestamp', async () => {
|
||||
const driverId = 'd12';
|
||||
const leagueId = 'l12';
|
||||
|
||||
const beforeCreate = new Date();
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Timestamp Team',
|
||||
tag: 'TT',
|
||||
description: 'A team with timestamp',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
const afterCreate = new Date();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.createdAt).toBeDefined();
|
||||
|
||||
const createdAt = team.createdAt.toDate();
|
||||
expect(createdAt.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAt.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
});
|
||||
});
|
||||
});
|
||||
83
tests/integration/teams/detail/get-team-details.test.ts
Normal file
83
tests/integration/teams/detail/get-team-details.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TeamsTestContext } from '../TeamsTestContext';
|
||||
import { Team } from '../../../../core/racing/domain/entities/Team';
|
||||
|
||||
describe('GetTeamDetailsUseCase', () => {
|
||||
const context = new TeamsTestContext();
|
||||
const getTeamDetailsUseCase = context.createGetTeamDetailsUseCase();
|
||||
|
||||
beforeEach(() => {
|
||||
context.clear();
|
||||
});
|
||||
|
||||
describe('Success Path', () => {
|
||||
it('should retrieve team detail with membership and management permissions for owner', async () => {
|
||||
const teamId = 't1';
|
||||
const ownerId = 'd1';
|
||||
const team = Team.create({ id: teamId, name: 'Team 1', tag: 'T1', description: 'Desc', ownerId, leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId, driverId: ownerId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.team.id.toString()).toBe(teamId);
|
||||
expect(data.membership?.role).toBe('owner');
|
||||
expect(data.canManage).toBe(true);
|
||||
});
|
||||
|
||||
it('should retrieve team detail for a non-member', async () => {
|
||||
const teamId = 't2';
|
||||
const team = Team.create({ id: teamId, name: 'Team 2', tag: 'T2', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId, driverId: 'non-member' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.team.id.toString()).toBe(teamId);
|
||||
expect(data.membership).toBeNull();
|
||||
expect(data.canManage).toBe(false);
|
||||
});
|
||||
|
||||
it('should retrieve team detail for a regular member', async () => {
|
||||
const teamId = 't3';
|
||||
const memberId = 'd3';
|
||||
const team = Team.create({ id: teamId, name: 'Team 3', tag: 'T3', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: memberId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId, driverId: memberId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.team.id.toString()).toBe(teamId);
|
||||
expect(data.membership?.role).toBe('driver');
|
||||
expect(data.canManage).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should throw error when team does not exist', async () => {
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId: 'nonexistent', driverId: 'any' });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('TEAM_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TeamsTestContext } from '../TeamsTestContext';
|
||||
import { Team } from '../../../../core/racing/domain/entities/Team';
|
||||
|
||||
describe('GetTeamsLeaderboardUseCase', () => {
|
||||
const context = new TeamsTestContext();
|
||||
|
||||
// Mock driver stats provider
|
||||
const getDriverStats = (driverId: string) => {
|
||||
const statsMap: Record<string, { rating: number, wins: number, totalRaces: number }> = {
|
||||
'd1': { rating: 2000, wins: 10, totalRaces: 50 },
|
||||
'd2': { rating: 1500, wins: 5, totalRaces: 30 },
|
||||
'd3': { rating: 1000, wins: 2, totalRaces: 20 },
|
||||
};
|
||||
return statsMap[driverId] || null;
|
||||
};
|
||||
|
||||
const getTeamsLeaderboardUseCase = context.createGetTeamsLeaderboardUseCase(getDriverStats);
|
||||
|
||||
beforeEach(() => {
|
||||
context.clear();
|
||||
});
|
||||
|
||||
describe('Success Path', () => {
|
||||
it('should retrieve ranked team leaderboard with performance metrics', async () => {
|
||||
const team1 = Team.create({ id: 't1', name: 'Pro Team', tag: 'PRO', description: 'Desc', ownerId: 'o1', leagues: [] });
|
||||
const team2 = Team.create({ id: 't2', name: 'Am Team', tag: 'AM', description: 'Desc', ownerId: 'o2', leagues: [] });
|
||||
await context.teamRepository.create(team1);
|
||||
await context.teamRepository.create(team2);
|
||||
|
||||
await context.membershipRepository.saveMembership({ teamId: 't1', driverId: 'd1', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
await context.membershipRepository.saveMembership({ teamId: 't2', driverId: 'd3', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
|
||||
const result = await getTeamsLeaderboardUseCase.execute({ leagueId: 'any' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { items, topItems } = result.unwrap();
|
||||
expect(items).toHaveLength(2);
|
||||
|
||||
expect(topItems[0]?.team.id.toString()).toBe('t1');
|
||||
expect(topItems[0]?.rating).toBe(2000);
|
||||
expect(topItems[1]?.team.id.toString()).toBe('t2');
|
||||
expect(topItems[1]?.rating).toBe(1000);
|
||||
});
|
||||
|
||||
it('should handle empty leaderboard', async () => {
|
||||
const result = await getTeamsLeaderboardUseCase.execute({ leagueId: 'any' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { items } = result.unwrap();
|
||||
expect(items).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
56
tests/integration/teams/list/get-all-teams.test.ts
Normal file
56
tests/integration/teams/list/get-all-teams.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TeamsTestContext } from '../TeamsTestContext';
|
||||
import { Team } from '../../../../core/racing/domain/entities/Team';
|
||||
|
||||
describe('GetAllTeamsUseCase', () => {
|
||||
const context = new TeamsTestContext();
|
||||
const getAllTeamsUseCase = context.createGetAllTeamsUseCase();
|
||||
|
||||
beforeEach(() => {
|
||||
context.clear();
|
||||
});
|
||||
|
||||
describe('Success Path', () => {
|
||||
it('should retrieve complete teams list with all teams and enrichment', async () => {
|
||||
const team1 = Team.create({ id: 't1', name: 'Team 1', tag: 'T1', description: 'Desc 1', ownerId: 'o1', leagues: [] });
|
||||
const team2 = Team.create({ id: 't2', name: 'Team 2', tag: 'T2', description: 'Desc 2', ownerId: 'o2', leagues: [] });
|
||||
await context.teamRepository.create(team1);
|
||||
await context.teamRepository.create(team2);
|
||||
|
||||
await context.membershipRepository.saveMembership({ teamId: 't1', driverId: 'd1', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
await context.membershipRepository.saveMembership({ teamId: 't1', driverId: 'd2', role: 'driver', status: 'active', joinedAt: new Date() });
|
||||
await context.membershipRepository.saveMembership({ teamId: 't2', driverId: 'd3', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
|
||||
await context.statsRepository.saveTeamStats('t1', {
|
||||
totalWins: 5,
|
||||
totalRaces: 20,
|
||||
rating: 1500,
|
||||
performanceLevel: 'intermediate',
|
||||
specialization: 'sprint',
|
||||
region: 'EU',
|
||||
languages: ['en'],
|
||||
isRecruiting: true
|
||||
});
|
||||
|
||||
const result = await getAllTeamsUseCase.execute({});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { teams, totalCount } = result.unwrap();
|
||||
expect(totalCount).toBe(2);
|
||||
|
||||
const enriched1 = teams.find(t => t.team.id.toString() === 't1');
|
||||
expect(enriched1?.memberCount).toBe(2);
|
||||
expect(enriched1?.totalWins).toBe(5);
|
||||
expect(enriched1?.rating).toBe(1500);
|
||||
});
|
||||
|
||||
it('should handle empty teams list', async () => {
|
||||
const result = await getAllTeamsUseCase.execute({});
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { teams, totalCount } = result.unwrap();
|
||||
expect(totalCount).toBe(0);
|
||||
expect(teams).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
203
tests/integration/teams/membership/team-membership.test.ts
Normal file
203
tests/integration/teams/membership/team-membership.test.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TeamsTestContext } from '../TeamsTestContext';
|
||||
import { Driver } from '../../../../core/racing/domain/entities/Driver';
|
||||
import { Team } from '../../../../core/racing/domain/entities/Team';
|
||||
|
||||
describe('Team Membership Use Cases', () => {
|
||||
const context = new TeamsTestContext();
|
||||
const joinTeamUseCase = context.createJoinTeamUseCase();
|
||||
const leaveTeamUseCase = context.createLeaveTeamUseCase();
|
||||
const getTeamMembershipUseCase = context.createGetTeamMembershipUseCase();
|
||||
const getTeamMembersUseCase = context.createGetTeamMembersUseCase();
|
||||
const getTeamJoinRequestsUseCase = context.createGetTeamJoinRequestsUseCase();
|
||||
const approveTeamJoinRequestUseCase = context.createApproveTeamJoinRequestUseCase();
|
||||
|
||||
beforeEach(() => {
|
||||
context.clear();
|
||||
});
|
||||
|
||||
describe('JoinTeamUseCase', () => {
|
||||
it('should create a join request for a team', async () => {
|
||||
const driverId = 'd1';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '1', name: 'Driver 1', country: 'US' });
|
||||
await context.driverRepository.create(driver);
|
||||
|
||||
const teamId = 't1';
|
||||
const team = Team.create({ id: teamId, name: 'Team 1', tag: 'T1', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
const result = await joinTeamUseCase.execute({ teamId, driverId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { membership } = result.unwrap();
|
||||
expect(membership.status).toBe('active');
|
||||
expect(membership.role).toBe('driver');
|
||||
|
||||
const savedMembership = await context.membershipRepository.getMembership(teamId, driverId);
|
||||
expect(savedMembership).toBeDefined();
|
||||
expect(savedMembership?.status).toBe('active');
|
||||
});
|
||||
|
||||
it('should reject join request when driver is already a member', async () => {
|
||||
const driverId = 'd3';
|
||||
const teamId = 't3';
|
||||
const team = Team.create({ id: teamId, name: 'Team 3', tag: 'T3', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await joinTeamUseCase.execute({ teamId, driverId });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
// JoinTeamUseCase returns ALREADY_IN_TEAM if driver is in ANY team,
|
||||
// and ALREADY_MEMBER if they are already in THIS team.
|
||||
// In this case, they are already in this team.
|
||||
expect(result.unwrapErr().code).toBe('ALREADY_IN_TEAM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LeaveTeamUseCase', () => {
|
||||
it('should allow driver to leave team', async () => {
|
||||
const driverId = 'd7';
|
||||
const teamId = 't7';
|
||||
const team = Team.create({ id: teamId, name: 'Team 7', tag: 'T7', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await leaveTeamUseCase.execute({ teamId, driverId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const savedMembership = await context.membershipRepository.getMembership(teamId, driverId);
|
||||
expect(savedMembership).toBeNull();
|
||||
});
|
||||
|
||||
it('should reject leave when driver is team owner', async () => {
|
||||
const driverId = 'd9';
|
||||
const teamId = 't9';
|
||||
const team = Team.create({ id: teamId, name: 'Team 9', tag: 'T9', description: 'Test Team', ownerId: driverId, leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await leaveTeamUseCase.execute({ teamId, driverId });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('OWNER_CANNOT_LEAVE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamMembershipUseCase', () => {
|
||||
it('should retrieve driver membership in team', async () => {
|
||||
const driverId = 'd10';
|
||||
const teamId = 't10';
|
||||
const team = Team.create({ id: teamId, name: 'Team 10', tag: 'T10', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
await context.membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await getTeamMembershipUseCase.execute({ teamId, driverId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { membership } = result.unwrap();
|
||||
expect(membership?.role).toBe('member');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamMembersUseCase', () => {
|
||||
it('should retrieve all team members', async () => {
|
||||
const teamId = 't12';
|
||||
const team = Team.create({ id: teamId, name: 'Team 12', tag: 'T12', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
const driver1 = Driver.create({ id: 'd12', iracingId: '12', name: 'Driver 12', country: 'US' });
|
||||
const driver2 = Driver.create({ id: 'd13', iracingId: '13', name: 'Driver 13', country: 'UK' });
|
||||
await context.driverRepository.create(driver1);
|
||||
await context.driverRepository.create(driver2);
|
||||
|
||||
await context.membershipRepository.saveMembership({ teamId, driverId: 'd12', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
await context.membershipRepository.saveMembership({ teamId, driverId: 'd13', role: 'driver', status: 'active', joinedAt: new Date() });
|
||||
|
||||
const result = await getTeamMembersUseCase.execute({ teamId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { members } = result.unwrap();
|
||||
expect(members).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamJoinRequestsUseCase', () => {
|
||||
it('should retrieve pending join requests', async () => {
|
||||
const teamId = 't14';
|
||||
const team = Team.create({ id: teamId, name: 'Team 14', tag: 'T14', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
const driver1 = Driver.create({ id: 'd14', iracingId: '14', name: 'Driver 14', country: 'US' });
|
||||
await context.driverRepository.create(driver1);
|
||||
|
||||
await context.membershipRepository.saveJoinRequest({
|
||||
id: 'jr2',
|
||||
teamId,
|
||||
driverId: 'd14',
|
||||
status: 'pending',
|
||||
requestedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await getTeamJoinRequestsUseCase.execute({ teamId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { joinRequests } = result.unwrap();
|
||||
expect(joinRequests).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApproveTeamJoinRequestUseCase', () => {
|
||||
it('should approve a pending join request', async () => {
|
||||
const teamId = 't16';
|
||||
const team = Team.create({ id: teamId, name: 'Team 16', tag: 'T16', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await context.teamRepository.create(team);
|
||||
|
||||
const driverId = 'd16';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '16', name: 'Driver 16', country: 'US' });
|
||||
await context.driverRepository.create(driver);
|
||||
|
||||
await context.membershipRepository.saveJoinRequest({
|
||||
id: 'jr4',
|
||||
teamId,
|
||||
driverId,
|
||||
status: 'pending',
|
||||
requestedAt: new Date()
|
||||
});
|
||||
|
||||
const result = await approveTeamJoinRequestUseCase.execute({ teamId, requestId: 'jr4' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const savedMembership = await context.membershipRepository.getMembership(teamId, driverId);
|
||||
expect(savedMembership?.status).toBe('active');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,201 +0,0 @@
|
||||
/**
|
||||
* Integration Test: Team Admin Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of team admin-related Use Cases:
|
||||
* - UpdateTeamUseCase: Admin updates team details
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { UpdateTeamUseCase } from '../../../core/racing/application/use-cases/UpdateTeamUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Team Admin Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let updateTeamUseCase: UpdateTeamUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
updateTeamUseCase = new UpdateTeamUseCase(teamRepository, membershipRepository);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
});
|
||||
|
||||
describe('UpdateTeamUseCase - Success Path', () => {
|
||||
it('should update team details when called by owner', async () => {
|
||||
// Scenario: Owner updates team details
|
||||
// Given: A team exists
|
||||
const teamId = 't1';
|
||||
const ownerId = 'o1';
|
||||
const team = Team.create({ id: teamId, name: 'Old Name', tag: 'OLD', description: 'Old Desc', ownerId, leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is the owner
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: UpdateTeamUseCase.execute() is called
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: ownerId,
|
||||
updates: {
|
||||
name: 'New Name',
|
||||
tag: 'NEW',
|
||||
description: 'New Desc'
|
||||
}
|
||||
});
|
||||
|
||||
// Then: The team should be updated successfully
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: updatedTeam } = result.unwrap();
|
||||
expect(updatedTeam.name.toString()).toBe('New Name');
|
||||
expect(updatedTeam.tag.toString()).toBe('NEW');
|
||||
expect(updatedTeam.description.toString()).toBe('New Desc');
|
||||
|
||||
// And: The changes should be in the repository
|
||||
const savedTeam = await teamRepository.findById(teamId);
|
||||
expect(savedTeam?.name.toString()).toBe('New Name');
|
||||
});
|
||||
|
||||
it('should update team details when called by manager', async () => {
|
||||
// Scenario: Manager updates team details
|
||||
// Given: A team exists
|
||||
const teamId = 't2';
|
||||
const managerId = 'm2';
|
||||
const team = Team.create({ id: teamId, name: 'Team 2', tag: 'T2', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is a manager
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: managerId,
|
||||
role: 'manager',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: UpdateTeamUseCase.execute() is called
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: managerId,
|
||||
updates: {
|
||||
name: 'Updated by Manager'
|
||||
}
|
||||
});
|
||||
|
||||
// Then: The team should be updated successfully
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: updatedTeam } = result.unwrap();
|
||||
expect(updatedTeam.name.toString()).toBe('Updated by Manager');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateTeamUseCase - Validation', () => {
|
||||
it('should reject update when called by regular member', async () => {
|
||||
// Scenario: Regular member tries to update team
|
||||
// Given: A team exists
|
||||
const teamId = 't3';
|
||||
const memberId = 'd3';
|
||||
const team = Team.create({ id: teamId, name: 'Team 3', tag: 'T3', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is a regular member
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: memberId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: UpdateTeamUseCase.execute() is called
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: memberId,
|
||||
updates: {
|
||||
name: 'Unauthorized Update'
|
||||
}
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('PERMISSION_DENIED');
|
||||
});
|
||||
|
||||
it('should reject update when called by non-member', async () => {
|
||||
// Scenario: Non-member tries to update team
|
||||
// Given: A team exists
|
||||
const teamId = 't4';
|
||||
const team = Team.create({ id: teamId, name: 'Team 4', tag: 'T4', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: UpdateTeamUseCase.execute() is called
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId,
|
||||
updatedBy: 'non-member',
|
||||
updates: {
|
||||
name: 'Unauthorized Update'
|
||||
}
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('PERMISSION_DENIED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateTeamUseCase - Error Handling', () => {
|
||||
it('should throw error when team does not exist', async () => {
|
||||
// Scenario: Non-existent team
|
||||
// Given: A driver exists who is a manager of some team
|
||||
const managerId = 'm5';
|
||||
await membershipRepository.saveMembership({
|
||||
teamId: 'some-team',
|
||||
driverId: managerId,
|
||||
role: 'manager',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: UpdateTeamUseCase.execute() is called with non-existent team ID
|
||||
const result = await updateTeamUseCase.execute({
|
||||
teamId: 'nonexistent',
|
||||
updatedBy: managerId,
|
||||
updates: {
|
||||
name: 'New Name'
|
||||
}
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('PERMISSION_DENIED'); // Because membership check fails first
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,403 +0,0 @@
|
||||
/**
|
||||
* Integration Test: Team Creation Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of team creation-related Use Cases:
|
||||
* - CreateTeamUseCase: Creates a new team with name, description, and leagues
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { CreateTeamUseCase } from '../../../core/racing/application/use-cases/CreateTeamUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Driver } from '../../../core/racing/domain/entities/Driver';
|
||||
import { League } from '../../../core/racing/domain/entities/League';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Team Creation Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let createTeamUseCase: CreateTeamUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
createTeamUseCase = new CreateTeamUseCase(teamRepository, membershipRepository, mockLogger);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
});
|
||||
|
||||
describe('CreateTeamUseCase - Success Path', () => {
|
||||
it('should create a team with all required fields', async () => {
|
||||
// Scenario: Team creation with complete information
|
||||
// Given: A driver exists
|
||||
const driverId = 'd1';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '1', name: 'John Doe', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l1';
|
||||
const league = League.create({ id: leagueId, name: 'League 1', description: 'Test League', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with valid command
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The team should be created successfully
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
|
||||
// And: The team should have the correct properties
|
||||
expect(team.name.toString()).toBe('Test Team');
|
||||
expect(team.tag.toString()).toBe('TT');
|
||||
expect(team.description.toString()).toBe('A test team');
|
||||
expect(team.ownerId.toString()).toBe(driverId);
|
||||
expect(team.leagues.map(l => l.toString())).toContain(leagueId);
|
||||
|
||||
// And: The team should be in the repository
|
||||
const savedTeam = await teamRepository.findById(team.id.toString());
|
||||
expect(savedTeam).toBeDefined();
|
||||
expect(savedTeam?.name.toString()).toBe('Test Team');
|
||||
|
||||
// And: The driver should have an owner membership
|
||||
const membership = await membershipRepository.getMembership(team.id.toString(), driverId);
|
||||
expect(membership).toBeDefined();
|
||||
expect(membership?.role).toBe('owner');
|
||||
expect(membership?.status).toBe('active');
|
||||
});
|
||||
|
||||
it('should create a team with optional description', async () => {
|
||||
// Scenario: Team creation with description
|
||||
// Given: A driver exists
|
||||
const driverId = 'd2';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '2', name: 'Jane Doe', country: 'UK' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l2';
|
||||
const league = League.create({ id: leagueId, name: 'League 2', description: 'Test League 2', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with description
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Team With Description',
|
||||
tag: 'TWD',
|
||||
description: 'This team has a detailed description',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The team should be created with the description
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.description.toString()).toBe('This team has a detailed description');
|
||||
});
|
||||
|
||||
it('should create a team with minimal required fields', async () => {
|
||||
// Scenario: Team creation with minimal information
|
||||
// Given: A driver exists
|
||||
const driverId = 'd3';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '3', name: 'Bob Smith', country: 'CA' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l3';
|
||||
const league = League.create({ id: leagueId, name: 'League 3', description: 'Test League 3', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with only required fields
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Minimal Team',
|
||||
tag: 'MT',
|
||||
description: '',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The team should be created with default values
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.name.toString()).toBe('Minimal Team');
|
||||
expect(team.tag.toString()).toBe('MT');
|
||||
expect(team.description.toString()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateTeamUseCase - Validation', () => {
|
||||
it('should reject team creation with empty team name', async () => {
|
||||
// Scenario: Team creation with empty name
|
||||
// Given: A driver exists
|
||||
const driverId = 'd4';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '4', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l4';
|
||||
const league = League.create({ id: leagueId, name: 'League 4', description: 'Test League 4', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with empty team name
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: '',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
it('should reject team creation with invalid team name format', async () => {
|
||||
// Scenario: Team creation with invalid name format
|
||||
// Given: A driver exists
|
||||
const driverId = 'd5';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '5', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l5';
|
||||
const league = League.create({ id: leagueId, name: 'League 5', description: 'Test League 5', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with invalid team name
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Invalid!@#$%',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
it('should reject team creation when driver already belongs to a team', async () => {
|
||||
// Scenario: Driver already belongs to a team
|
||||
// Given: A driver exists
|
||||
const driverId = 'd6';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '6', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l6';
|
||||
const league = League.create({ id: leagueId, name: 'League 6', description: 'Test League 6', ownerId: 'owner' });
|
||||
|
||||
// And: The driver already belongs to a team
|
||||
const existingTeam = Team.create({ id: 'existing', name: 'Existing Team', tag: 'ET', description: 'Existing', ownerId: driverId, leagues: [] });
|
||||
await teamRepository.create(existingTeam);
|
||||
await membershipRepository.saveMembership({
|
||||
teamId: 'existing',
|
||||
driverId: driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'New Team',
|
||||
tag: 'NT',
|
||||
description: 'A new team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
expect(error.details.message).toContain('already belongs to a team');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateTeamUseCase - Error Handling', () => {
|
||||
it('should throw error when driver does not exist', async () => {
|
||||
// Scenario: Non-existent driver
|
||||
// Given: No driver exists with the given ID
|
||||
const nonExistentDriverId = 'nonexistent';
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l7';
|
||||
const league = League.create({ id: leagueId, name: 'League 7', description: 'Test League 7', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with non-existent driver ID
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: nonExistentDriverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
it('should throw error when league does not exist', async () => {
|
||||
// Scenario: Non-existent league
|
||||
// Given: A driver exists
|
||||
const driverId = 'd8';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '8', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: No league exists with the given ID
|
||||
const nonExistentLeagueId = 'nonexistent';
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with non-existent league ID
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driverId,
|
||||
leagues: [nonExistentLeagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('LEAGUE_NOT_FOUND');
|
||||
});
|
||||
|
||||
it('should throw error when team name already exists', async () => {
|
||||
// Scenario: Duplicate team name
|
||||
// Given: A driver exists
|
||||
const driverId = 'd9';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '9', name: 'Test Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l9';
|
||||
const league = League.create({ id: leagueId, name: 'League 9', description: 'Test League 9', ownerId: 'owner' });
|
||||
|
||||
// And: A team with the same name already exists
|
||||
const existingTeam = Team.create({ id: 'existing2', name: 'Duplicate Team', tag: 'DT', description: 'Existing', ownerId: 'other', leagues: [] });
|
||||
await teamRepository.create(existingTeam);
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called with duplicate team name
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Duplicate Team',
|
||||
tag: 'DT2',
|
||||
description: 'A new team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('VALIDATION_ERROR');
|
||||
expect(error.details.message).toContain('already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateTeamUseCase - Business Logic', () => {
|
||||
it('should set the creating driver as team captain', async () => {
|
||||
// Scenario: Driver becomes captain
|
||||
// Given: A driver exists
|
||||
const driverId = 'd10';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '10', name: 'Captain Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l10';
|
||||
const league = League.create({ id: leagueId, name: 'League 10', description: 'Test League 10', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Captain Team',
|
||||
tag: 'CT',
|
||||
description: 'A team with captain',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The creating driver should be set as team captain
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
|
||||
// And: The captain role should be recorded in the team roster
|
||||
const membership = await membershipRepository.getMembership(team.id.toString(), driverId);
|
||||
expect(membership).toBeDefined();
|
||||
expect(membership?.role).toBe('owner');
|
||||
});
|
||||
|
||||
it('should generate unique team ID', async () => {
|
||||
// Scenario: Unique team ID generation
|
||||
// Given: A driver exists
|
||||
const driverId = 'd11';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '11', name: 'Unique Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l11';
|
||||
const league = League.create({ id: leagueId, name: 'League 11', description: 'Test League 11', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Unique Team',
|
||||
tag: 'UT',
|
||||
description: 'A unique team',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
|
||||
// Then: The team should have a unique ID
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.id.toString()).toBeDefined();
|
||||
expect(team.id.toString().length).toBeGreaterThan(0);
|
||||
|
||||
// And: The ID should not conflict with existing teams
|
||||
const existingTeam = await teamRepository.findById(team.id.toString());
|
||||
expect(existingTeam).toBeDefined();
|
||||
expect(existingTeam?.id.toString()).toBe(team.id.toString());
|
||||
});
|
||||
|
||||
it('should set creation timestamp', async () => {
|
||||
// Scenario: Creation timestamp
|
||||
// Given: A driver exists
|
||||
const driverId = 'd12';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '12', name: 'Timestamp Driver', country: 'US' });
|
||||
|
||||
// And: A league exists
|
||||
const leagueId = 'l12';
|
||||
const league = League.create({ id: leagueId, name: 'League 12', description: 'Test League 12', ownerId: 'owner' });
|
||||
|
||||
// When: CreateTeamUseCase.execute() is called
|
||||
const beforeCreate = new Date();
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Timestamp Team',
|
||||
tag: 'TT',
|
||||
description: 'A team with timestamp',
|
||||
ownerId: driverId,
|
||||
leagues: [leagueId]
|
||||
});
|
||||
const afterCreate = new Date();
|
||||
|
||||
// Then: The team should have a creation timestamp
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team } = result.unwrap();
|
||||
expect(team.createdAt).toBeDefined();
|
||||
|
||||
// And: The timestamp should be current or recent
|
||||
const createdAt = team.createdAt.toDate();
|
||||
expect(createdAt.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAt.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* Integration Test: Team Detail Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of team detail-related Use Cases:
|
||||
* - GetTeamDetailsUseCase: Retrieves detailed team information including roster and management permissions
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { GetTeamDetailsUseCase } from '../../../core/racing/application/use-cases/GetTeamDetailsUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Team Detail Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let getTeamDetailsUseCase: GetTeamDetailsUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
getTeamDetailsUseCase = new GetTeamDetailsUseCase(teamRepository, membershipRepository);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetTeamDetailsUseCase - Success Path', () => {
|
||||
it('should retrieve team detail with membership and management permissions for owner', async () => {
|
||||
// Scenario: Team owner views team details
|
||||
// Given: A team exists
|
||||
const teamId = 't1';
|
||||
const ownerId = 'd1';
|
||||
const team = Team.create({ id: teamId, name: 'Team 1', tag: 'T1', description: 'Desc', ownerId, leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is the owner
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: GetTeamDetailsUseCase.execute() is called
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId, driverId: ownerId });
|
||||
|
||||
// Then: The result should contain team information and management permissions
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.team.id.toString()).toBe(teamId);
|
||||
expect(data.membership?.role).toBe('owner');
|
||||
expect(data.canManage).toBe(true);
|
||||
});
|
||||
|
||||
it('should retrieve team detail for a non-member', async () => {
|
||||
// Scenario: Non-member views team details
|
||||
// Given: A team exists
|
||||
const teamId = 't2';
|
||||
const team = Team.create({ id: teamId, name: 'Team 2', tag: 'T2', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: GetTeamDetailsUseCase.execute() is called with a driver who is not a member
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId, driverId: 'non-member' });
|
||||
|
||||
// Then: The result should contain team information but no membership and no management permissions
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.team.id.toString()).toBe(teamId);
|
||||
expect(data.membership).toBeNull();
|
||||
expect(data.canManage).toBe(false);
|
||||
});
|
||||
|
||||
it('should retrieve team detail for a regular member', async () => {
|
||||
// Scenario: Regular member views team details
|
||||
// Given: A team exists
|
||||
const teamId = 't3';
|
||||
const memberId = 'd3';
|
||||
const team = Team.create({ id: teamId, name: 'Team 3', tag: 'T3', description: 'Desc', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is a regular member
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: memberId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: GetTeamDetailsUseCase.execute() is called
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId, driverId: memberId });
|
||||
|
||||
// Then: The result should contain team information and membership but no management permissions
|
||||
expect(result.isOk()).toBe(true);
|
||||
const data = result.unwrap();
|
||||
expect(data.team.id.toString()).toBe(teamId);
|
||||
expect(data.membership?.role).toBe('driver');
|
||||
expect(data.canManage).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamDetailsUseCase - Error Handling', () => {
|
||||
it('should throw error when team does not exist', async () => {
|
||||
// Scenario: Non-existent team
|
||||
// When: GetTeamDetailsUseCase.execute() is called with non-existent team ID
|
||||
const result = await getTeamDetailsUseCase.execute({ teamId: 'nonexistent', driverId: 'any' });
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('TEAM_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
/**
|
||||
* Integration Test: Team Leaderboard Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of team leaderboard-related Use Cases:
|
||||
* - GetTeamsLeaderboardUseCase: Retrieves ranked list of teams with performance metrics
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { GetTeamsLeaderboardUseCase } from '../../../core/racing/application/use-cases/GetTeamsLeaderboardUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Team Leaderboard Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let getTeamsLeaderboardUseCase: GetTeamsLeaderboardUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
|
||||
// Mock driver stats provider
|
||||
const getDriverStats = (driverId: string) => {
|
||||
const statsMap: Record<string, { rating: number, wins: number, totalRaces: number }> = {
|
||||
'd1': { rating: 2000, wins: 10, totalRaces: 50 },
|
||||
'd2': { rating: 1500, wins: 5, totalRaces: 30 },
|
||||
'd3': { rating: 1000, wins: 2, totalRaces: 20 },
|
||||
};
|
||||
return statsMap[driverId] || null;
|
||||
};
|
||||
|
||||
getTeamsLeaderboardUseCase = new GetTeamsLeaderboardUseCase(
|
||||
teamRepository,
|
||||
membershipRepository,
|
||||
getDriverStats,
|
||||
mockLogger
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetTeamsLeaderboardUseCase - Success Path', () => {
|
||||
it('should retrieve ranked team leaderboard with performance metrics', async () => {
|
||||
// Scenario: Leaderboard with multiple teams
|
||||
// Given: Multiple teams exist
|
||||
const team1 = Team.create({ id: 't1', name: 'Pro Team', tag: 'PRO', description: 'Desc', ownerId: 'o1', leagues: [] });
|
||||
const team2 = Team.create({ id: 't2', name: 'Am Team', tag: 'AM', description: 'Desc', ownerId: 'o2', leagues: [] });
|
||||
await teamRepository.create(team1);
|
||||
await teamRepository.create(team2);
|
||||
|
||||
// And: Teams have members with different stats
|
||||
await membershipRepository.saveMembership({ teamId: 't1', driverId: 'd1', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
await membershipRepository.saveMembership({ teamId: 't2', driverId: 'd3', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
|
||||
// When: GetTeamsLeaderboardUseCase.execute() is called
|
||||
const result = await getTeamsLeaderboardUseCase.execute({ leagueId: 'any' });
|
||||
|
||||
// Then: The result should contain ranked teams
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { items, topItems } = result.unwrap();
|
||||
expect(items).toHaveLength(2);
|
||||
|
||||
// And: Teams should be ranked by rating (Pro Team has d1 with 2000, Am Team has d3 with 1000)
|
||||
expect(topItems[0]?.team.id.toString()).toBe('t1');
|
||||
expect(topItems[0]?.rating).toBe(2000);
|
||||
expect(topItems[1]?.team.id.toString()).toBe('t2');
|
||||
expect(topItems[1]?.rating).toBe(1000);
|
||||
});
|
||||
|
||||
it('should handle empty leaderboard', async () => {
|
||||
// Scenario: No teams exist
|
||||
// When: GetTeamsLeaderboardUseCase.execute() is called
|
||||
const result = await getTeamsLeaderboardUseCase.execute({ leagueId: 'any' });
|
||||
|
||||
// Then: The result should be empty
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { items } = result.unwrap();
|
||||
expect(items).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,536 +0,0 @@
|
||||
/**
|
||||
* Integration Test: Team Membership Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of team membership-related Use Cases:
|
||||
* - JoinTeamUseCase: Allows driver to request to join a team
|
||||
* - LeaveTeamUseCase: Allows driver to leave a team
|
||||
* - GetTeamMembershipUseCase: Retrieves driver's membership in a team
|
||||
* - GetTeamMembersUseCase: Retrieves all team members
|
||||
* - GetTeamJoinRequestsUseCase: Retrieves pending join requests
|
||||
* - ApproveTeamJoinRequestUseCase: Admin approves join request
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { JoinTeamUseCase } from '../../../core/racing/application/use-cases/JoinTeamUseCase';
|
||||
import { LeaveTeamUseCase } from '../../../core/racing/application/use-cases/LeaveTeamUseCase';
|
||||
import { GetTeamMembershipUseCase } from '../../../core/racing/application/use-cases/GetTeamMembershipUseCase';
|
||||
import { GetTeamMembersUseCase } from '../../../core/racing/application/use-cases/GetTeamMembersUseCase';
|
||||
import { GetTeamJoinRequestsUseCase } from '../../../core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
|
||||
import { ApproveTeamJoinRequestUseCase } from '../../../core/racing/application/use-cases/ApproveTeamJoinRequestUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Driver } from '../../../core/racing/domain/entities/Driver';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Team Membership Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let driverRepository: InMemoryDriverRepository;
|
||||
let joinTeamUseCase: JoinTeamUseCase;
|
||||
let leaveTeamUseCase: LeaveTeamUseCase;
|
||||
let getTeamMembershipUseCase: GetTeamMembershipUseCase;
|
||||
let getTeamMembersUseCase: GetTeamMembersUseCase;
|
||||
let getTeamJoinRequestsUseCase: GetTeamJoinRequestsUseCase;
|
||||
let approveTeamJoinRequestUseCase: ApproveTeamJoinRequestUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
driverRepository = new InMemoryDriverRepository(mockLogger);
|
||||
|
||||
joinTeamUseCase = new JoinTeamUseCase(teamRepository, membershipRepository, mockLogger);
|
||||
leaveTeamUseCase = new LeaveTeamUseCase(teamRepository, membershipRepository, mockLogger);
|
||||
getTeamMembershipUseCase = new GetTeamMembershipUseCase(membershipRepository, mockLogger);
|
||||
getTeamMembersUseCase = new GetTeamMembersUseCase(membershipRepository, driverRepository, teamRepository, mockLogger);
|
||||
getTeamJoinRequestsUseCase = new GetTeamJoinRequestsUseCase(membershipRepository, driverRepository, teamRepository);
|
||||
approveTeamJoinRequestUseCase = new ApproveTeamJoinRequestUseCase(membershipRepository);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
driverRepository.clear();
|
||||
});
|
||||
|
||||
describe('JoinTeamUseCase - Success Path', () => {
|
||||
it('should create a join request for a team', async () => {
|
||||
// Scenario: Driver requests to join team
|
||||
// Given: A driver exists
|
||||
const driverId = 'd1';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '1', name: 'Driver 1', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team exists
|
||||
const teamId = 't1';
|
||||
const team = Team.create({ id: teamId, name: 'Team 1', tag: 'T1', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The team has available roster slots
|
||||
// (Team has no members yet, so it has available slots)
|
||||
|
||||
// When: JoinTeamUseCase.execute() is called
|
||||
const result = await joinTeamUseCase.execute({
|
||||
teamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: A join request should be created
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: resultTeam, membership } = result.unwrap();
|
||||
expect(resultTeam.id.toString()).toBe(teamId);
|
||||
|
||||
// And: The request should be in pending status
|
||||
expect(membership.status).toBe('active');
|
||||
expect(membership.role).toBe('driver');
|
||||
|
||||
// And: The membership should be in the repository
|
||||
const savedMembership = await membershipRepository.getMembership(teamId, driverId);
|
||||
expect(savedMembership).toBeDefined();
|
||||
expect(savedMembership?.status).toBe('active');
|
||||
});
|
||||
|
||||
it('should create a join request when team is not full', async () => {
|
||||
// Scenario: Team has available slots
|
||||
// Given: A driver exists
|
||||
const driverId = 'd2';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '2', name: 'Driver 2', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team exists with available roster slots
|
||||
const teamId = 't2';
|
||||
const team = Team.create({ id: teamId, name: 'Team 2', tag: 'T2', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: JoinTeamUseCase.execute() is called
|
||||
const result = await joinTeamUseCase.execute({
|
||||
teamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: A join request should be created
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { membership } = result.unwrap();
|
||||
expect(membership.status).toBe('active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JoinTeamUseCase - Validation', () => {
|
||||
it('should reject join request when driver is already a member', async () => {
|
||||
// Scenario: Driver already member
|
||||
// Given: A driver exists
|
||||
const driverId = 'd3';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '3', name: 'Driver 3', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team exists
|
||||
const teamId = 't3';
|
||||
const team = Team.create({ id: teamId, name: 'Team 3', tag: 'T3', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is already a member of the team
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: JoinTeamUseCase.execute() is called
|
||||
const result = await joinTeamUseCase.execute({
|
||||
teamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('ALREADY_MEMBER');
|
||||
});
|
||||
|
||||
it('should reject join request when driver already has pending request', async () => {
|
||||
// Scenario: Driver has pending request
|
||||
// Given: A driver exists
|
||||
const driverId = 'd4';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '4', name: 'Driver 4', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team exists
|
||||
const teamId = 't4';
|
||||
const team = Team.create({ id: teamId, name: 'Team 4', tag: 'T4', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver already has a pending join request for the team
|
||||
await membershipRepository.saveJoinRequest({
|
||||
id: 'jr1',
|
||||
teamId,
|
||||
driverId,
|
||||
status: 'pending',
|
||||
requestedAt: new Date()
|
||||
});
|
||||
|
||||
// When: JoinTeamUseCase.execute() is called
|
||||
const result = await joinTeamUseCase.execute({
|
||||
teamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('ALREADY_MEMBER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JoinTeamUseCase - Error Handling', () => {
|
||||
it('should throw error when driver does not exist', async () => {
|
||||
// Scenario: Non-existent driver
|
||||
// Given: No driver exists with the given ID
|
||||
const nonExistentDriverId = 'nonexistent';
|
||||
|
||||
// And: A team exists
|
||||
const teamId = 't5';
|
||||
const team = Team.create({ id: teamId, name: 'Team 5', tag: 'T5', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: JoinTeamUseCase.execute() is called with non-existent driver ID
|
||||
const result = await joinTeamUseCase.execute({
|
||||
teamId,
|
||||
driverId: nonExistentDriverId
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('TEAM_NOT_FOUND');
|
||||
});
|
||||
|
||||
it('should throw error when team does not exist', async () => {
|
||||
// Scenario: Non-existent team
|
||||
// Given: A driver exists
|
||||
const driverId = 'd6';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '6', name: 'Driver 6', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: No team exists with the given ID
|
||||
const nonExistentTeamId = 'nonexistent';
|
||||
|
||||
// When: JoinTeamUseCase.execute() is called with non-existent team ID
|
||||
const result = await joinTeamUseCase.execute({
|
||||
teamId: nonExistentTeamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('TEAM_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LeaveTeamUseCase - Success Path', () => {
|
||||
it('should allow driver to leave team', async () => {
|
||||
// Scenario: Driver leaves team
|
||||
// Given: A driver exists
|
||||
const driverId = 'd7';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '7', name: 'Driver 7', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team exists
|
||||
const teamId = 't7';
|
||||
const team = Team.create({ id: teamId, name: 'Team 7', tag: 'T7', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is a member of the team
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: LeaveTeamUseCase.execute() is called
|
||||
const result = await leaveTeamUseCase.execute({
|
||||
teamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: The driver should be removed from the team
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: resultTeam, previousMembership } = result.unwrap();
|
||||
expect(resultTeam.id.toString()).toBe(teamId);
|
||||
expect(previousMembership.driverId).toBe(driverId);
|
||||
|
||||
// And: The membership should be removed from the repository
|
||||
const savedMembership = await membershipRepository.getMembership(teamId, driverId);
|
||||
expect(savedMembership).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('LeaveTeamUseCase - Validation', () => {
|
||||
it('should reject leave when driver is not a member', async () => {
|
||||
// Scenario: Driver not member
|
||||
// Given: A driver exists
|
||||
const driverId = 'd8';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '8', name: 'Driver 8', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team exists
|
||||
const teamId = 't8';
|
||||
const team = Team.create({ id: teamId, name: 'Team 8', tag: 'T8', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: LeaveTeamUseCase.execute() is called
|
||||
const result = await leaveTeamUseCase.execute({
|
||||
teamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('NOT_MEMBER');
|
||||
});
|
||||
|
||||
it('should reject leave when driver is team owner', async () => {
|
||||
// Scenario: Team owner cannot leave
|
||||
// Given: A driver exists
|
||||
const driverId = 'd9';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '9', name: 'Driver 9', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team exists with the driver as owner
|
||||
const teamId = 't9';
|
||||
const team = Team.create({ id: teamId, name: 'Team 9', tag: 'T9', description: 'Test Team', ownerId: driverId, leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is the owner
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: LeaveTeamUseCase.execute() is called
|
||||
const result = await leaveTeamUseCase.execute({
|
||||
teamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: Should return error
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.unwrapErr();
|
||||
expect(error.code).toBe('OWNER_CANNOT_LEAVE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamMembershipUseCase - Success Path', () => {
|
||||
it('should retrieve driver membership in team', async () => {
|
||||
// Scenario: Retrieve membership
|
||||
// Given: A driver exists
|
||||
const driverId = 'd10';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '10', name: 'Driver 10', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team exists
|
||||
const teamId = 't10';
|
||||
const team = Team.create({ id: teamId, name: 'Team 10', tag: 'T10', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: The driver is a member of the team
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: GetTeamMembershipUseCase.execute() is called
|
||||
const result = await getTeamMembershipUseCase.execute({
|
||||
teamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: It should return the membership
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { membership } = result.unwrap();
|
||||
expect(membership).toBeDefined();
|
||||
expect(membership?.role).toBe('member');
|
||||
expect(membership?.isActive).toBe(true);
|
||||
});
|
||||
|
||||
it('should return null when driver is not a member', async () => {
|
||||
// Scenario: No membership found
|
||||
// Given: A driver exists
|
||||
const driverId = 'd11';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '11', name: 'Driver 11', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team exists
|
||||
const teamId = 't11';
|
||||
const team = Team.create({ id: teamId, name: 'Team 11', tag: 'T11', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: GetTeamMembershipUseCase.execute() is called
|
||||
const result = await getTeamMembershipUseCase.execute({
|
||||
teamId,
|
||||
driverId
|
||||
});
|
||||
|
||||
// Then: It should return null
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { membership } = result.unwrap();
|
||||
expect(membership).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamMembersUseCase - Success Path', () => {
|
||||
it('should retrieve all team members', async () => {
|
||||
// Scenario: Retrieve team members
|
||||
// Given: A team exists
|
||||
const teamId = 't12';
|
||||
const team = Team.create({ id: teamId, name: 'Team 12', tag: 'T12', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: Multiple drivers exist
|
||||
const driver1 = Driver.create({ id: 'd12', iracingId: '12', name: 'Driver 12', country: 'US' });
|
||||
const driver2 = Driver.create({ id: 'd13', iracingId: '13', name: 'Driver 13', country: 'UK' });
|
||||
await driverRepository.create(driver1);
|
||||
await driverRepository.create(driver2);
|
||||
|
||||
// And: Drivers are members of the team
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: 'd12',
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
await membershipRepository.saveMembership({
|
||||
teamId,
|
||||
driverId: 'd13',
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date()
|
||||
});
|
||||
|
||||
// When: GetTeamMembersUseCase.execute() is called
|
||||
const result = await getTeamMembersUseCase.execute({
|
||||
teamId
|
||||
});
|
||||
|
||||
// Then: It should return all team members
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: resultTeam, members } = result.unwrap();
|
||||
expect(resultTeam.id.toString()).toBe(teamId);
|
||||
expect(members).toHaveLength(2);
|
||||
expect(members[0].membership.driverId).toBe('d12');
|
||||
expect(members[1].membership.driverId).toBe('d13');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetTeamJoinRequestsUseCase - Success Path', () => {
|
||||
it('should retrieve pending join requests', async () => {
|
||||
// Scenario: Retrieve join requests
|
||||
// Given: A team exists
|
||||
const teamId = 't14';
|
||||
const team = Team.create({ id: teamId, name: 'Team 14', tag: 'T14', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: Multiple drivers exist
|
||||
const driver1 = Driver.create({ id: 'd14', iracingId: '14', name: 'Driver 14', country: 'US' });
|
||||
const driver2 = Driver.create({ id: 'd15', iracingId: '15', name: 'Driver 15', country: 'UK' });
|
||||
await driverRepository.create(driver1);
|
||||
await driverRepository.create(driver2);
|
||||
|
||||
// And: Drivers have pending join requests
|
||||
await membershipRepository.saveJoinRequest({
|
||||
id: 'jr2',
|
||||
teamId,
|
||||
driverId: 'd14',
|
||||
status: 'pending',
|
||||
requestedAt: new Date()
|
||||
});
|
||||
await membershipRepository.saveJoinRequest({
|
||||
id: 'jr3',
|
||||
teamId,
|
||||
driverId: 'd15',
|
||||
status: 'pending',
|
||||
requestedAt: new Date()
|
||||
});
|
||||
|
||||
// When: GetTeamJoinRequestsUseCase.execute() is called
|
||||
const result = await getTeamJoinRequestsUseCase.execute({
|
||||
teamId
|
||||
});
|
||||
|
||||
// Then: It should return the join requests
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { team: resultTeam, joinRequests } = result.unwrap();
|
||||
expect(resultTeam.id.toString()).toBe(teamId);
|
||||
expect(joinRequests).toHaveLength(2);
|
||||
expect(joinRequests[0].driverId).toBe('d14');
|
||||
expect(joinRequests[1].driverId).toBe('d15');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApproveTeamJoinRequestUseCase - Success Path', () => {
|
||||
it('should approve a pending join request', async () => {
|
||||
// Scenario: Admin approves join request
|
||||
// Given: A team exists
|
||||
const teamId = 't16';
|
||||
const team = Team.create({ id: teamId, name: 'Team 16', tag: 'T16', description: 'Test Team', ownerId: 'owner', leagues: [] });
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: A driver exists
|
||||
const driverId = 'd16';
|
||||
const driver = Driver.create({ id: driverId, iracingId: '16', name: 'Driver 16', country: 'US' });
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A driver has a pending join request for the team
|
||||
await membershipRepository.saveJoinRequest({
|
||||
id: 'jr4',
|
||||
teamId,
|
||||
driverId,
|
||||
status: 'pending',
|
||||
requestedAt: new Date()
|
||||
});
|
||||
|
||||
// When: ApproveTeamJoinRequestUseCase.execute() is called
|
||||
const result = await approveTeamJoinRequestUseCase.execute({
|
||||
teamId,
|
||||
requestId: 'jr4'
|
||||
});
|
||||
|
||||
// Then: The join request should be approved
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { membership } = result.unwrap();
|
||||
expect(membership.driverId).toBe(driverId);
|
||||
expect(membership.teamId).toBe(teamId);
|
||||
expect(membership.status).toBe('active');
|
||||
|
||||
// And: The driver should be added to the team roster
|
||||
const savedMembership = await membershipRepository.getMembership(teamId, driverId);
|
||||
expect(savedMembership).toBeDefined();
|
||||
expect(savedMembership?.status).toBe('active');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* Integration Test: Teams List Use Case Orchestration
|
||||
*
|
||||
* Tests the orchestration logic of teams list-related Use Cases:
|
||||
* - GetAllTeamsUseCase: Retrieves list of teams with enrichment (member count, stats)
|
||||
* - Validates that Use Cases correctly interact with their Ports (Repositories)
|
||||
* - Uses In-Memory adapters for fast, deterministic testing
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT UI rendering
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { InMemoryTeamRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { InMemoryTeamStatsRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryTeamStatsRepository';
|
||||
import { GetAllTeamsUseCase } from '../../../core/racing/application/use-cases/GetAllTeamsUseCase';
|
||||
import { Team } from '../../../core/racing/domain/entities/Team';
|
||||
import { Logger } from '../../../core/shared/domain/Logger';
|
||||
|
||||
describe('Teams List Use Case Orchestration', () => {
|
||||
let teamRepository: InMemoryTeamRepository;
|
||||
let membershipRepository: InMemoryTeamMembershipRepository;
|
||||
let statsRepository: InMemoryTeamStatsRepository;
|
||||
let getAllTeamsUseCase: GetAllTeamsUseCase;
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeAll(() => {
|
||||
mockLogger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as unknown as Logger;
|
||||
|
||||
teamRepository = new InMemoryTeamRepository(mockLogger);
|
||||
membershipRepository = new InMemoryTeamMembershipRepository(mockLogger);
|
||||
statsRepository = new InMemoryTeamStatsRepository();
|
||||
getAllTeamsUseCase = new GetAllTeamsUseCase(teamRepository, membershipRepository, statsRepository, mockLogger);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
teamRepository.clear();
|
||||
membershipRepository.clear();
|
||||
statsRepository.clear();
|
||||
});
|
||||
|
||||
describe('GetAllTeamsUseCase - Success Path', () => {
|
||||
it('should retrieve complete teams list with all teams and enrichment', async () => {
|
||||
// Scenario: Teams list with multiple teams
|
||||
// Given: Multiple teams exist
|
||||
const team1 = Team.create({ id: 't1', name: 'Team 1', tag: 'T1', description: 'Desc 1', ownerId: 'o1', leagues: [] });
|
||||
const team2 = Team.create({ id: 't2', name: 'Team 2', tag: 'T2', description: 'Desc 2', ownerId: 'o2', leagues: [] });
|
||||
await teamRepository.create(team1);
|
||||
await teamRepository.create(team2);
|
||||
|
||||
// And: Teams have members
|
||||
await membershipRepository.saveMembership({ teamId: 't1', driverId: 'd1', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
await membershipRepository.saveMembership({ teamId: 't1', driverId: 'd2', role: 'driver', status: 'active', joinedAt: new Date() });
|
||||
await membershipRepository.saveMembership({ teamId: 't2', driverId: 'd3', role: 'owner', status: 'active', joinedAt: new Date() });
|
||||
|
||||
// And: Teams have stats
|
||||
await statsRepository.saveTeamStats('t1', {
|
||||
totalWins: 5,
|
||||
totalRaces: 20,
|
||||
rating: 1500,
|
||||
performanceLevel: 'intermediate',
|
||||
specialization: 'sprint',
|
||||
region: 'EU',
|
||||
languages: ['en'],
|
||||
isRecruiting: true
|
||||
});
|
||||
|
||||
// When: GetAllTeamsUseCase.execute() is called
|
||||
const result = await getAllTeamsUseCase.execute({});
|
||||
|
||||
// Then: The result should contain all teams with enrichment
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { teams, totalCount } = result.unwrap();
|
||||
expect(totalCount).toBe(2);
|
||||
|
||||
const enriched1 = teams.find(t => t.team.id.toString() === 't1');
|
||||
expect(enriched1).toBeDefined();
|
||||
expect(enriched1?.memberCount).toBe(2);
|
||||
expect(enriched1?.totalWins).toBe(5);
|
||||
expect(enriched1?.rating).toBe(1500);
|
||||
|
||||
const enriched2 = teams.find(t => t.team.id.toString() === 't2');
|
||||
expect(enriched2).toBeDefined();
|
||||
expect(enriched2?.memberCount).toBe(1);
|
||||
expect(enriched2?.totalWins).toBe(0); // Default value
|
||||
});
|
||||
|
||||
it('should handle empty teams list', async () => {
|
||||
// Scenario: No teams exist
|
||||
// When: GetAllTeamsUseCase.execute() is called
|
||||
const result = await getAllTeamsUseCase.execute({});
|
||||
|
||||
// Then: The result should be empty
|
||||
expect(result.isOk()).toBe(true);
|
||||
const { teams, totalCount } = result.unwrap();
|
||||
expect(totalCount).toBe(0);
|
||||
expect(teams).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user