/** * 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'); }); }); });