"""密码哈希与验证单元测试 — wave131.""" from __future__ import annotations import pytest from packages.application.auth.password_hasher import ( PasswordHasher, PasswordValidator, password_hasher, password_validator, ) # ── PasswordHasher 初始化 ─────────────────────────────────────────────────── class TestPasswordHasherInit: def test_default_rounds(self): hasher = PasswordHasher() assert hasher.rounds == 12 def test_custom_rounds(self): hasher = PasswordHasher(rounds=8) assert hasher.rounds == 8 def test_min_rounds(self): hasher = PasswordHasher(rounds=4) assert hasher.rounds == 4 def test_max_rounds(self): hasher = PasswordHasher(rounds=31) assert hasher.rounds == 31 def test_rounds_below_min_raises(self): with pytest.raises(ValueError, match="rounds must be between"): PasswordHasher(rounds=3) def test_rounds_above_max_raises(self): with pytest.raises(ValueError, match="rounds must be between"): PasswordHasher(rounds=32) def test_rounds_zero_raises(self): with pytest.raises(ValueError): PasswordHasher(rounds=0) def test_rounds_negative_raises(self): with pytest.raises(ValueError): PasswordHasher(rounds=-5) # ── hash_password ────────────────────────────────────────────────────────── # 用 rounds=4 加速测试 def _make_hasher(): return PasswordHasher(rounds=4) class TestHashPassword: def test_returns_string(self): hasher = _make_hasher() result = hasher.hash_password("testpassword") assert isinstance(result, str) assert len(result) > 0 def test_empty_password_raises(self): hasher = _make_hasher() with pytest.raises(ValueError, match="Password cannot be empty"): hasher.hash_password("") def test_none_password_raises(self): hasher = _make_hasher() with pytest.raises(ValueError): hasher.hash_password(None) # type: ignore def test_bcrypt_format(self): """bcrypt hash 格式: $2b$rounds$...""" hasher = _make_hasher() result = hasher.hash_password("test") assert result.startswith("$2b$04$") def test_same_password_different_hash(self): """每次哈希应该生成不同的 salt.""" hasher = _make_hasher() h1 = hasher.hash_password("samepassword") h2 = hasher.hash_password("samepassword") assert h1 != h2 def test_hash_contains_salt(self): """bcrypt hash 长度固定约 60 字符.""" hasher = _make_hasher() result = hasher.hash_password("test") assert len(result) >= 59 # bcrypt 标准长度 def test_long_password(self): hasher = _make_hasher() long_pw = "x" * 100 result = hasher.hash_password(long_pw) assert isinstance(result, str) assert len(result) > 0 def test_unicode_password(self): hasher = _make_hasher() result = hasher.hash_password("密码测试_🎉_123") assert isinstance(result, str) assert len(result) > 0 def test_special_chars_password(self): hasher = _make_hasher() result = hasher.hash_password("p@ssw0rd!#$%^&*()") assert isinstance(result, str) # ── verify_password ───────────────────────────────────────────────────────── class TestVerifyPassword: def test_correct_password_returns_true(self): hasher = _make_hasher() pw = "MySecureP@ss123" hashed = hasher.hash_password(pw) assert hasher.verify_password(pw, hashed) is True def test_wrong_password_returns_false(self): hasher = _make_hasher() hashed = hasher.hash_password("correct_password") assert hasher.verify_password("wrong_password", hashed) is False def test_empty_password_returns_false(self): hasher = _make_hasher() hashed = hasher.hash_password("test") assert hasher.verify_password("", hashed) is False def test_empty_hash_returns_false(self): hasher = _make_hasher() assert hasher.verify_password("test", "") is False def test_none_password_returns_false(self): hasher = _make_hasher() hashed = hasher.hash_password("test") assert hasher.verify_password(None, hashed) is False # type: ignore def test_invalid_hash_format_returns_false(self): hasher = _make_hasher() assert hasher.verify_password("test", "not-a-valid-hash") is False def test_case_sensitive(self): hasher = _make_hasher() hashed = hasher.hash_password("Password") assert hasher.verify_password("password", hashed) is False assert hasher.verify_password("PASSWORD", hashed) is False assert hasher.verify_password("Password", hashed) is True def test_whitespace_matters(self): hasher = _make_hasher() hashed = hasher.hash_password("password") assert hasher.verify_password(" password", hashed) is False assert hasher.verify_password("password ", hashed) is False def test_unicode_roundtrip(self): hasher = _make_hasher() pw = "密码_测试_🎉" hashed = hasher.hash_password(pw) assert hasher.verify_password(pw, hashed) is True # ── needs_rehash ──────────────────────────────────────────────────────────── class TestNeedsRehash: def test_same_rounds_no_rehash(self): hasher = _make_hasher() # rounds=4 hashed = hasher.hash_password("test") assert hasher.needs_rehash(hashed) is False def test_different_rounds_needs_rehash(self): hasher4 = PasswordHasher(rounds=4) hashed = hasher4.hash_password("test") hasher6 = PasswordHasher(rounds=6) assert hasher6.needs_rehash(hashed) is True def test_higher_rounds_needs_rehash(self): hasher4 = PasswordHasher(rounds=4) hashed = hasher4.hash_password("test") hasher10 = PasswordHasher(rounds=10) assert hasher10.needs_rehash(hashed) is True def test_lower_rounds_also_needs_rehash(self): hasher10 = PasswordHasher(rounds=10) # 用 rounds=10 的 hasher 生成的 hash,用 rounds=4 的检查应该也要重算(因为 cost 不同) hashed = hasher10.hash_password("test") hasher4 = PasswordHasher(rounds=4) assert hasher4.needs_rehash(hashed) is True def test_invalid_hash_returns_false(self): hasher = _make_hasher() assert hasher.needs_rehash("invalid-hash") is False def test_empty_hash_returns_false(self): hasher = _make_hasher() assert hasher.needs_rehash("") is False # ── PasswordValidator 初始化 ──────────────────────────────────────────────── class TestPasswordValidatorInit: def test_default_settings(self): validator = PasswordValidator() assert validator.min_length == 8 assert validator.require_uppercase is True assert validator.require_lowercase is True assert validator.require_digit is True assert validator.require_special is False def test_custom_settings(self): validator = PasswordValidator( min_length=12, require_uppercase=False, require_lowercase=False, require_digit=False, require_special=True, ) assert validator.min_length == 12 assert validator.require_uppercase is False assert validator.require_special is True # ── PasswordValidator.validate ────────────────────────────────────────────── class TestPasswordValidator: def setup_method(self): self.validator = PasswordValidator() # 默认 8 位 + 大小写 + 数字 def test_strong_password_passes(self): valid, error = self.validator.validate("MyStr0ngP@ss") assert valid is True assert error is None def test_empty_password_fails(self): valid, error = self.validator.validate("") assert valid is False assert "empty" in error.lower() def test_too_short_fails(self): valid, error = self.validator.validate("Abc123") # 6 chars assert valid is False assert "at least" in error.lower() assert "8" in error def test_exactly_min_length_passes(self): valid, error = self.validator.validate("Abcd1234") # 8 chars assert valid is True def test_no_uppercase_fails(self): valid, error = self.validator.validate("lowercase123") assert valid is False assert "uppercase" in error.lower() def test_no_lowercase_fails(self): valid, error = self.validator.validate("UPPERCASE123") assert valid is False assert "lowercase" in error.lower() def test_no_digit_fails(self): valid, error = self.validator.validate("NoDigitsHere") assert valid is False assert "digit" in error.lower() def test_special_char_not_required_by_default(self): # 默认不要求特殊字符,没有也通过 valid, error = self.validator.validate("GoodPass123") assert valid is True def test_special_char_required_and_missing(self): validator = PasswordValidator(require_special=True) valid, error = validator.validate("GoodPass123") assert valid is False assert "special character" in error.lower() def test_special_char_required_and_present(self): validator = PasswordValidator(require_special=True) valid, error = validator.validate("GoodPass123!") assert valid is True def test_all_special_chars_accepted(self): validator = PasswordValidator(require_special=True) specials = "!@#$%^&*()_+-=[]{}|;:,.<>?~" for c in specials: pw = f"Pass123{c}" valid, _ = validator.validate(pw) assert valid is True, f"Special char '{c}' should be accepted" def test_minimal_valid_password(self): """刚好满足所有要求的最短密码.""" valid, _ = self.validator.validate("Ab1cdefg") # 8 chars: 1 upper, 1 lower, 1 digit assert valid is True def test_only_digits_fails(self): valid, _ = self.validator.validate("12345678") assert valid is False def test_only_letters_fails(self): valid, _ = self.validator.validate("Abcdefgh") assert valid is False # 没有数字 def test_long_password_passes(self): valid, _ = self.validator.validate("VeryLongPassword123WithManyCharacters") assert valid is True def test_unicode_password_passes(self): """Unicode 字符计数正确.""" valid, error = self.validator.validate("密码Abc12345") assert valid is True def test_whitespace_password_passes_if_meets_req(self): """包含空格的密码(如果满足其他要求)应该通过.""" valid, _ = self.validator.validate("Pass word 123") assert valid is True # ── 全局实例 ──────────────────────────────────────────────────────────────── class TestGlobalInstances: def test_global_hasher_exists(self): assert password_hasher is not None assert isinstance(password_hasher, PasswordHasher) assert password_hasher.rounds == 12 def test_global_validator_exists(self): assert password_validator is not None assert isinstance(password_validator, PasswordValidator) def test_global_hasher_works(self): hashed = password_hasher.hash_password("test_global") assert password_hasher.verify_password("test_global", hashed) is True def test_global_validator_works(self): valid, _ = password_validator.validate("TestPass123") assert valid is True