271 lines
8.2 KiB
TypeScript
271 lines
8.2 KiB
TypeScript
import { PasswordService } from '../../../src/Application/Services/PasswordService';
|
|
|
|
// Mock bcrypt completely
|
|
jest.mock('bcrypt');
|
|
|
|
describe('PasswordService', () => {
|
|
// Mock functions for bcrypt
|
|
const mockBcryptHash = jest.fn();
|
|
const mockBcryptCompare = jest.fn();
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
// Reset console.error mock to avoid noise in tests
|
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
// Setup bcrypt mocks
|
|
const bcrypt = require('bcrypt');
|
|
bcrypt.hash = mockBcryptHash;
|
|
bcrypt.compare = mockBcryptCompare;
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
describe('hashPassword', () => {
|
|
it('should hash a valid password successfully', async () => {
|
|
// Arrange
|
|
const password = 'validPassword123!';
|
|
const hashedPassword = '$2b$12$hashed.password.here';
|
|
|
|
mockBcryptHash.mockResolvedValue(hashedPassword);
|
|
|
|
// Act
|
|
const result = await PasswordService.hashPassword(password);
|
|
|
|
// Assert
|
|
expect(result).toBe(hashedPassword);
|
|
expect(mockBcryptHash).toHaveBeenCalledWith(password, 12);
|
|
});
|
|
|
|
it('should throw error for empty password', async () => {
|
|
// Arrange
|
|
const password = '';
|
|
|
|
// Act & Assert
|
|
await expect(PasswordService.hashPassword(password)).rejects.toThrow('Password must be a non-empty string');
|
|
expect(mockBcryptHash).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should throw error for non-string password', async () => {
|
|
// Arrange
|
|
const password = null as any;
|
|
|
|
// Act & Assert
|
|
await expect(PasswordService.hashPassword(password)).rejects.toThrow('Password must be a non-empty string');
|
|
expect(mockBcryptHash).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle bcrypt errors and throw generic error', async () => {
|
|
// Arrange
|
|
const password = 'validPassword123!';
|
|
mockBcryptHash.mockRejectedValue(new Error('Bcrypt error'));
|
|
|
|
// Act & Assert
|
|
await expect(PasswordService.hashPassword(password)).rejects.toThrow('Failed to hash password');
|
|
expect(mockBcryptHash).toHaveBeenCalledWith(password, 12);
|
|
});
|
|
});
|
|
|
|
describe('verifyPassword', () => {
|
|
it('should return true for matching password and hash', async () => {
|
|
// Arrange
|
|
const password = 'validPassword123!';
|
|
const hashedPassword = '$2b$12$hashed.password.here';
|
|
|
|
mockBcryptCompare.mockResolvedValue(true);
|
|
|
|
// Act
|
|
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword);
|
|
});
|
|
|
|
it('should return false for non-matching password and hash', async () => {
|
|
// Arrange
|
|
const password = 'wrongPassword';
|
|
const hashedPassword = '$2b$12$hashed.password.here';
|
|
|
|
mockBcryptCompare.mockResolvedValue(false);
|
|
|
|
// Act
|
|
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword);
|
|
});
|
|
|
|
it('should return false for empty password', async () => {
|
|
// Arrange
|
|
const password = '';
|
|
const hashedPassword = '$2b$12$hashed.password.here';
|
|
|
|
// Act
|
|
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
expect(mockBcryptCompare).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return false for empty hashed password', async () => {
|
|
// Arrange
|
|
const password = 'validPassword123!';
|
|
const hashedPassword = '';
|
|
|
|
// Act
|
|
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
expect(mockBcryptCompare).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return false for non-string inputs', async () => {
|
|
// Arrange
|
|
const password = null as any;
|
|
const hashedPassword = undefined as any;
|
|
|
|
// Act
|
|
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
expect(mockBcryptCompare).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return false when bcrypt throws error', async () => {
|
|
// Arrange
|
|
const password = 'validPassword123!';
|
|
const hashedPassword = '$2b$12$hashed.password.here';
|
|
|
|
mockBcryptCompare.mockRejectedValue(new Error('Bcrypt compare error'));
|
|
|
|
// Act
|
|
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword);
|
|
});
|
|
});
|
|
|
|
describe('validatePasswordStrength', () => {
|
|
it('should return valid for strong password', () => {
|
|
// Arrange
|
|
const password = 'StrongPass123!';
|
|
|
|
// Act
|
|
const result = PasswordService.validatePasswordStrength(password);
|
|
|
|
// Assert
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.errors).toEqual([]);
|
|
});
|
|
|
|
it('should return invalid for short password', () => {
|
|
// Arrange
|
|
const password = 'Short1!';
|
|
|
|
// Act
|
|
const result = PasswordService.validatePasswordStrength(password);
|
|
|
|
// Assert
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toContain('Password must be at least 8 characters long');
|
|
});
|
|
|
|
it('should return invalid for password without uppercase', () => {
|
|
// Arrange
|
|
const password = 'lowercase123!';
|
|
|
|
// Act
|
|
const result = PasswordService.validatePasswordStrength(password);
|
|
|
|
// Assert
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toContain('Password must contain at least one uppercase letter');
|
|
});
|
|
|
|
it('should return invalid for password without lowercase', () => {
|
|
// Arrange
|
|
const password = 'UPPERCASE123!';
|
|
|
|
// Act
|
|
const result = PasswordService.validatePasswordStrength(password);
|
|
|
|
// Assert
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toContain('Password must contain at least one lowercase letter');
|
|
});
|
|
|
|
it('should return invalid for password without numbers', () => {
|
|
// Arrange
|
|
const password = 'NoNumbers!';
|
|
|
|
// Act
|
|
const result = PasswordService.validatePasswordStrength(password);
|
|
|
|
// Assert
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toContain('Password must contain at least one number');
|
|
});
|
|
|
|
it('should return invalid for password without special characters', () => {
|
|
// Arrange
|
|
const password = 'NoSpecial123';
|
|
|
|
// Act
|
|
const result = PasswordService.validatePasswordStrength(password);
|
|
|
|
// Assert
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toContain('Password must contain at least one special character');
|
|
});
|
|
|
|
it('should return multiple errors for weak password', () => {
|
|
// Arrange
|
|
const password = 'weak';
|
|
|
|
// Act
|
|
const result = PasswordService.validatePasswordStrength(password);
|
|
|
|
// Assert
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toHaveLength(4);
|
|
expect(result.errors).toContain('Password must be at least 8 characters long');
|
|
expect(result.errors).toContain('Password must contain at least one uppercase letter');
|
|
expect(result.errors).toContain('Password must contain at least one number');
|
|
expect(result.errors).toContain('Password must contain at least one special character');
|
|
});
|
|
|
|
it('should handle empty password', () => {
|
|
// Arrange
|
|
const password = '';
|
|
|
|
// Act
|
|
const result = PasswordService.validatePasswordStrength(password);
|
|
|
|
// Assert
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toContain('Password must be provided as a string');
|
|
});
|
|
|
|
it('should handle null password', () => {
|
|
// Arrange
|
|
const password = null as any;
|
|
|
|
// Act
|
|
const result = PasswordService.validatePasswordStrength(password);
|
|
|
|
// Assert
|
|
expect(result.isValid).toBe(false);
|
|
expect(result.errors).toContain('Password must be provided as a string');
|
|
});
|
|
});
|
|
});
|