"""密码哈希与验证模块单元测试.""" import pytest from packages.application.auth.password_handler import ( PasswordHandler, configure_password_handler, get_password_handler, ) from packages.application.auth.password_hasher import ( PasswordHasher, PasswordValidator, ) # ==================== PasswordValidator ==================== class TestPasswordValidator: @pytest.fixture def default_validator(self): return PasswordValidator() @pytest.fixture def strict_validator(self): return PasswordValidator( min_length=12, require_uppercase=True, require_lowercase=True, require_digit=True, require_special=True, ) class TestBasicValidation: def test_valid_password(self, default_validator): ok, msg = default_validator.validate("SecurePass123") assert ok is True assert msg is None def test_empty_password(self, default_validator): ok, msg = default_validator.validate("") assert ok is False assert "empty" in msg.lower() def test_none_password_treated_as_empty(self, default_validator): # None被not判定为falsy,返回空密码错误 ok, msg = default_validator.validate(None) assert ok is False assert "empty" in msg.lower() class TestMinLength: def test_too_short(self, default_validator): ok, msg = default_validator.validate("Ab1") assert ok is False assert "8" in msg def test_exact_min_length(self, default_validator): # 刚好8个字符 ok, _ = default_validator.validate("Abcdefg1") assert ok is True def test_custom_min_length(self, strict_validator): ok, msg = strict_validator.validate("Short1!") assert ok is False assert "12" in msg class TestUppercase: def test_no_uppercase(self, default_validator): ok, msg = default_validator.validate("password123") assert ok is False assert "uppercase" in msg.lower() def test_with_uppercase(self, default_validator): ok, _ = default_validator.validate("Password123") assert ok is True def test_disabled_requirement(self): v = PasswordValidator(require_uppercase=False) ok, _ = v.validate("password123") assert ok is True class TestLowercase: def test_no_lowercase(self, default_validator): ok, msg = default_validator.validate("PASSWORD123") assert ok is False assert "lowercase" in msg.lower() def test_with_lowercase(self, default_validator): ok, _ = default_validator.validate("Password123") assert ok is True def test_disabled_requirement(self): v = PasswordValidator(require_lowercase=False) ok, _ = v.validate("PASSWORD123") assert ok is True class TestDigit: def test_no_digit(self, default_validator): ok, msg = default_validator.validate("Passworddd") assert ok is False assert "digit" in msg.lower() def test_with_digit(self, default_validator): ok, _ = default_validator.validate("Password1") assert ok is True def test_disabled_requirement(self): v = PasswordValidator(require_digit=False) ok, _ = v.validate("Passworddd") assert ok is True class TestSpecialChar: def test_no_special_when_not_required(self, default_validator): ok, _ = default_validator.validate("Password123") assert ok is True def test_no_special_when_required(self, strict_validator): ok, msg = strict_validator.validate("Password1234") assert ok is False assert "special" in msg.lower() def test_with_special(self, strict_validator): ok, _ = strict_validator.validate("Password123!") assert ok is True def test_various_special_chars(self, strict_validator): for char in "!@#$%^&*()_+-=[]{}|;:,.<>?~": pw = f"LongPassword1{char}" # 13字符,含大小写数字特殊 ok, msg = strict_validator.validate(pw) assert ok is True, f"special char {char} should be valid: {msg}" class TestAllDisabled: def test_all_disabled_min_length_only(self): v = PasswordValidator( min_length=1, require_uppercase=False, require_lowercase=False, require_digit=False, require_special=False, ) ok, _ = v.validate("a") assert ok is True def test_all_disabled_empty_still_fails(self): v = PasswordValidator( min_length=1, require_uppercase=False, require_lowercase=False, require_digit=False, require_special=False, ) ok, _ = v.validate("") assert ok is False # ==================== PasswordHasher ==================== class TestPasswordHasher: @pytest.fixture def hasher(self): return PasswordHasher(rounds=4) # 用最低rounds加速测试 class TestHashPassword: def test_hash_returns_string(self, hasher): result = hasher.hash_password("testpassword") assert isinstance(result, str) assert len(result) > 0 def test_hash_starts_with_bcrypt_prefix(self, hasher): result = hasher.hash_password("testpassword") assert result.startswith("$2") def test_hash_contains_rounds(self, hasher): result = hasher.hash_password("testpassword") parts = result.split("$") assert parts[2] == "04" # bcrypt rounds格式是两位数 def test_hash_different_salts(self, hasher): # 同一密码两次哈希结果不同(因为salt随机) h1 = hasher.hash_password("samepassword") h2 = hasher.hash_password("samepassword") assert h1 != h2 def test_hash_empty_password_raises(self, hasher): with pytest.raises(ValueError, match="empty"): hasher.hash_password("") def test_hash_unicode_password(self, hasher): result = hasher.hash_password("密码Pass123!") assert isinstance(result, str) assert len(result) > 0 def test_hash_long_password(self, hasher): long_pw = "a" * 72 # bcrypt最大72字节 result = hasher.hash_password(long_pw) assert isinstance(result, str) class TestVerifyPassword: def test_verify_correct_password(self, hasher): hashed = hasher.hash_password("CorrectPass123") assert hasher.verify_password("CorrectPass123", hashed) is True def test_verify_wrong_password(self, hasher): hashed = hasher.hash_password("CorrectPass123") assert hasher.verify_password("WrongPass123", hashed) is False def test_verify_empty_password(self, hasher): hashed = hasher.hash_password("testpass") assert hasher.verify_password("", hashed) is False def test_verify_empty_hash(self, hasher): assert hasher.verify_password("testpass", "") is False def test_verify_invalid_hash_format(self, hasher): assert hasher.verify_password("testpass", "invalid-hash-format") is False def test_verify_none_hash(self, hasher): assert hasher.verify_password("testpass", None) is False def test_verify_unicode_password(self, hasher): pw = "密码Pass123!" hashed = hasher.hash_password(pw) assert hasher.verify_password(pw, hashed) is True class TestNeedsRehash: def test_same_rounds_no_rehash(self, hasher): hashed = hasher.hash_password("testpass") assert hasher.needs_rehash(hashed) is False def test_lower_rounds_needs_rehash(self): hasher_low = PasswordHasher(rounds=4) hashed = hasher_low.hash_password("testpass") hasher_high = PasswordHasher(rounds=5) assert hasher_high.needs_rehash(hashed) is True def test_higher_rounds_needs_rehash(self): hasher_high = PasswordHasher(rounds=5) hashed = hasher_high.hash_password("testpass") hasher_low = PasswordHasher(rounds=4) assert hasher_low.needs_rehash(hashed) is True def test_invalid_hash_no_rehash(self, hasher): assert hasher.needs_rehash("invalid-format") is False def test_empty_hash_no_rehash(self, hasher): assert hasher.needs_rehash("") is False class TestInit: def test_rounds_below_min_raises(self): with pytest.raises(ValueError): PasswordHasher(rounds=3) def test_rounds_above_max_raises(self): with pytest.raises(ValueError): PasswordHasher(rounds=32) def test_min_rounds_ok(self): h = PasswordHasher(rounds=4) assert h.rounds == 4 def test_max_rounds_ok(self): h = PasswordHasher(rounds=31) assert h.rounds == 31 # ==================== PasswordHandler ==================== class TestPasswordHandler: @pytest.fixture def handler(self): return PasswordHandler(rounds=4) def test_hash_and_verify_roundtrip(self, handler): hashed = handler.hash_password("MySecurePass123") assert handler.verify_password("MySecurePass123", hashed) is True assert handler.verify_password("WrongPass", hashed) is False def test_needs_rehash(self, handler): # 用当前rounds哈希,不需要rehash hashed = handler.hash_password("testpass") assert handler.needs_rehash(hashed) is False def test_validate_strength(self, handler): # 强密码通过 ok, msg = handler.validate_strength("StrongPass123") assert ok is True assert msg is None # 弱密码不通过 ok, msg = handler.validate_strength("weak") assert ok is False assert msg is not None def test_hash_empty_raises(self, handler): with pytest.raises(ValueError): handler.hash_password("") class TestGlobalHandler: def test_get_password_handler_returns_instance(self): # 重置全局实例 import packages.application.auth.password_handler as ph ph._default_handler = None handler = get_password_handler() assert isinstance(handler, PasswordHandler) def test_configure_password_handler(self): handler = configure_password_handler(rounds=4) assert isinstance(handler, PasswordHandler) # get应该返回同一个配置好的实例 same_handler = get_password_handler() assert same_handler is handler