const User = require('../../src/models/User'); const bcrypt = require('bcrypt'); const crypto = require('crypto'); const dbConnection = require('../../src/database/connection'); // Mock dependencies jest.mock('bcrypt'); jest.mock('crypto'); jest.mock('../../src/database/connection'); describe('User Model Unit Tests', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('Password Hashing', () => { describe('hashPassword', () => { it('should hash password with bcrypt', async () => { const password = 'testPassword123'; const hashedPassword = 'hashed-password'; bcrypt.hash.mockResolvedValue(hashedPassword); const result = await User.hashPassword(password); expect(bcrypt.hash).toHaveBeenCalledWith(password, 12); expect(result).toBe(hashedPassword); }); }); describe('verifyPassword', () => { it('should verify password correctly', async () => { const password = 'testPassword123'; const hash = 'hashed-password'; bcrypt.compare.mockResolvedValue(true); const result = await User.verifyPassword(password, hash); expect(bcrypt.compare).toHaveBeenCalledWith(password, hash); expect(result).toBe(true); }); it('should return false for incorrect password', async () => { const password = 'wrongPassword'; const hash = 'hashed-password'; bcrypt.compare.mockResolvedValue(false); const result = await User.verifyPassword(password, hash); expect(result).toBe(false); }); }); }); describe('Validation', () => { describe('validateEmail', () => { it('should validate correct email formats', () => { const validEmails = [ 'test@example.com', 'user.name@domain.co.uk', 'user+tag@example.org', 'user123@test-domain.com' ]; validEmails.forEach(email => { expect(User.validateEmail(email)).toBe(true); }); }); it('should reject invalid email formats', () => { const invalidEmails = [ 'invalid-email', '@example.com', 'user@', 'user@.com', 'user..name@example.com', 'user name@example.com' ]; invalidEmails.forEach(email => { expect(User.validateEmail(email)).toBe(false); }); }); }); describe('validatePassword', () => { it('should validate strong passwords', () => { const strongPasswords = [ 'Password123!', 'MyStr0ng@Pass', 'C0mplex#Password', 'Secure123$' ]; strongPasswords.forEach(password => { const result = User.validatePassword(password); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); }); }); it('should reject weak passwords', () => { const weakPasswords = [ { password: 'short', expectedErrors: ['Password must be at least 8 characters long', 'Password must contain at least one uppercase letter', 'Password must contain at least one number', 'Password must contain at least one special character'] }, { password: 'nouppercase123!', expectedErrors: ['Password must contain at least one uppercase letter'] }, { password: 'NOLOWERCASE123!', expectedErrors: ['Password must contain at least one lowercase letter'] }, { password: 'NoNumbers!', expectedErrors: ['Password must contain at least one number'] }, { password: 'NoSpecialChars123', expectedErrors: ['Password must contain at least one special character'] } ]; weakPasswords.forEach(({ password, expectedErrors }) => { const result = User.validatePassword(password); expect(result.isValid).toBe(false); expect(result.errors).toEqual(expect.arrayContaining(expectedErrors)); }); }); it('should handle null or undefined password', () => { const result = User.validatePassword(null); expect(result.isValid).toBe(false); expect(result.errors).toContain('Password must be at least 8 characters long'); }); }); }); describe('Token Generation', () => { describe('generateToken', () => { it('should generate a random token', () => { const mockToken = 'random-hex-token'; const mockBuffer = Buffer.from('random-bytes'); crypto.randomBytes.mockReturnValue(mockBuffer); mockBuffer.toString = jest.fn().mockReturnValue(mockToken); const result = User.generateToken(); expect(crypto.randomBytes).toHaveBeenCalledWith(32); expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); expect(result).toBe(mockToken); }); }); }); describe('Database Operations', () => { describe('create', () => { it('should create a new user successfully', async () => { const userData = { email: 'test@example.com', password: 'Password123!' }; const mockHashedPassword = 'hashed-password'; const mockToken = 'verification-token'; const mockUserData = { id: 'user-123', email: 'test@example.com', password_hash: mockHashedPassword, verification_token: mockToken }; User.findByEmail = jest.fn().mockResolvedValue(null); bcrypt.hash.mockResolvedValue(mockHashedPassword); crypto.randomBytes.mockReturnValue(Buffer.from('random')); Buffer.prototype.toString = jest.fn().mockReturnValue(mockToken); dbConnection.query.mockResolvedValue({ rows: [mockUserData] }); const result = await User.create(userData); expect(User.findByEmail).toHaveBeenCalledWith('test@example.com'); expect(bcrypt.hash).toHaveBeenCalledWith('Password123!', 12); expect(dbConnection.query).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO users'), ['test@example.com', mockHashedPassword, mockToken] ); expect(result).toBeInstanceOf(User); expect(result.email).toBe('test@example.com'); }); it('should reject invalid email', async () => { const userData = { email: 'invalid-email', password: 'Password123!' }; await expect(User.create(userData)).rejects.toThrow('Invalid email format'); }); it('should reject weak password', async () => { const userData = { email: 'test@example.com', password: 'weak' }; await expect(User.create(userData)).rejects.toThrow('Password validation failed'); }); it('should reject duplicate email', async () => { const userData = { email: 'test@example.com', password: 'Password123!' }; const existingUser = new User({ id: 'existing-user', email: 'test@example.com' }); User.findByEmail = jest.fn().mockResolvedValue(existingUser); await expect(User.create(userData)).rejects.toThrow('User with this email already exists'); }); }); describe('findByEmail', () => { beforeEach(() => { // Reset the mock implementation for each test jest.resetModules(); jest.clearAllMocks(); }); it('should find user by email', async () => { const mockUserData = { id: 'user-123', email: 'test@example.com' }; // Mock the dbErrorHandler wrapper const { dbErrorHandler } = require('../../src/middleware/errorHandler'); jest.mock('../../src/middleware/errorHandler', () => ({ dbErrorHandler: jest.fn((fn) => fn()) })); dbConnection.query.mockResolvedValue({ rows: [mockUserData] }); const result = await User.findByEmail('test@example.com'); expect(dbConnection.query).toHaveBeenCalledWith( 'SELECT * FROM users WHERE email = $1', ['test@example.com'] ); expect(result).toBeInstanceOf(User); expect(result.email).toBe('test@example.com'); }); it('should return null if user not found', async () => { dbConnection.query.mockResolvedValue({ rows: [] }); const result = await User.findByEmail('nonexistent@example.com'); expect(result).toBeNull(); }); }); describe('findById', () => { it('should find user by ID', async () => { const mockUserData = { id: 'user-123', email: 'test@example.com' }; dbConnection.query.mockResolvedValue({ rows: [mockUserData] }); const result = await User.findById('user-123'); expect(dbConnection.query).toHaveBeenCalledWith( 'SELECT * FROM users WHERE id = $1', ['user-123'] ); expect(result).toBeInstanceOf(User); expect(result.id).toBe('user-123'); }); }); describe('authenticate', () => { it('should authenticate user with correct credentials', async () => { const mockUser = new User({ id: 'user-123', email: 'test@example.com', password_hash: 'hashed-password' }); User.findByEmail = jest.fn().mockResolvedValue(mockUser); bcrypt.compare.mockResolvedValue(true); mockUser.updateLastLogin = jest.fn().mockResolvedValue(true); const result = await User.authenticate('test@example.com', 'password123'); expect(User.findByEmail).toHaveBeenCalledWith('test@example.com'); expect(bcrypt.compare).toHaveBeenCalledWith('password123', 'hashed-password'); expect(mockUser.updateLastLogin).toHaveBeenCalled(); expect(result).toBe(mockUser); }); it('should return null for non-existent user', async () => { User.findByEmail = jest.fn().mockResolvedValue(null); const result = await User.authenticate('nonexistent@example.com', 'password123'); expect(result).toBeNull(); }); it('should return null for incorrect password', async () => { const mockUser = new User({ id: 'user-123', email: 'test@example.com', password_hash: 'hashed-password' }); User.findByEmail = jest.fn().mockResolvedValue(mockUser); bcrypt.compare.mockResolvedValue(false); const result = await User.authenticate('test@example.com', 'wrongpassword'); expect(result).toBeNull(); }); }); }); describe('Instance Methods', () => { describe('verifyEmail', () => { it('should verify user email', async () => { const user = new User({ id: 'user-123', is_verified: false }); dbConnection.query.mockResolvedValue({ rowCount: 1 }); const result = await user.verifyEmail(); expect(dbConnection.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE users SET is_verified = true'), ['user-123'] ); expect(result).toBe(true); expect(user.is_verified).toBe(true); }); }); describe('updatePassword', () => { it('should update user password', async () => { const user = new User({ id: 'user-123' }); const newPassword = 'NewPassword123!'; const hashedPassword = 'new-hashed-password'; bcrypt.hash.mockResolvedValue(hashedPassword); dbConnection.query.mockResolvedValue({ rowCount: 1 }); const result = await user.updatePassword(newPassword); expect(bcrypt.hash).toHaveBeenCalledWith(newPassword, 12); expect(dbConnection.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE users SET password_hash'), [hashedPassword, 'user-123'] ); expect(result).toBe(true); expect(user.password_hash).toBe(hashedPassword); }); it('should reject weak password', async () => { const user = new User({ id: 'user-123' }); await expect(user.updatePassword('weak')).rejects.toThrow('Password validation failed'); }); }); describe('setResetToken', () => { it('should set password reset token', async () => { const user = new User({ id: 'user-123' }); const mockToken = 'reset-token'; crypto.randomBytes.mockReturnValue(Buffer.from('random')); Buffer.prototype.toString = jest.fn().mockReturnValue(mockToken); dbConnection.query.mockResolvedValue({ rowCount: 1 }); const result = await user.setResetToken(); expect(dbConnection.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE users SET reset_token'), [mockToken, expect.any(Date), 'user-123'] ); expect(result).toBe(mockToken); expect(user.reset_token).toBe(mockToken); }); }); describe('toSafeObject', () => { it('should return safe user object without sensitive data', () => { const user = new User({ id: 'user-123', email: 'test@example.com', password_hash: 'sensitive-hash', verification_token: 'sensitive-token', reset_token: 'sensitive-reset-token', is_verified: true, created_at: new Date(), updated_at: new Date(), last_login: new Date() }); const safeObject = user.toSafeObject(); expect(safeObject).toHaveProperty('id'); expect(safeObject).toHaveProperty('email'); expect(safeObject).toHaveProperty('is_verified'); expect(safeObject).toHaveProperty('created_at'); expect(safeObject).toHaveProperty('updated_at'); expect(safeObject).toHaveProperty('last_login'); expect(safeObject).not.toHaveProperty('password_hash'); expect(safeObject).not.toHaveProperty('verification_token'); expect(safeObject).not.toHaveProperty('reset_token'); }); }); }); });