phase 3.2 & 4.1
This commit is contained in:
235
backend/tests/auth/test_security.py
Normal file
235
backend/tests/auth/test_security.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""Unit tests for password hashing and validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user