"""密码哈希与验证器单元测试.""" from __future__ import annotations import pytest from packages.application.auth.password_hasher import ( PasswordHasher, PasswordValidator, password_hasher, password_validator, ) class TestPasswordHasher: """PasswordHasher 测试""" def test_hash_password_returns_string(self): """哈希密码返回非空字符串""" hasher = PasswordHasher(rounds=4) hashed = hasher.hash_password("TestPass1!") assert isinstance(hashed, str) assert len(hashed) > 0 assert hashed.startswith("$2") # bcrypt hash 格式 def test_hash_password_different_salts(self): """相同密码每次哈希结果不同(加盐)""" hasher = PasswordHasher(rounds=4) h1 = hasher.hash_password("SamePass1!") h2 = hasher.hash_password("SamePass1!") assert h1 != h2 def test_verify_correct_password(self): """正确密码验证通过""" hasher = PasswordHasher(rounds=4) hashed = hasher.hash_password("Correct1!") assert hasher.verify_password("Correct1!", hashed) is True def test_verify_wrong_password(self): """错误密码验证失败""" hasher = PasswordHasher(rounds=4) hashed = hasher.hash_password("Right123!") assert hasher.verify_password("Wrong123!", hashed) is False def test_hash_empty_password_raises(self): """空密码哈希抛出 ValueError""" hasher = PasswordHasher(rounds=4) with pytest.raises(ValueError, match="Password cannot be empty"): hasher.hash_password("") def test_verify_empty_password_returns_false(self): """空密码验证返回 False""" hasher = PasswordHasher(rounds=4) hashed = hasher.hash_password("TestPass1!") assert hasher.verify_password("", hashed) is False def test_verify_empty_hash_returns_false(self): """空哈希验证返回 False""" hasher = PasswordHasher(rounds=4) assert hasher.verify_password("TestPass1!", "") is False def test_verify_invalid_hash_format(self): """无效格式的哈希验证返回 False(不抛异常)""" hasher = PasswordHasher(rounds=4) assert hasher.verify_password("TestPass1!", "not_a_valid_hash") is False def test_needs_rehash_same_rounds(self): """相同 rounds 不需要重新哈希""" hasher = PasswordHasher(rounds=4) hashed = hasher.hash_password("TestPass1!") assert hasher.needs_rehash(hashed) is False def test_needs_rehash_different_rounds(self): """不同 rounds 需要重新哈希""" hasher_low = PasswordHasher(rounds=4) hasher_high = PasswordHasher(rounds=5) hashed = hasher_low.hash_password("TestPass1!") assert hasher_high.needs_rehash(hashed) is True def test_needs_rehash_invalid_hash(self): """无效哈希格式返回 False(不抛异常)""" hasher = PasswordHasher(rounds=4) assert hasher.needs_rehash("invalid_hash") is False def test_rounds_too_low_raises(self): """rounds 小于 4 抛出 ValueError""" with pytest.raises(ValueError, match="rounds must be between 4 and 31"): PasswordHasher(rounds=3) def test_rounds_too_high_raises(self): """rounds 大于 31 抛出 ValueError""" with pytest.raises(ValueError, match="rounds must be between 4 and 31"): PasswordHasher(rounds=32) def test_rounds_boundary_values(self): """rounds 边界值 4 和 31 是合法的""" hasher_low = PasswordHasher(rounds=4) hasher_high = PasswordHasher(rounds=31) assert hasher_low.rounds == 4 assert hasher_high.rounds == 31 def test_hash_and_verify_various_passwords(self): """多种密码的哈希-验证往返""" hasher = PasswordHasher(rounds=4) passwords = [ "Simple12", "C0mpl3x!@#", " spaces ", "中文密码123", "a" * 50, # 50字节,在72字节限制内 "12345678", ] for pwd in passwords: hashed = hasher.hash_password(pwd) assert hasher.verify_password(pwd, hashed) assert not hasher.verify_password(pwd + "x", hashed) class TestPasswordValidator: """PasswordValidator 测试""" def test_strong_password_passes(self): """强密码通过验证""" validator = PasswordValidator() valid, error = validator.validate("Str0ngP@ss") assert valid is True assert error is None def test_empty_password_fails(self): """空密码验证失败""" validator = PasswordValidator() valid, error = validator.validate("") assert valid is False assert "empty" in error.lower() def test_too_short_fails(self): """密码太短失败""" validator = PasswordValidator(min_length=8) valid, error = validator.validate("Sh0rt!") assert valid is False assert "at least 8" in error def test_no_uppercase_fails(self): """没有大写字母失败""" validator = PasswordValidator(require_uppercase=True) valid, error = validator.validate("lowercase1!") assert valid is False assert "uppercase" in error.lower() def test_no_lowercase_fails(self): """没有小写字母失败""" validator = PasswordValidator(require_lowercase=True) valid, error = validator.validate("UPPERCASE1!") assert valid is False assert "lowercase" in error.lower() def test_no_digit_fails(self): """没有数字失败""" validator = PasswordValidator(require_digit=True) valid, error = validator.validate("NoDigitsHere!") assert valid is False assert "digit" in error.lower() def test_no_special_not_required_passes(self): """不要求特殊字符时,不含特殊字符也通过""" validator = PasswordValidator(require_special=False) valid, error = validator.validate("NoSpecial1") assert valid is True def test_no_special_required_fails(self): """要求特殊字符时,不含特殊字符失败""" validator = PasswordValidator(require_special=True) valid, error = validator.validate("NoSpecial1") assert valid is False assert "special" in error.lower() def test_custom_min_length(self): """自定义最小长度""" validator = PasswordValidator( min_length=12, require_uppercase=False, require_lowercase=False, require_digit=False, ) valid, _ = validator.validate("123456789012") # 12字符 assert valid is True valid, _ = validator.validate("12345678901") # 11字符 assert valid is False def test_all_requirements_disabled(self): """所有要求都禁用时,任意非空密码都通过""" validator = PasswordValidator( min_length=1, require_uppercase=False, require_lowercase=False, require_digit=False, require_special=False, ) valid, error = validator.validate("x") assert valid is True assert error is None def test_special_characters_recognized(self): """各种特殊字符都被识别""" validator = PasswordValidator(require_special=True, require_uppercase=False, require_lowercase=False) specials = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "=", "+"] for ch in specials: valid, _ = validator.validate(f"abcd1234{ch}") assert valid is True, f"Special char '{ch}' not recognized" class TestGlobalInstances: """全局实例测试""" def test_global_password_hasher_exists(self): """全局 password_hasher 实例存在""" assert password_hasher is not None assert isinstance(password_hasher, PasswordHasher) assert password_hasher.rounds == 12 def test_global_password_validator_exists(self): """全局 password_validator 实例存在""" assert password_validator is not None assert isinstance(password_validator, PasswordValidator) assert password_validator.min_length == 8 assert password_validator.require_uppercase is True assert password_validator.require_special is False