- 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
275 lines
8.8 KiB
Python
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
|