Files
openrouter-watcher/tests/unit/services/test_password.py
Luca Sacchi Ricciardi 54e81162df feat(security): T13 implement bcrypt password hashing
- Add password hashing with bcrypt (12 rounds)
- Implement verify_password with timing-safe comparison
- Add validate_password_strength with comprehensive rules
  - Min 12 chars, uppercase, lowercase, digit, special char
- 19 comprehensive tests with 100% coverage
- Handle TypeError for non-string inputs
2026-04-07 12:06:38 +02:00

275 lines
8.8 KiB
Python

"""Tests for password hashing service - T13.
Tests for bcrypt password hashing with strength validation.
"""
import pytest
pytestmark = [pytest.mark.unit, pytest.mark.security, pytest.mark.slow]
class TestPasswordHashing:
"""Test suite for password hashing service."""
def test_hash_password_returns_string(self):
"""Test that hash_password returns a string."""
# Arrange
from src.openrouter_monitor.services.password import hash_password
password = "SecurePass123!"
# Act
hashed = hash_password(password)
# Assert
assert isinstance(hashed, str)
assert len(hashed) > 0
def test_hash_password_generates_different_hash_each_time(self):
"""Test that hashing same password produces different hashes (due to salt)."""
# Arrange
from src.openrouter_monitor.services.password import hash_password
password = "SamePassword123!"
# Act
hash1 = hash_password(password)
hash2 = hash_password(password)
# Assert
assert hash1 != hash2
assert hash1.startswith("$2b$")
assert hash2.startswith("$2b$")
def test_verify_password_correct_returns_true(self):
"""Test that verify_password returns True for correct password."""
# Arrange
from src.openrouter_monitor.services.password import hash_password, verify_password
password = "MySecurePass123!"
hashed = hash_password(password)
# Act
result = verify_password(password, hashed)
# Assert
assert result is True
def test_verify_password_incorrect_returns_false(self):
"""Test that verify_password returns False for incorrect password."""
# Arrange
from src.openrouter_monitor.services.password import hash_password, verify_password
password = "CorrectPass123!"
wrong_password = "WrongPass123!"
hashed = hash_password(password)
# Act
result = verify_password(wrong_password, hashed)
# Assert
assert result is False
def test_verify_password_with_different_hash_fails(self):
"""Test that verify_password fails with a hash from different password."""
# Arrange
from src.openrouter_monitor.services.password import hash_password, verify_password
password1 = "PasswordOne123!"
password2 = "PasswordTwo123!"
hashed1 = hash_password(password1)
# Act
result = verify_password(password2, hashed1)
# Assert
assert result is False
class TestPasswordStrengthValidation:
"""Test suite for password strength validation."""
def test_validate_password_strong_returns_true(self):
"""Test that strong password passes validation."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
passwords = [
"SecurePass123!",
"MyP@ssw0rd2024",
"C0mpl3x!Pass#",
"Valid-Password-123!",
]
# Act & Assert
for password in passwords:
assert validate_password_strength(password) is True, f"Failed for: {password}"
def test_validate_password_too_short_returns_false(self):
"""Test that password less than 12 chars fails validation."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
passwords = [
"Short1!",
"Abc123!",
"NoSpecial1",
"OnlyLower!",
]
# Act & Assert
for password in passwords:
assert validate_password_strength(password) is False, f"Should fail for: {password}"
def test_validate_password_no_uppercase_returns_false(self):
"""Test that password without uppercase fails validation."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
password = "lowercase123!"
# Act
result = validate_password_strength(password)
# Assert
assert result is False
def test_validate_password_no_lowercase_returns_false(self):
"""Test that password without lowercase fails validation."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
password = "UPPERCASE123!"
# Act
result = validate_password_strength(password)
# Assert
assert result is False
def test_validate_password_no_digit_returns_false(self):
"""Test that password without digit fails validation."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
password = "NoDigitsHere!"
# Act
result = validate_password_strength(password)
# Assert
assert result is False
def test_validate_password_no_special_returns_false(self):
"""Test that password without special char fails validation."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
password = "NoSpecialChar1"
# Act
result = validate_password_strength(password)
# Assert
assert result is False
def test_validate_password_only_special_chars_returns_false(self):
"""Test that password with only special chars fails validation."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
password = "!@#$%^&*()_+"
# Act
result = validate_password_strength(password)
# Assert
assert result is False
def test_validate_password_empty_returns_false(self):
"""Test that empty password fails validation."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
password = ""
# Act
result = validate_password_strength(password)
# Assert
assert result is False
def test_validate_password_unicode_handled_correctly(self):
"""Test that unicode password is handled correctly."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
password = "日本語パスワード123!"
# Act
result = validate_password_strength(password)
# Assert - Unicode chars are not special chars in regex sense
# but the password has uppercase/lowercase (in unicode), digits, and special
# This depends on implementation, but should not crash
assert isinstance(result, bool)
def test_hash_and_verify_integration(self):
"""Test full hash and verify workflow."""
# Arrange
from src.openrouter_monitor.services.password import (
hash_password,
verify_password,
validate_password_strength,
)
password = "Str0ng!Passw0rd"
# Act & Assert
assert validate_password_strength(password) is True
hashed = hash_password(password)
assert verify_password(password, hashed) is True
assert verify_password("WrongPass", hashed) is False
class TestPasswordTypeValidation:
"""Test suite for type validation in password functions."""
def test_hash_password_non_string_raises_type_error(self):
"""Test that hash_password with non-string raises TypeError."""
# Arrange
from src.openrouter_monitor.services.password import hash_password
# Act & Assert
with pytest.raises(TypeError, match="password must be a string"):
hash_password(12345)
def test_verify_password_non_string_plain_raises_type_error(self):
"""Test that verify_password with non-string plain raises TypeError."""
# Arrange
from src.openrouter_monitor.services.password import verify_password
# Act & Assert
with pytest.raises(TypeError, match="plain_password must be a string"):
verify_password(12345, "hashed_password")
def test_verify_password_non_string_hash_raises_type_error(self):
"""Test that verify_password with non-string hash raises TypeError."""
# Arrange
from src.openrouter_monitor.services.password import verify_password
# Act & Assert
with pytest.raises(TypeError, match="hashed_password must be a string"):
verify_password("plain_password", 12345)
def test_validate_password_strength_non_string_returns_false(self):
"""Test that validate_password_strength with non-string returns False."""
# Arrange
from src.openrouter_monitor.services.password import validate_password_strength
# Act & Assert
assert validate_password_strength(12345) is False
assert validate_password_strength(None) is False
assert validate_password_strength([]) is False