All checks were successful
CI/CD Pipeline / VM Test - security (push) Successful in 7s
CI/CD Pipeline / Backend Linting (push) Successful in 4s
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 11s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 8s
CI/CD Pipeline / VM Test - performance (push) Successful in 8s
CI/CD Pipeline / Nix Flake Check (push) Successful in 38s
CI/CD Pipeline / CI Summary (push) Successful in 0s
CI/CD Pipeline / Frontend Linting (push) Successful in 17s
235 lines
7.9 KiB
Python
235 lines
7.9 KiB
Python
"""Unit tests for password hashing and validation."""
|
|
|
|
|
|
from app.auth.security import hash_password, validate_password_strength, verify_password
|
|
|
|
|
|
class TestPasswordHashing:
|
|
"""Test password hashing functionality."""
|
|
|
|
def test_hash_password_returns_string(self):
|
|
"""Test that hash_password returns a non-empty string."""
|
|
password = "TestPassword123"
|
|
hashed = hash_password(password)
|
|
|
|
assert isinstance(hashed, str)
|
|
assert len(hashed) > 0
|
|
assert hashed != password
|
|
|
|
def test_hash_password_generates_unique_hashes(self):
|
|
"""Test that same password generates different hashes (bcrypt salt)."""
|
|
password = "TestPassword123"
|
|
hash1 = hash_password(password)
|
|
hash2 = hash_password(password)
|
|
|
|
assert hash1 != hash2 # Different salts
|
|
|
|
def test_hash_password_with_special_characters(self):
|
|
"""Test hashing passwords with special characters."""
|
|
password = "P@ssw0rd!#$%"
|
|
hashed = hash_password(password)
|
|
|
|
assert isinstance(hashed, str)
|
|
assert len(hashed) > 0
|
|
|
|
def test_hash_password_with_unicode(self):
|
|
"""Test hashing passwords with unicode characters."""
|
|
password = "Pässwörd123"
|
|
hashed = hash_password(password)
|
|
|
|
assert isinstance(hashed, str)
|
|
assert len(hashed) > 0
|
|
|
|
|
|
class TestPasswordVerification:
|
|
"""Test password verification functionality."""
|
|
|
|
def test_verify_password_correct_password(self):
|
|
"""Test that correct password verifies successfully."""
|
|
password = "TestPassword123"
|
|
hashed = hash_password(password)
|
|
|
|
assert verify_password(password, hashed) is True
|
|
|
|
def test_verify_password_incorrect_password(self):
|
|
"""Test that incorrect password fails verification."""
|
|
password = "TestPassword123"
|
|
hashed = hash_password(password)
|
|
|
|
assert verify_password("WrongPassword123", hashed) is False
|
|
|
|
def test_verify_password_case_sensitive(self):
|
|
"""Test that password verification is case-sensitive."""
|
|
password = "TestPassword123"
|
|
hashed = hash_password(password)
|
|
|
|
assert verify_password("testpassword123", hashed) is False
|
|
assert verify_password("TESTPASSWORD123", hashed) is False
|
|
|
|
def test_verify_password_empty_string(self):
|
|
"""Test that empty password fails verification."""
|
|
password = "TestPassword123"
|
|
hashed = hash_password(password)
|
|
|
|
assert verify_password("", hashed) is False
|
|
|
|
def test_verify_password_with_special_characters(self):
|
|
"""Test verification of passwords with special characters."""
|
|
password = "P@ssw0rd!#$%"
|
|
hashed = hash_password(password)
|
|
|
|
assert verify_password(password, hashed) is True
|
|
assert verify_password("P@ssw0rd!#$", hashed) is False # Missing last char
|
|
|
|
def test_verify_password_invalid_hash_format(self):
|
|
"""Test that invalid hash format returns False."""
|
|
password = "TestPassword123"
|
|
|
|
assert verify_password(password, "invalid_hash") is False
|
|
assert verify_password(password, "") is False
|
|
|
|
|
|
class TestPasswordStrengthValidation:
|
|
"""Test password strength validation."""
|
|
|
|
def test_validate_password_valid_password(self):
|
|
"""Test that valid passwords pass validation."""
|
|
valid_passwords = [
|
|
"Password123",
|
|
"Abcdef123",
|
|
"SecureP@ss1",
|
|
"MyP4ssword",
|
|
]
|
|
|
|
for password in valid_passwords:
|
|
is_valid, error = validate_password_strength(password)
|
|
assert is_valid is True, f"Password '{password}' should be valid"
|
|
assert error == ""
|
|
|
|
def test_validate_password_too_short(self):
|
|
"""Test that passwords shorter than 8 characters fail."""
|
|
short_passwords = [
|
|
"Pass1",
|
|
"Abc123",
|
|
"Short1A",
|
|
]
|
|
|
|
for password in short_passwords:
|
|
is_valid, error = validate_password_strength(password)
|
|
assert is_valid is False
|
|
assert "at least 8 characters" in error
|
|
|
|
def test_validate_password_no_uppercase(self):
|
|
"""Test that passwords without uppercase letters fail."""
|
|
passwords = [
|
|
"password123",
|
|
"mypassword1",
|
|
"lowercase8",
|
|
]
|
|
|
|
for password in passwords:
|
|
is_valid, error = validate_password_strength(password)
|
|
assert is_valid is False
|
|
assert "uppercase letter" in error
|
|
|
|
def test_validate_password_no_lowercase(self):
|
|
"""Test that passwords without lowercase letters fail."""
|
|
passwords = [
|
|
"PASSWORD123",
|
|
"MYPASSWORD1",
|
|
"UPPERCASE8",
|
|
]
|
|
|
|
for password in passwords:
|
|
is_valid, error = validate_password_strength(password)
|
|
assert is_valid is False
|
|
assert "lowercase letter" in error
|
|
|
|
def test_validate_password_no_number(self):
|
|
"""Test that passwords without numbers fail."""
|
|
passwords = [
|
|
"Password",
|
|
"MyPassword",
|
|
"NoNumbers",
|
|
]
|
|
|
|
for password in passwords:
|
|
is_valid, error = validate_password_strength(password)
|
|
assert is_valid is False
|
|
assert "one number" in error
|
|
|
|
def test_validate_password_edge_cases(self):
|
|
"""Test password validation edge cases."""
|
|
# Exactly 8 characters, all requirements met
|
|
is_valid, error = validate_password_strength("Abcdef12")
|
|
assert is_valid is True
|
|
assert error == ""
|
|
|
|
# Very long password
|
|
is_valid, error = validate_password_strength("A" * 100 + "a1")
|
|
assert is_valid is True
|
|
|
|
# Empty password
|
|
is_valid, error = validate_password_strength("")
|
|
assert is_valid is False
|
|
|
|
def test_validate_password_with_special_chars(self):
|
|
"""Test that special characters don't interfere with validation."""
|
|
passwords_with_special = [
|
|
"P@ssw0rd!",
|
|
"MyP@ss123",
|
|
"Test#Pass1",
|
|
]
|
|
|
|
for password in passwords_with_special:
|
|
is_valid, error = validate_password_strength(password)
|
|
assert is_valid is True, f"Password '{password}' should be valid"
|
|
assert error == ""
|
|
|
|
|
|
class TestPasswordSecurityProperties:
|
|
"""Test security properties of password handling."""
|
|
|
|
def test_hashed_password_not_reversible(self):
|
|
"""Test that hashed passwords cannot be easily reversed."""
|
|
password = "TestPassword123"
|
|
hashed = hash_password(password)
|
|
|
|
# Hash should not contain original password
|
|
assert password not in hashed
|
|
assert password.lower() not in hashed.lower()
|
|
|
|
def test_different_passwords_different_hashes(self):
|
|
"""Test that different passwords produce different hashes."""
|
|
password1 = "TestPassword123"
|
|
password2 = "TestPassword124" # Only last char different
|
|
|
|
hash1 = hash_password(password1)
|
|
hash2 = hash_password(password2)
|
|
|
|
assert hash1 != hash2
|
|
|
|
def test_hashed_password_length_consistent(self):
|
|
"""Test that bcrypt hashes have consistent length."""
|
|
passwords = ["Short1A", "MediumPassword123", "VeryLongPasswordWithLotsOfCharacters123"]
|
|
|
|
hashes = [hash_password(p) for p in passwords]
|
|
|
|
# All bcrypt hashes should be 60 characters
|
|
for hashed in hashes:
|
|
assert len(hashed) == 60
|
|
|
|
def test_verify_handles_timing_attack_resistant(self):
|
|
"""Test that verification doesn't leak timing information (bcrypt property)."""
|
|
# This is more of a documentation test - bcrypt is designed to be timing-attack resistant
|
|
password = "TestPassword123"
|
|
hashed = hash_password(password)
|
|
|
|
# Both should take roughly the same time (bcrypt property)
|
|
verify_password("WrongPassword123", hashed)
|
|
verify_password(password, hashed)
|
|
|
|
# No actual timing measurement here, just documenting the property
|
|
assert True
|
|
|