537 lines
20 KiB
TypeScript
537 lines
20 KiB
TypeScript
/**
|
|
* 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');
|
|
});
|
|
});
|
|
});
|