Some checks failed
CI / lint-typecheck (pull_request) Failing after 12s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
216 lines
7.3 KiB
TypeScript
216 lines
7.3 KiB
TypeScript
/**
|
|
* Domain Service Tests: PasswordHashingService
|
|
*
|
|
* Tests for password hashing and verification business logic
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { PasswordHashingService } from './PasswordHashingService';
|
|
|
|
describe('PasswordHashingService', () => {
|
|
let service: PasswordHashingService;
|
|
|
|
beforeEach(() => {
|
|
service = new PasswordHashingService();
|
|
});
|
|
|
|
describe('hash', () => {
|
|
it('should hash a plain text password', async () => {
|
|
const plainPassword = 'mySecurePassword123';
|
|
const hash = await service.hash(plainPassword);
|
|
|
|
expect(hash).toBeDefined();
|
|
expect(typeof hash).toBe('string');
|
|
expect(hash.length).toBeGreaterThan(0);
|
|
// Hash should not be the same as the plain password
|
|
expect(hash).not.toBe(plainPassword);
|
|
});
|
|
|
|
it('should produce different hashes for the same password (with salt)', async () => {
|
|
const plainPassword = 'mySecurePassword123';
|
|
const hash1 = await service.hash(plainPassword);
|
|
const hash2 = await service.hash(plainPassword);
|
|
|
|
// Due to salting, hashes should be different
|
|
expect(hash1).not.toBe(hash2);
|
|
});
|
|
|
|
it('should handle empty string password', async () => {
|
|
const hash = await service.hash('');
|
|
expect(hash).toBeDefined();
|
|
expect(typeof hash).toBe('string');
|
|
});
|
|
|
|
it('should handle special characters in password', async () => {
|
|
const specialPassword = 'P@ssw0rd!#$%^&*()_+-=[]{}|;:,.<>?';
|
|
const hash = await service.hash(specialPassword);
|
|
|
|
expect(hash).toBeDefined();
|
|
expect(typeof hash).toBe('string');
|
|
});
|
|
|
|
it('should handle unicode characters in password', async () => {
|
|
const unicodePassword = 'Pässwörd!🔒';
|
|
const hash = await service.hash(unicodePassword);
|
|
|
|
expect(hash).toBeDefined();
|
|
expect(typeof hash).toBe('string');
|
|
});
|
|
|
|
it('should handle very long passwords', async () => {
|
|
const longPassword = 'a'.repeat(1000);
|
|
const hash = await service.hash(longPassword);
|
|
|
|
expect(hash).toBeDefined();
|
|
expect(typeof hash).toBe('string');
|
|
});
|
|
|
|
it('should handle whitespace-only password', async () => {
|
|
const whitespacePassword = ' ';
|
|
const hash = await service.hash(whitespacePassword);
|
|
|
|
expect(hash).toBeDefined();
|
|
expect(typeof hash).toBe('string');
|
|
});
|
|
});
|
|
|
|
describe('verify', () => {
|
|
it('should verify correct password against hash', async () => {
|
|
const plainPassword = 'mySecurePassword123';
|
|
const hash = await service.hash(plainPassword);
|
|
|
|
const isValid = await service.verify(plainPassword, hash);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it('should reject incorrect password', async () => {
|
|
const plainPassword = 'mySecurePassword123';
|
|
const hash = await service.hash(plainPassword);
|
|
|
|
const isValid = await service.verify('wrongPassword', hash);
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it('should reject empty password against hash', async () => {
|
|
const plainPassword = 'mySecurePassword123';
|
|
const hash = await service.hash(plainPassword);
|
|
|
|
const isValid = await service.verify('', hash);
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it('should handle verification with special characters', async () => {
|
|
const specialPassword = 'P@ssw0rd!#$%^&*()_+-=[]{}|;:,.<>?';
|
|
const hash = await service.hash(specialPassword);
|
|
|
|
const isValid = await service.verify(specialPassword, hash);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it('should handle verification with unicode characters', async () => {
|
|
const unicodePassword = 'Pässwörd!🔒';
|
|
const hash = await service.hash(unicodePassword);
|
|
|
|
const isValid = await service.verify(unicodePassword, hash);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it('should handle verification with very long passwords', async () => {
|
|
const longPassword = 'a'.repeat(1000);
|
|
const hash = await service.hash(longPassword);
|
|
|
|
const isValid = await service.verify(longPassword, hash);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it('should handle verification with whitespace-only password', async () => {
|
|
const whitespacePassword = ' ';
|
|
const hash = await service.hash(whitespacePassword);
|
|
|
|
const isValid = await service.verify(whitespacePassword, hash);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it('should reject verification with null hash', async () => {
|
|
// bcrypt throws an error when hash is null, which is expected behavior
|
|
await expect(service.verify('password', null as unknown as string)).rejects.toThrow();
|
|
});
|
|
|
|
it('should reject verification with empty hash', async () => {
|
|
const isValid = await service.verify('password', '');
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it('should reject verification with invalid hash format', async () => {
|
|
const isValid = await service.verify('password', 'invalid-hash-format');
|
|
expect(isValid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Hash Consistency', () => {
|
|
it('should consistently verify the same password-hash pair', async () => {
|
|
const plainPassword = 'testPassword123';
|
|
const hash = await service.hash(plainPassword);
|
|
|
|
// Verify multiple times
|
|
const result1 = await service.verify(plainPassword, hash);
|
|
const result2 = await service.verify(plainPassword, hash);
|
|
const result3 = await service.verify(plainPassword, hash);
|
|
|
|
expect(result1).toBe(true);
|
|
expect(result2).toBe(true);
|
|
expect(result3).toBe(true);
|
|
}, 10000);
|
|
|
|
it('should consistently reject wrong password', async () => {
|
|
const plainPassword = 'testPassword123';
|
|
const wrongPassword = 'wrongPassword';
|
|
const hash = await service.hash(plainPassword);
|
|
|
|
// Verify multiple times with wrong password
|
|
const result1 = await service.verify(wrongPassword, hash);
|
|
const result2 = await service.verify(wrongPassword, hash);
|
|
const result3 = await service.verify(wrongPassword, hash);
|
|
|
|
expect(result1).toBe(false);
|
|
expect(result2).toBe(false);
|
|
expect(result3).toBe(false);
|
|
}, 10000);
|
|
});
|
|
|
|
describe('Security Properties', () => {
|
|
it('should not leak information about the original password from hash', async () => {
|
|
const password1 = 'password123';
|
|
const password2 = 'password456';
|
|
|
|
const hash1 = await service.hash(password1);
|
|
const hash2 = await service.hash(password2);
|
|
|
|
// Hashes should be different
|
|
expect(hash1).not.toBe(hash2);
|
|
|
|
// Neither hash should contain the original password
|
|
expect(hash1).not.toContain(password1);
|
|
expect(hash2).not.toContain(password2);
|
|
});
|
|
|
|
it('should handle case sensitivity correctly', async () => {
|
|
const password1 = 'Password';
|
|
const password2 = 'password';
|
|
|
|
const hash1 = await service.hash(password1);
|
|
const hash2 = await service.hash(password2);
|
|
|
|
// Should be treated as different passwords
|
|
const isValid1 = await service.verify(password1, hash1);
|
|
const isValid2 = await service.verify(password2, hash2);
|
|
const isCrossValid1 = await service.verify(password1, hash2);
|
|
const isCrossValid2 = await service.verify(password2, hash1);
|
|
|
|
expect(isValid1).toBe(true);
|
|
expect(isValid2).toBe(true);
|
|
expect(isCrossValid1).toBe(false);
|
|
expect(isCrossValid2).toBe(false);
|
|
}, 10000);
|
|
});
|
|
}); |