Files
gridpilot.gg/tests/integration/teams/team-membership-use-cases.integration.test.ts
Marc Mintel 2fba80da57
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 4m46s
Contract Testing / contract-snapshot (pull_request) Has been skipped
integration tests
2026-01-22 19:16:43 +01:00

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