Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
+333
@@ -0,0 +1,333 @@
|
||||
import { CreateOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/CreateOrganizationCommandHandler';
|
||||
import { UpdateOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/UpdateOrganizationCommandHandler';
|
||||
import { DeleteOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/DeleteOrganizationCommandHandler';
|
||||
import { CreateOrganizationCommand } from '../../../../src/Application/Organization/commands/CreateOrganizationCommand';
|
||||
import { UpdateOrganizationCommand } from '../../../../src/Application/Organization/commands/UpdateOrganizationCommand';
|
||||
import { DeleteOrganizationCommand } from '../../../../src/Application/Organization/commands/DeleteOrganizationCommand';
|
||||
import { OrganizationState } from '../../../../src/Domain/Organization/OrganizationAggregate';
|
||||
import { createMockOrganizationRepository, createMockOrganization } from '../../../testUtils';
|
||||
|
||||
describe('Organization Command Handlers - Comprehensive', () => {
|
||||
let mockOrganizationRepository: ReturnType<typeof createMockOrganizationRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOrganizationRepository = createMockOrganizationRepository();
|
||||
});
|
||||
|
||||
describe('CreateOrganizationCommandHandler', () => {
|
||||
let handler: CreateOrganizationCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new CreateOrganizationCommandHandler(mockOrganizationRepository);
|
||||
});
|
||||
|
||||
it('should create organization successfully', async () => {
|
||||
// Arrange
|
||||
const mockOrgData = createMockOrganization({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Test Organization',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactphone: '+1234567890',
|
||||
contactemail: 'john@testorg.com',
|
||||
url: null,
|
||||
state: OrganizationState.REGISTERED
|
||||
});
|
||||
|
||||
mockOrganizationRepository.create.mockResolvedValue(mockOrgData);
|
||||
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Test Organization',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@testorg.com',
|
||||
contactphone: '+1234567890'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Returns ShortOrganizationDto
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Test Organization',
|
||||
state: 0,
|
||||
userinorg: 0,
|
||||
maxOrganizationalDecks: 10
|
||||
});
|
||||
expect(mockOrganizationRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'Test Organization',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@testorg.com',
|
||||
contactphone: '+1234567890',
|
||||
state: OrganizationState.REGISTERED
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create organization with optional URL', async () => {
|
||||
// Arrange
|
||||
const mockOrgData = createMockOrganization({
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Org with URL',
|
||||
contactfname: 'Jane',
|
||||
contactlname: 'Smith',
|
||||
contactphone: '+1987654321',
|
||||
contactemail: 'jane@orgwithurl.com',
|
||||
url: 'https://orgwithurl.com',
|
||||
state: OrganizationState.REGISTERED
|
||||
});
|
||||
|
||||
mockOrganizationRepository.create.mockResolvedValue(mockOrgData);
|
||||
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Org with URL',
|
||||
contactfname: 'Jane',
|
||||
contactlname: 'Smith',
|
||||
contactemail: 'jane@orgwithurl.com',
|
||||
contactphone: '+1987654321',
|
||||
url: 'https://orgwithurl.com'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Org with URL',
|
||||
state: 0,
|
||||
userinorg: 0,
|
||||
maxOrganizationalDecks: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle duplicate organization name error', async () => {
|
||||
// Arrange
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Duplicate Org',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@duplicate.com',
|
||||
contactphone: '+1234567890'
|
||||
};
|
||||
|
||||
const duplicateError = new Error('duplicate key value violates unique constraint "organization_name_unique"');
|
||||
mockOrganizationRepository.create.mockRejectedValue(duplicateError);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Organization with this name or contact email already exists');
|
||||
});
|
||||
|
||||
it('should handle generic database errors', async () => {
|
||||
// Arrange
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Error Org',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@error.com',
|
||||
contactphone: '+1234567890'
|
||||
};
|
||||
|
||||
mockOrganizationRepository.create.mockRejectedValue(new Error('Database connection failed'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create organization');
|
||||
});
|
||||
|
||||
it('should handle non-Error exceptions', async () => {
|
||||
// Arrange
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Non-Error Exception Org',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@exception.com',
|
||||
contactphone: '+1234567890'
|
||||
};
|
||||
|
||||
mockOrganizationRepository.create.mockRejectedValue('String error');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create organization');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateOrganizationCommandHandler', () => {
|
||||
let handler: UpdateOrganizationCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new UpdateOrganizationCommandHandler(mockOrganizationRepository);
|
||||
});
|
||||
|
||||
it('should update organization successfully', async () => {
|
||||
// Arrange
|
||||
const updatedOrgData = createMockOrganization({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Updated Organization',
|
||||
contactemail: 'john@updated.com',
|
||||
url: 'https://updated.com',
|
||||
state: OrganizationState.ACTIVE
|
||||
});
|
||||
|
||||
mockOrganizationRepository.update.mockResolvedValue(updatedOrgData);
|
||||
|
||||
const command: UpdateOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Updated Organization',
|
||||
contactemail: 'john@updated.com',
|
||||
url: 'https://updated.com'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Returns ShortOrganizationDto
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Updated Organization',
|
||||
state: 1,
|
||||
userinorg: 0,
|
||||
maxOrganizationalDecks: 10
|
||||
});
|
||||
expect(mockOrganizationRepository.update).toHaveBeenCalledWith(
|
||||
'550e8400-e29b-41d4-a716-446655440000',
|
||||
command
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when organization not found', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.update.mockResolvedValue(null);
|
||||
|
||||
const command: UpdateOrganizationCommand = {
|
||||
id: 'non-existent-id',
|
||||
name: 'Non-existent Organization'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
expect(mockOrganizationRepository.update).toHaveBeenCalledWith('non-existent-id', command);
|
||||
});
|
||||
|
||||
it('should update organization with partial data', async () => {
|
||||
// Arrange
|
||||
const partialUpdatedOrgData = createMockOrganization({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Original Name',
|
||||
contactemail: 'john@newmail.com',
|
||||
state: OrganizationState.ACTIVE
|
||||
});
|
||||
|
||||
mockOrganizationRepository.update.mockResolvedValue(partialUpdatedOrgData);
|
||||
|
||||
const command: UpdateOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
contactemail: 'john@newmail.com'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Original Name',
|
||||
state: 1,
|
||||
userinorg: 0,
|
||||
maxOrganizationalDecks: 10
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteOrganizationCommandHandler', () => {
|
||||
let handler: DeleteOrganizationCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new DeleteOrganizationCommandHandler(mockOrganizationRepository);
|
||||
});
|
||||
|
||||
it('should perform soft delete successfully', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.softDelete.mockResolvedValue(null);
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
soft: true
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockOrganizationRepository.softDelete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockOrganizationRepository.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should perform hard delete successfully', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.delete.mockResolvedValue(true);
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
soft: false
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockOrganizationRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockOrganizationRepository.softDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should default to hard delete when soft flag not specified', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.delete.mockResolvedValue(true);
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockOrganizationRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockOrganizationRepository.softDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle soft delete with repository error gracefully', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.softDelete.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
soft: true
|
||||
};
|
||||
|
||||
// Act & Assert - Handler doesn't catch errors, they bubble up
|
||||
await expect(handler.execute(command)).rejects.toThrow('Database error');
|
||||
});
|
||||
|
||||
it('should handle hard delete with repository error gracefully', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.delete.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
soft: false
|
||||
};
|
||||
|
||||
// Act & Assert - Handler doesn't catch errors, they bubble up
|
||||
await expect(handler.execute(command)).rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user