// 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; 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; let mockOrgRepository: jest.Mocked; let mockJwtService: jest.Mocked; 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; 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; 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; 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; 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; let mockOrgRepository: jest.Mocked; let mockJwtService: jest.Mocked; 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(); }); }); });