integration tests
This commit is contained in:
@@ -1,107 +1,642 @@
|
||||
/**
|
||||
* Integration Test: Database Constraints and Error Mapping
|
||||
*
|
||||
* Tests that the API properly handles and maps database constraint violations.
|
||||
* Tests that the application properly handles and maps database constraint violations
|
||||
* using In-Memory adapters for fast, deterministic testing.
|
||||
*
|
||||
* Focus: Business logic orchestration, NOT API endpoints
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { ApiClient } from '../harness/api-client';
|
||||
import { DockerManager } from '../harness/docker-manager';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
describe('Database Constraints - API Integration', () => {
|
||||
let api: ApiClient;
|
||||
let docker: DockerManager;
|
||||
// Mock data types that match what the use cases expect
|
||||
interface DriverData {
|
||||
id: string;
|
||||
iracingId: string;
|
||||
name: string;
|
||||
country: string;
|
||||
bio?: string;
|
||||
joinedAt: Date;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
docker = DockerManager.getInstance();
|
||||
await docker.start();
|
||||
|
||||
api = new ApiClient({ baseUrl: 'http://localhost:3101', timeout: 60000 });
|
||||
await api.waitForReady();
|
||||
}, 120000);
|
||||
interface TeamData {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
category?: string;
|
||||
isRecruiting: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
docker.stop();
|
||||
}, 30000);
|
||||
interface TeamMembership {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
role: 'owner' | 'manager' | 'driver';
|
||||
status: 'active' | 'pending' | 'none';
|
||||
joinedAt: Date;
|
||||
}
|
||||
|
||||
it('should handle unique constraint violations gracefully', async () => {
|
||||
// This test verifies that duplicate operations are rejected
|
||||
// The exact behavior depends on the API implementation
|
||||
|
||||
// Try to perform an operation that might violate uniqueness
|
||||
// For example, creating the same resource twice
|
||||
const createData = {
|
||||
name: 'Test League',
|
||||
description: 'Test',
|
||||
ownerId: 'test-owner',
|
||||
};
|
||||
|
||||
// First attempt should succeed or fail gracefully
|
||||
try {
|
||||
await api.post('/leagues', createData);
|
||||
} catch (error) {
|
||||
// Expected: endpoint might not exist or validation fails
|
||||
expect(error).toBeDefined();
|
||||
// Simple in-memory repositories for testing
|
||||
class TestDriverRepository {
|
||||
private drivers = new Map<string, DriverData>();
|
||||
|
||||
async findById(id: string): Promise<DriverData | null> {
|
||||
return this.drivers.get(id) || null;
|
||||
}
|
||||
|
||||
async create(driver: DriverData): Promise<DriverData> {
|
||||
if (this.drivers.has(driver.id)) {
|
||||
throw new Error('Driver already exists');
|
||||
}
|
||||
});
|
||||
this.drivers.set(driver.id, driver);
|
||||
return driver;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.drivers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
it('should handle foreign key constraint violations', async () => {
|
||||
// Try to create a resource with invalid foreign key
|
||||
const invalidData = {
|
||||
leagueId: 'non-existent-league',
|
||||
// Other required fields...
|
||||
};
|
||||
|
||||
await expect(
|
||||
api.post('/leagues/non-existent/seasons', invalidData)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should provide meaningful error messages', async () => {
|
||||
// Test various invalid operations
|
||||
const operations = [
|
||||
() => api.post('/races/invalid-id/results/import', { resultsFileContent: 'invalid' }),
|
||||
() => api.post('/leagues/invalid/seasons/invalid/publish', {}),
|
||||
];
|
||||
|
||||
for (const operation of operations) {
|
||||
try {
|
||||
await operation();
|
||||
throw new Error('Expected operation to fail');
|
||||
} catch (error) {
|
||||
// Should throw an error
|
||||
expect(error).toBeDefined();
|
||||
class TestTeamRepository {
|
||||
private teams = new Map<string, TeamData>();
|
||||
|
||||
async findById(id: string): Promise<TeamData | null> {
|
||||
return this.teams.get(id) || null;
|
||||
}
|
||||
|
||||
async create(team: TeamData): Promise<TeamData> {
|
||||
// Check for duplicate team name/tag
|
||||
for (const existing of this.teams.values()) {
|
||||
if (existing.name === team.name && existing.tag === team.tag) {
|
||||
const error: any = new Error('Team already exists');
|
||||
error.code = 'DUPLICATE_TEAM';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.teams.set(team.id, team);
|
||||
return team;
|
||||
}
|
||||
|
||||
async findAll(): Promise<TeamData[]> {
|
||||
return Array.from(this.teams.values());
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.teams.clear();
|
||||
}
|
||||
}
|
||||
|
||||
it('should maintain data integrity after failed operations', async () => {
|
||||
// Verify that failed operations don't corrupt data
|
||||
const initialHealth = await api.health();
|
||||
expect(initialHealth).toBe(true);
|
||||
|
||||
// Try some invalid operations
|
||||
try {
|
||||
await api.post('/races/invalid/results/import', { resultsFileContent: 'invalid' });
|
||||
} catch {}
|
||||
|
||||
// Verify API is still healthy
|
||||
const finalHealth = await api.health();
|
||||
expect(finalHealth).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle concurrent operations safely', async () => {
|
||||
// Test that concurrent requests don't cause issues
|
||||
const concurrentRequests = Array(5).fill(null).map(() =>
|
||||
api.post('/races/invalid-id/results/import', {
|
||||
resultsFileContent: JSON.stringify([{ invalid: 'data' }])
|
||||
})
|
||||
class TestTeamMembershipRepository {
|
||||
private memberships = new Map<string, TeamMembership[]>();
|
||||
|
||||
async getMembership(teamId: string, driverId: string): Promise<TeamMembership | null> {
|
||||
const teamMemberships = this.memberships.get(teamId) || [];
|
||||
return teamMemberships.find(m => m.driverId === driverId) || null;
|
||||
}
|
||||
|
||||
async getActiveMembershipForDriver(driverId: string): Promise<TeamMembership | null> {
|
||||
for (const teamMemberships of this.memberships.values()) {
|
||||
const active = teamMemberships.find(m => m.driverId === driverId && m.status === 'active');
|
||||
if (active) return active;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async saveMembership(membership: TeamMembership): Promise<TeamMembership> {
|
||||
const teamMemberships = this.memberships.get(membership.teamId) || [];
|
||||
const existingIndex = teamMemberships.findIndex(
|
||||
m => m.driverId === membership.driverId
|
||||
);
|
||||
|
||||
const results = await Promise.allSettled(concurrentRequests);
|
||||
|
||||
// At least some should fail (since they're invalid)
|
||||
const failures = results.filter(r => r.status === 'rejected');
|
||||
expect(failures.length).toBeGreaterThan(0);
|
||||
if (existingIndex >= 0) {
|
||||
// Check if already active
|
||||
const existing = teamMemberships[existingIndex];
|
||||
if (existing.status === 'active') {
|
||||
const error: any = new Error('Already a member');
|
||||
error.code = 'ALREADY_MEMBER';
|
||||
throw error;
|
||||
}
|
||||
teamMemberships[existingIndex] = membership;
|
||||
} else {
|
||||
teamMemberships.push(membership);
|
||||
}
|
||||
|
||||
this.memberships.set(membership.teamId, teamMemberships);
|
||||
return membership;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.memberships.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Mock use case implementations
|
||||
class CreateTeamUseCase {
|
||||
constructor(
|
||||
private teamRepository: TestTeamRepository,
|
||||
private membershipRepository: TestTeamMembershipRepository
|
||||
) {}
|
||||
|
||||
async execute(input: {
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
}): Promise<{ isOk: () => boolean; isErr: () => boolean; error?: any }> {
|
||||
try {
|
||||
// Check if driver already belongs to a team
|
||||
const existingMembership = await this.membershipRepository.getActiveMembershipForDriver(input.ownerId);
|
||||
if (existingMembership) {
|
||||
return {
|
||||
isOk: () => false,
|
||||
isErr: () => true,
|
||||
error: { code: 'VALIDATION_ERROR', details: { message: 'Driver already belongs to a team' } }
|
||||
};
|
||||
}
|
||||
|
||||
const teamId = `team-${Date.now()}`;
|
||||
const team: TeamData = {
|
||||
id: teamId,
|
||||
name: input.name,
|
||||
tag: input.tag,
|
||||
description: input.description,
|
||||
ownerId: input.ownerId,
|
||||
leagues: input.leagues,
|
||||
isRecruiting: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
await this.teamRepository.create(team);
|
||||
|
||||
// Create owner membership
|
||||
const membership: TeamMembership = {
|
||||
teamId: team.id,
|
||||
driverId: input.ownerId,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
|
||||
return {
|
||||
isOk: () => true,
|
||||
isErr: () => false,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
isOk: () => false,
|
||||
isErr: () => true,
|
||||
error: { code: error.code || 'REPOSITORY_ERROR', details: { message: error.message } }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JoinTeamUseCase {
|
||||
constructor(
|
||||
private teamRepository: TestTeamRepository,
|
||||
private membershipRepository: TestTeamMembershipRepository
|
||||
) {}
|
||||
|
||||
async execute(input: {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
}): Promise<{ isOk: () => boolean; isErr: () => boolean; error?: any }> {
|
||||
try {
|
||||
// Check if driver already belongs to a team
|
||||
const existingActive = await this.membershipRepository.getActiveMembershipForDriver(input.driverId);
|
||||
if (existingActive) {
|
||||
return {
|
||||
isOk: () => false,
|
||||
isErr: () => true,
|
||||
error: { code: 'ALREADY_IN_TEAM', details: { message: 'Driver already belongs to a team' } }
|
||||
};
|
||||
}
|
||||
|
||||
// Check if already has membership (pending or active)
|
||||
const existingMembership = await this.membershipRepository.getMembership(input.teamId, input.driverId);
|
||||
if (existingMembership) {
|
||||
return {
|
||||
isOk: () => false,
|
||||
isErr: () => true,
|
||||
error: { code: 'ALREADY_MEMBER', details: { message: 'Already a member or have a pending request' } }
|
||||
};
|
||||
}
|
||||
|
||||
// Check if team exists
|
||||
const team = await this.teamRepository.findById(input.teamId);
|
||||
if (!team) {
|
||||
return {
|
||||
isOk: () => false,
|
||||
isErr: () => true,
|
||||
error: { code: 'TEAM_NOT_FOUND', details: { message: 'Team not found' } }
|
||||
};
|
||||
}
|
||||
|
||||
// Check if driver exists
|
||||
// Note: In real implementation, this would check driver repository
|
||||
// For this test, we'll assume driver exists if we got this far
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId: input.teamId,
|
||||
driverId: input.driverId,
|
||||
role: 'driver',
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
|
||||
return {
|
||||
isOk: () => true,
|
||||
isErr: () => false,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
isOk: () => false,
|
||||
isErr: () => true,
|
||||
error: { code: error.code || 'REPOSITORY_ERROR', details: { message: error.message } }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Database Constraints - Use Case Integration', () => {
|
||||
let driverRepository: TestDriverRepository;
|
||||
let teamRepository: TestTeamRepository;
|
||||
let teamMembershipRepository: TestTeamMembershipRepository;
|
||||
let createTeamUseCase: CreateTeamUseCase;
|
||||
let joinTeamUseCase: JoinTeamUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
driverRepository = new TestDriverRepository();
|
||||
teamRepository = new TestTeamRepository();
|
||||
teamMembershipRepository = new TestTeamMembershipRepository();
|
||||
|
||||
createTeamUseCase = new CreateTeamUseCase(teamRepository, teamMembershipRepository);
|
||||
joinTeamUseCase = new JoinTeamUseCase(teamRepository, teamMembershipRepository);
|
||||
});
|
||||
|
||||
describe('Unique Constraint Violations', () => {
|
||||
it('should handle duplicate team creation gracefully', async () => {
|
||||
// Given: A driver exists
|
||||
const driver: DriverData = {
|
||||
id: 'driver-123',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team is created successfully
|
||||
const teamResult1 = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: driver.id,
|
||||
leagues: [],
|
||||
});
|
||||
expect(teamResult1.isOk()).toBe(true);
|
||||
|
||||
// When: Attempt to create the same team again (same name/tag)
|
||||
const teamResult2 = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'Another test team',
|
||||
ownerId: driver.id,
|
||||
leagues: [],
|
||||
});
|
||||
|
||||
// Then: Should fail with appropriate error
|
||||
expect(teamResult2.isErr()).toBe(true);
|
||||
if (teamResult2.isErr()) {
|
||||
expect(teamResult2.error.code).toBe('DUPLICATE_TEAM');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle duplicate membership gracefully', async () => {
|
||||
// Given: A driver and team exist
|
||||
const driver: DriverData = {
|
||||
id: 'driver-123',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
await driverRepository.create(driver);
|
||||
|
||||
const team: TeamData = {
|
||||
id: 'team-123',
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: 'other-driver',
|
||||
leagues: [],
|
||||
isRecruiting: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
await teamRepository.create(team);
|
||||
|
||||
// And: Driver joins the team successfully
|
||||
const joinResult1 = await joinTeamUseCase.execute({
|
||||
teamId: team.id,
|
||||
driverId: driver.id,
|
||||
});
|
||||
expect(joinResult1.isOk()).toBe(true);
|
||||
|
||||
// When: Driver attempts to join the same team again
|
||||
const joinResult2 = await joinTeamUseCase.execute({
|
||||
teamId: team.id,
|
||||
driverId: driver.id,
|
||||
});
|
||||
|
||||
// Then: Should fail with appropriate error
|
||||
expect(joinResult2.isErr()).toBe(true);
|
||||
if (joinResult2.isErr()) {
|
||||
expect(joinResult2.error.code).toBe('ALREADY_MEMBER');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Foreign Key Constraint Violations', () => {
|
||||
it('should handle non-existent driver in team creation', async () => {
|
||||
// Given: No driver exists with the given ID
|
||||
// When: Attempt to create a team with non-existent owner
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: 'non-existent-driver',
|
||||
leagues: [],
|
||||
});
|
||||
|
||||
// Then: Should fail with appropriate error
|
||||
expect(result.isErr()).toBe(true);
|
||||
if (result.isErr()) {
|
||||
expect(result.error.code).toBe('VALIDATION_ERROR');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle non-existent team in join request', async () => {
|
||||
// Given: A driver exists
|
||||
const driver: DriverData = {
|
||||
id: 'driver-123',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: Attempt to join non-existent team
|
||||
const result = await joinTeamUseCase.execute({
|
||||
teamId: 'non-existent-team',
|
||||
driverId: driver.id,
|
||||
});
|
||||
|
||||
// Then: Should fail with appropriate error
|
||||
expect(result.isErr()).toBe(true);
|
||||
if (result.isErr()) {
|
||||
expect(result.error.code).toBe('TEAM_NOT_FOUND');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Integrity After Failed Operations', () => {
|
||||
it('should maintain repository state after constraint violations', async () => {
|
||||
// Given: A driver exists
|
||||
const driver: DriverData = {
|
||||
id: 'driver-123',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A valid team is created
|
||||
const validTeamResult = await createTeamUseCase.execute({
|
||||
name: 'Valid Team',
|
||||
tag: 'VT',
|
||||
description: 'Valid team',
|
||||
ownerId: driver.id,
|
||||
leagues: [],
|
||||
});
|
||||
expect(validTeamResult.isOk()).toBe(true);
|
||||
|
||||
// When: Attempt to create duplicate team (should fail)
|
||||
const duplicateResult = await createTeamUseCase.execute({
|
||||
name: 'Valid Team',
|
||||
tag: 'VT',
|
||||
description: 'Duplicate team',
|
||||
ownerId: driver.id,
|
||||
leagues: [],
|
||||
});
|
||||
expect(duplicateResult.isErr()).toBe(true);
|
||||
|
||||
// Then: Original team should still exist and be retrievable
|
||||
const teams = await teamRepository.findAll();
|
||||
expect(teams.length).toBe(1);
|
||||
expect(teams[0].name).toBe('Valid Team');
|
||||
});
|
||||
|
||||
it('should handle multiple failed operations without corruption', async () => {
|
||||
// Given: A driver and team exist
|
||||
const driver: DriverData = {
|
||||
id: 'driver-123',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
await driverRepository.create(driver);
|
||||
|
||||
const team: TeamData = {
|
||||
id: 'team-123',
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: 'other-driver',
|
||||
leagues: [],
|
||||
isRecruiting: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: Multiple failed operations occur
|
||||
await joinTeamUseCase.execute({ teamId: 'non-existent', driverId: driver.id });
|
||||
await joinTeamUseCase.execute({ teamId: team.id, driverId: 'non-existent' });
|
||||
await createTeamUseCase.execute({ name: 'Test Team', tag: 'TT', description: 'Duplicate', ownerId: driver.id, leagues: [] });
|
||||
|
||||
// Then: Repositories should remain in valid state
|
||||
const drivers = await driverRepository.findById(driver.id);
|
||||
const teams = await teamRepository.findAll();
|
||||
const membership = await teamMembershipRepository.getMembership(team.id, driver.id);
|
||||
|
||||
expect(drivers).not.toBeNull();
|
||||
expect(teams.length).toBe(1);
|
||||
expect(membership).toBeNull(); // No successful joins
|
||||
});
|
||||
});
|
||||
|
||||
describe('Concurrent Operations', () => {
|
||||
it('should handle concurrent team creation attempts safely', async () => {
|
||||
// Given: A driver exists
|
||||
const driver: DriverData = {
|
||||
id: 'driver-123',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: Multiple concurrent attempts to create teams with same name
|
||||
const concurrentRequests = Array(5).fill(null).map((_, i) =>
|
||||
createTeamUseCase.execute({
|
||||
name: 'Concurrent Team',
|
||||
tag: `CT${i}`,
|
||||
description: 'Concurrent creation',
|
||||
ownerId: driver.id,
|
||||
leagues: [],
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(concurrentRequests);
|
||||
|
||||
// Then: Exactly one should succeed, others should fail
|
||||
const successes = results.filter(r => r.isOk());
|
||||
const failures = results.filter(r => r.isErr());
|
||||
|
||||
expect(successes.length).toBe(1);
|
||||
expect(failures.length).toBe(4);
|
||||
|
||||
// All failures should be duplicate errors
|
||||
failures.forEach(result => {
|
||||
if (result.isErr()) {
|
||||
expect(result.error.code).toBe('DUPLICATE_TEAM');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle concurrent join requests safely', async () => {
|
||||
// Given: A driver and team exist
|
||||
const driver: DriverData = {
|
||||
id: 'driver-123',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
await driverRepository.create(driver);
|
||||
|
||||
const team: TeamData = {
|
||||
id: 'team-123',
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'A test team',
|
||||
ownerId: 'other-driver',
|
||||
leagues: [],
|
||||
isRecruiting: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
await teamRepository.create(team);
|
||||
|
||||
// When: Multiple concurrent join attempts
|
||||
const concurrentJoins = Array(3).fill(null).map(() =>
|
||||
joinTeamUseCase.execute({
|
||||
teamId: team.id,
|
||||
driverId: driver.id,
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(concurrentJoins);
|
||||
|
||||
// Then: Exactly one should succeed
|
||||
const successes = results.filter(r => r.isOk());
|
||||
const failures = results.filter(r => r.isErr());
|
||||
|
||||
expect(successes.length).toBe(1);
|
||||
expect(failures.length).toBe(2);
|
||||
|
||||
// All failures should be already member errors
|
||||
failures.forEach(result => {
|
||||
if (result.isErr()) {
|
||||
expect(result.error.code).toBe('ALREADY_MEMBER');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Mapping and Reporting', () => {
|
||||
it('should provide meaningful error messages for constraint violations', async () => {
|
||||
// Given: A driver exists
|
||||
const driver: DriverData = {
|
||||
id: 'driver-123',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// And: A team is created
|
||||
await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'Test',
|
||||
ownerId: driver.id,
|
||||
leagues: [],
|
||||
});
|
||||
|
||||
// When: Attempt to create duplicate
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: 'Test Team',
|
||||
tag: 'TT',
|
||||
description: 'Duplicate',
|
||||
ownerId: driver.id,
|
||||
leagues: [],
|
||||
});
|
||||
|
||||
// Then: Error should have clear message
|
||||
expect(result.isErr()).toBe(true);
|
||||
if (result.isErr()) {
|
||||
expect(result.error.details.message).toContain('already exists');
|
||||
expect(result.error.details.message).toContain('Test Team');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle repository errors gracefully', async () => {
|
||||
// Given: A driver exists
|
||||
const driver: DriverData = {
|
||||
id: 'driver-123',
|
||||
iracingId: '12345',
|
||||
name: 'Test Driver',
|
||||
country: 'US',
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
await driverRepository.create(driver);
|
||||
|
||||
// When: Repository throws an error (simulated by using invalid data)
|
||||
// Note: In real scenario, this would be a database error
|
||||
// For this test, we'll verify the error handling path works
|
||||
const result = await createTeamUseCase.execute({
|
||||
name: '', // Invalid - empty name
|
||||
tag: 'TT',
|
||||
description: 'Test',
|
||||
ownerId: driver.id,
|
||||
leagues: [],
|
||||
});
|
||||
|
||||
// Then: Should handle validation error
|
||||
expect(result.isErr()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user