Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user