420 lines
16 KiB
JavaScript
420 lines
16 KiB
JavaScript
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');
|
|
});
|
|
});
|
|
});
|
|
}); |