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:
+430
@@ -0,0 +1,430 @@
|
||||
// Comprehensive test coverage for User Command Handlers
|
||||
import { CreateUserCommand } from '../../../../src/Application/User/commands/CreateUserCommand';
|
||||
import { CreateUserCommandHandler } from '../../../../src/Application/User/commands/CreateUserCommandHandler';
|
||||
import { LoginCommand } from '../../../../src/Application/User/commands/LoginCommand';
|
||||
import { LoginCommandHandler } from '../../../../src/Application/User/commands/LoginCommandHandler';
|
||||
import { UpdateUserCommand } from '../../../../src/Application/User/commands/UpdateUserCommand';
|
||||
import { UpdateUserCommandHandler } from '../../../../src/Application/User/commands/UpdateUserCommandHandler';
|
||||
import { DeactivateUserCommand } from '../../../../src/Application/User/commands/DeactivateUserCommand';
|
||||
import { DeactivateUserCommandHandler } from '../../../../src/Application/User/commands/DeactivateUserCommandHandler';
|
||||
import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository';
|
||||
import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository';
|
||||
import { JWTService } from '../../../../src/Application/Services/JWTService';
|
||||
import { PasswordService } from '../../../../src/Application/Services/PasswordService';
|
||||
import { UserState } from '../../../../src/Domain/User/UserAggregate';
|
||||
import {
|
||||
createMockUser,
|
||||
createMockUserRepository,
|
||||
createMockOrganizationRepository,
|
||||
createMockJWTService
|
||||
} from '../../../testUtils';
|
||||
|
||||
// Mock PasswordService static methods
|
||||
jest.mock('../../../../src/Application/Services/PasswordService', () => ({
|
||||
PasswordService: {
|
||||
validatePasswordStrength: jest.fn().mockReturnValue({ isValid: true, errors: [] }),
|
||||
hashPassword: jest.fn().mockResolvedValue('hashed-password'),
|
||||
verifyPassword: jest.fn().mockResolvedValue(true)
|
||||
}
|
||||
}));
|
||||
|
||||
describe('User Command Handlers - Comprehensive Coverage', () => {
|
||||
describe('CreateUserCommandHandler', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let handler: CreateUserCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
handler = new CreateUserCommandHandler(mockUserRepository);
|
||||
});
|
||||
|
||||
it('should create a new user successfully', async () => {
|
||||
// Arrange
|
||||
const command: CreateUserCommand = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: command.username,
|
||||
email: command.email,
|
||||
state: UserState.REGISTERED_NOT_VERIFIED
|
||||
});
|
||||
|
||||
// CreateUserCommandHandler doesn't check existing users - goes directly to create
|
||||
mockUserRepository.create.mockResolvedValue(mockUser);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
// CreateUserCommandHandler doesn't call findByUsername/findByEmail
|
||||
expect(mockUserRepository.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when username already exists', async () => {
|
||||
// Arrange
|
||||
const command: CreateUserCommand = {
|
||||
username: 'existinguser',
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
// Simulate database constraint error for duplicate username
|
||||
mockUserRepository.create.mockRejectedValue(new Error('duplicate key value violates unique constraint'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('User with this username or email already exists');
|
||||
});
|
||||
|
||||
it('should throw error when email already exists', async () => {
|
||||
// Arrange
|
||||
const command: CreateUserCommand = {
|
||||
username: 'testuser',
|
||||
email: 'existing@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
// Simulate database constraint error for duplicate email
|
||||
mockUserRepository.create.mockRejectedValue(new Error('unique constraint violation'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('User with this username or email already exists');
|
||||
});
|
||||
|
||||
it('should handle repository errors', async () => {
|
||||
// Arrange
|
||||
const command: CreateUserCommand = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(null);
|
||||
mockUserRepository.findByEmail.mockResolvedValue(null);
|
||||
mockUserRepository.create.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create user');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LoginCommandHandler', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let mockOrgRepository: jest.Mocked<IOrganizationRepository>;
|
||||
let mockJwtService: jest.Mocked<JWTService>;
|
||||
let handler: LoginCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
mockOrgRepository = createMockOrganizationRepository();
|
||||
mockJwtService = createMockJWTService();
|
||||
handler = new LoginCommandHandler(mockUserRepository, mockJwtService, mockOrgRepository);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set default PasswordService behavior
|
||||
const mockPasswordService = PasswordService as jest.Mocked<typeof PasswordService>;
|
||||
mockPasswordService.verifyPassword.mockResolvedValue(true); // Default to valid password
|
||||
});
|
||||
|
||||
it('should login user with valid credentials', async () => {
|
||||
// Arrange
|
||||
const command: LoginCommand = {
|
||||
username: 'testuser',
|
||||
password: 'Password123!'
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: command.username,
|
||||
state: UserState.VERIFIED_REGULAR
|
||||
});
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(mockUser);
|
||||
mockJwtService.create.mockReturnValue('jwt-token');
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.token).toBe('jwt-token');
|
||||
expect(mockJwtService.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle user not found', async () => {
|
||||
// Arrange
|
||||
const command: LoginCommand = {
|
||||
username: 'nonexistent',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(null);
|
||||
|
||||
// Act & Assert
|
||||
const result = await handler.execute(command);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle invalid password', async () => {
|
||||
// Arrange
|
||||
const command: LoginCommand = {
|
||||
username: 'testuser',
|
||||
password: 'wrongpassword'
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: command.username,
|
||||
password: 'hashedpassword'
|
||||
});
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(mockUser);
|
||||
|
||||
// Mock password verification to return false for wrong password
|
||||
const mockPasswordService = PasswordService as jest.Mocked<typeof PasswordService>;
|
||||
mockPasswordService.verifyPassword.mockResolvedValue(false);
|
||||
|
||||
// Act & Assert
|
||||
const result = await handler.execute(command);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle unverified user', async () => {
|
||||
// Arrange - LoginCommandHandler doesn't reject unverified users, it processes them normally
|
||||
const command: LoginCommand = {
|
||||
username: 'testuser',
|
||||
password: 'Password123!'
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: command.username,
|
||||
password: 'hashedpassword',
|
||||
state: UserState.REGISTERED_NOT_VERIFIED
|
||||
});
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(mockUser);
|
||||
mockJwtService.create.mockReturnValue('jwt-token');
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - LoginCommandHandler processes unverified users normally
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.user).toBeDefined();
|
||||
expect(result!.token).toBe('jwt-token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateUserCommandHandler', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let handler: UpdateUserCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
handler = new UpdateUserCommandHandler(mockUserRepository);
|
||||
});
|
||||
|
||||
it('should update user successfully', async () => {
|
||||
// Arrange
|
||||
const command: UpdateUserCommand = {
|
||||
id: 'user-123',
|
||||
email: 'newemail@example.com'
|
||||
};
|
||||
|
||||
const existingUser = createMockUser({ id: command.id });
|
||||
const updatedUser = createMockUser({
|
||||
id: command.id,
|
||||
email: command.email
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(existingUser);
|
||||
mockUserRepository.update.mockResolvedValue(updatedUser);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(mockUserRepository.update).toHaveBeenCalledWith(command.id, expect.any(Object));
|
||||
});
|
||||
|
||||
it('should return null when user not found', async () => {
|
||||
// Arrange
|
||||
const command: UpdateUserCommand = {
|
||||
id: 'nonexistent-user',
|
||||
email: 'newemail@example.com'
|
||||
};
|
||||
|
||||
mockUserRepository.update.mockResolvedValue(null); // UpdateUserCommandHandler calls update directly, not findById first
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
expect(mockUserRepository.update).toHaveBeenCalledWith(command.id, expect.any(Object));
|
||||
});
|
||||
|
||||
it('should handle partial updates', async () => {
|
||||
// Arrange
|
||||
const command: UpdateUserCommand = {
|
||||
id: 'user-123',
|
||||
username: 'newusername'
|
||||
};
|
||||
|
||||
const existingUser = createMockUser({ id: command.id });
|
||||
const updatedUser = createMockUser({
|
||||
id: command.id,
|
||||
username: command.username
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(existingUser);
|
||||
mockUserRepository.update.mockResolvedValue(updatedUser);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeactivateUserCommandHandler', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let handler: DeactivateUserCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
handler = new DeactivateUserCommandHandler(mockUserRepository);
|
||||
});
|
||||
|
||||
it('should deactivate user successfully', async () => {
|
||||
// Arrange
|
||||
const command: DeactivateUserCommand = {
|
||||
id: 'user-123'
|
||||
};
|
||||
|
||||
const deactivatedUser = createMockUser({
|
||||
id: command.id,
|
||||
state: UserState.DEACTIVATED
|
||||
});
|
||||
|
||||
mockUserRepository.deactivate.mockResolvedValue(deactivatedUser);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockUserRepository.deactivate).toHaveBeenCalledWith(command.id);
|
||||
});
|
||||
|
||||
it('should handle repository errors', async () => {
|
||||
// Arrange
|
||||
const command: DeactivateUserCommand = {
|
||||
id: 'user-123'
|
||||
};
|
||||
|
||||
mockUserRepository.deactivate.mockRejectedValue(new Error('Deactivation failed'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Deactivation failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cross-Command Integration Tests', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let mockOrgRepository: jest.Mocked<IOrganizationRepository>;
|
||||
let mockJwtService: jest.Mocked<JWTService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
mockOrgRepository = createMockOrganizationRepository();
|
||||
mockJwtService = createMockJWTService();
|
||||
});
|
||||
|
||||
it('should create user and then login', async () => {
|
||||
// Arrange
|
||||
const createHandler = new CreateUserCommandHandler(mockUserRepository);
|
||||
const loginHandler = new LoginCommandHandler(mockUserRepository, mockJwtService, mockOrgRepository);
|
||||
|
||||
const createCommand: CreateUserCommand = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
const loginCommand: LoginCommand = {
|
||||
username: 'testuser',
|
||||
password: 'Password123!' // Strong password
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: createCommand.username,
|
||||
email: createCommand.email,
|
||||
state: UserState.VERIFIED_REGULAR
|
||||
});
|
||||
|
||||
// Mock create user flow
|
||||
mockUserRepository.findByUsername.mockResolvedValueOnce(null);
|
||||
mockUserRepository.findByEmail.mockResolvedValue(null);
|
||||
mockUserRepository.create.mockResolvedValue(mockUser);
|
||||
|
||||
// Mock login flow
|
||||
mockUserRepository.findByUsername.mockResolvedValueOnce(mockUser);
|
||||
mockJwtService.create.mockReturnValue('jwt-token');
|
||||
|
||||
// Act
|
||||
const createResult = await createHandler.execute(createCommand);
|
||||
const loginResult = await loginHandler.execute(loginCommand);
|
||||
|
||||
// Assert
|
||||
expect(createResult).toBeDefined();
|
||||
expect(loginResult).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update user after creation', async () => {
|
||||
// Arrange
|
||||
const updateHandler = new UpdateUserCommandHandler(mockUserRepository);
|
||||
|
||||
const updateCommand: UpdateUserCommand = {
|
||||
id: 'user-123',
|
||||
email: 'updated@example.com'
|
||||
};
|
||||
|
||||
const existingUser = createMockUser({ id: updateCommand.id });
|
||||
const updatedUser = createMockUser({
|
||||
id: updateCommand.id,
|
||||
email: updateCommand.email
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(existingUser);
|
||||
mockUserRepository.update.mockResolvedValue(updatedUser);
|
||||
|
||||
// Act
|
||||
const result = await updateHandler.execute(updateCommand);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user