431 lines
14 KiB
TypeScript
431 lines
14 KiB
TypeScript
// 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();
|
|
});
|
|
});
|
|
});
|