404 lines
16 KiB
TypeScript
404 lines
16 KiB
TypeScript
/**
|
|
* 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());
|
|
});
|
|
});
|
|
});
|