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:
+402
@@ -0,0 +1,402 @@
|
||||
import { CreateContactCommandHandler } from '../../../../src/Application/Contact/commands/CreateContactCommandHandler';
|
||||
import { UpdateContactCommandHandler } from '../../../../src/Application/Contact/commands/UpdateContactCommandHandler';
|
||||
import { DeleteContactCommandHandler } from '../../../../src/Application/Contact/commands/DeleteContactCommandHandler';
|
||||
import { CreateContactCommand } from '../../../../src/Application/Contact/commands/CreateContactCommand';
|
||||
import { UpdateContactCommand } from '../../../../src/Application/Contact/commands/UpdateContactCommand';
|
||||
import { DeleteContactCommand } from '../../../../src/Application/Contact/commands/DeleteContactCommand';
|
||||
import { ContactType, ContactState } from '../../../../src/Domain/Contact/ContactAggregate';
|
||||
import { createMockContactRepository, createMockContact } from '../../../testUtils';
|
||||
|
||||
describe('Contact Command Handlers - Comprehensive', () => {
|
||||
let mockContactRepository: ReturnType<typeof createMockContactRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContactRepository = createMockContactRepository();
|
||||
});
|
||||
|
||||
describe('CreateContactCommandHandler', () => {
|
||||
let handler: CreateContactCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new CreateContactCommandHandler(mockContactRepository);
|
||||
});
|
||||
|
||||
it('should create contact successfully with all fields', async () => {
|
||||
// Arrange
|
||||
const mockContactData = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
userid: '123e4567-e89b-12d3-a456-426614174000',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'Test question',
|
||||
state: ContactState.ACTIVE
|
||||
});
|
||||
|
||||
mockContactRepository.create.mockResolvedValue(mockContactData);
|
||||
|
||||
const command: CreateContactCommand = {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
userid: '123e4567-e89b-12d3-a456-426614174000',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'Test question'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Returns ShortContactDto
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
type: ContactType.QUESTION,
|
||||
state: ContactState.ACTIVE,
|
||||
createDate: expect.any(Date)
|
||||
});
|
||||
expect(mockContactRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
userid: '123e4567-e89b-12d3-a456-426614174000',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'Test question',
|
||||
state: ContactState.ACTIVE
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create contact without userid (anonymous)', async () => {
|
||||
// Arrange
|
||||
const mockContactData = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Anonymous User',
|
||||
email: 'anon@example.com',
|
||||
userid: null,
|
||||
type: ContactType.BUG,
|
||||
txt: 'Bug report',
|
||||
state: ContactState.ACTIVE
|
||||
});
|
||||
|
||||
mockContactRepository.create.mockResolvedValue(mockContactData);
|
||||
|
||||
const command: CreateContactCommand = {
|
||||
name: 'Anonymous User',
|
||||
email: 'anon@example.com',
|
||||
type: ContactType.BUG,
|
||||
txt: 'Bug report'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Anonymous User',
|
||||
email: 'anon@example.com',
|
||||
type: ContactType.BUG,
|
||||
state: ContactState.ACTIVE,
|
||||
createDate: expect.any(Date)
|
||||
});
|
||||
expect(mockContactRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userid: null
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create contact with different contact types', async () => {
|
||||
const testCases = [
|
||||
{ type: ContactType.BUG, description: 'Bug report' },
|
||||
{ type: ContactType.PROBLEM, description: 'Problem report' },
|
||||
{ type: ContactType.QUESTION, description: 'Question' },
|
||||
{ type: ContactType.SALES, description: 'Sales inquiry' },
|
||||
{ type: ContactType.OTHER, description: 'Other inquiry' }
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
// Arrange
|
||||
const mockContactData = createMockContact({
|
||||
type: testCase.type,
|
||||
txt: testCase.description
|
||||
});
|
||||
|
||||
mockContactRepository.create.mockResolvedValue(mockContactData);
|
||||
|
||||
const command: CreateContactCommand = {
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
type: testCase.type,
|
||||
txt: testCase.description
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result.type).toBe(testCase.type);
|
||||
expect(mockContactRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
txt: testCase.description
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
// Arrange
|
||||
const command: CreateContactCommand = {
|
||||
name: 'Error User',
|
||||
email: 'error@example.com',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'This will cause an error'
|
||||
};
|
||||
|
||||
mockContactRepository.create.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create contact');
|
||||
});
|
||||
|
||||
it('should handle non-Error exceptions', async () => {
|
||||
// Arrange
|
||||
const command: CreateContactCommand = {
|
||||
name: 'Exception User',
|
||||
email: 'exception@example.com',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'This will cause an exception'
|
||||
};
|
||||
|
||||
mockContactRepository.create.mockRejectedValue('String error');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create contact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateContactCommandHandler', () => {
|
||||
let handler: UpdateContactCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new UpdateContactCommandHandler(mockContactRepository);
|
||||
});
|
||||
|
||||
it('should update contact with admin response', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
adminResponse: null,
|
||||
state: ContactState.ACTIVE
|
||||
});
|
||||
|
||||
const updatedContact = createMockContact({
|
||||
...existingContact,
|
||||
adminResponse: 'Thank you for your inquiry',
|
||||
state: ContactState.RESOLVED,
|
||||
responseDate: new Date(),
|
||||
respondedBy: 'admin123'
|
||||
});
|
||||
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.update.mockResolvedValue(updatedContact);
|
||||
|
||||
const command: UpdateContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
adminResponse: 'Thank you for your inquiry'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Returns DetailContactDto
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: expect.any(String),
|
||||
email: expect.any(String),
|
||||
userid: expect.any(String),
|
||||
type: expect.any(Number),
|
||||
txt: expect.any(String),
|
||||
state: ContactState.RESOLVED,
|
||||
createDate: expect.any(Date),
|
||||
updateDate: expect.any(Date),
|
||||
adminResponse: 'Thank you for your inquiry',
|
||||
responseDate: expect.any(Date),
|
||||
respondedBy: 'admin123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should update contact state', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
state: ContactState.ACTIVE
|
||||
});
|
||||
|
||||
const updatedContact = createMockContact({
|
||||
...existingContact,
|
||||
state: ContactState.RESOLVED
|
||||
});
|
||||
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.update.mockResolvedValue(updatedContact);
|
||||
|
||||
const command: UpdateContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
state: ContactState.RESOLVED
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result.state).toBe(ContactState.RESOLVED);
|
||||
});
|
||||
|
||||
it('should throw error when contact not found', async () => {
|
||||
// Arrange
|
||||
mockContactRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const command: UpdateContactCommand = {
|
||||
id: 'non-existent-id',
|
||||
adminResponse: 'Response'
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Contact not found');
|
||||
});
|
||||
|
||||
it('should handle repository errors during update', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact();
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.update.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: UpdateContactCommand = {
|
||||
id: 'existing-id',
|
||||
adminResponse: 'Response'
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to update contact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteContactCommandHandler', () => {
|
||||
let handler: DeleteContactCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new DeleteContactCommandHandler(mockContactRepository);
|
||||
});
|
||||
|
||||
it('should perform soft delete successfully', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000'
|
||||
});
|
||||
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.softDelete.mockResolvedValue(null);
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
hard: false
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockContactRepository.findById).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockContactRepository.softDelete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockContactRepository.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should perform hard delete successfully', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000'
|
||||
});
|
||||
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.delete.mockResolvedValue(true);
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
hard: true
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockContactRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockContactRepository.softDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should default to soft delete when hard flag not specified', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact();
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.softDelete.mockResolvedValue(null);
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockContactRepository.softDelete).toHaveBeenCalled();
|
||||
expect(mockContactRepository.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when contact not found', async () => {
|
||||
// Arrange
|
||||
mockContactRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: 'non-existent-id',
|
||||
hard: false
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Contact not found');
|
||||
});
|
||||
|
||||
it('should handle repository errors during deletion', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact();
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.softDelete.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: 'existing-id',
|
||||
hard: false
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to delete contact');
|
||||
});
|
||||
|
||||
it('should handle hard delete repository errors', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact();
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.delete.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: 'existing-id',
|
||||
hard: true
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to delete contact');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user