From 210d9eae98c6e716489efd80ab8fd058df05ab65 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Mon, 27 Jul 2026 15:19:35 +0800 Subject: [PATCH] =?UTF-8?q?test(wave131):=20password=5Fhasher=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E5=93=88=E5=B8=8C+=E9=AA=8C=E8=AF=81+55=E5=8D=95?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PasswordHasher: 初始化(8)/hash_password(9)/verify_password(9)/needs_rehash(6) - PasswordValidator: 初始化(2)/validate(16) - 全局实例: 4 - 覆盖场景: bcrypt格式/盐值随机性/大小写敏感/Unicode/特殊字符/ 空密码/无效hash/不同rounds重哈希/长度/大小写/数字/特殊字符 --- tests/unit/test_password_hasher.py | 456 +++++++++++++++++------------ 1 file changed, 274 insertions(+), 182 deletions(-) diff --git a/tests/unit/test_password_hasher.py b/tests/unit/test_password_hasher.py index bc3a4787e..2298615a5 100755 --- a/tests/unit/test_password_hasher.py +++ b/tests/unit/test_password_hasher.py @@ -1,4 +1,4 @@ -"""密码哈希与验证器单元测试.""" +"""密码哈希与验证单元测试 — wave131.""" from __future__ import annotations @@ -11,240 +11,332 @@ from packages.application.auth.password_hasher import ( password_validator, ) +# ── PasswordHasher 初始化 ─────────────────────────────────────────────────── -class TestPasswordHasher: - """PasswordHasher 测试""" - def test_hash_password_returns_string(self): - """哈希密码返回非空字符串""" +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) - hashed = hasher.hash_password("TestPass1!") + assert hasher.rounds == 4 - assert isinstance(hashed, str) - assert len(hashed) > 0 - assert hashed.startswith("$2") # bcrypt hash 格式 + def test_max_rounds(self): + hasher = PasswordHasher(rounds=31) + assert hasher.rounds == 31 - def test_hash_password_different_salts(self): - """相同密码每次哈希结果不同(加盐)""" - hasher = PasswordHasher(rounds=4) + def test_rounds_below_min_raises(self): + with pytest.raises(ValueError, match="rounds must be between"): + PasswordHasher(rounds=3) - h1 = hasher.hash_password("SamePass1!") - h2 = hasher.hash_password("SamePass1!") + def test_rounds_above_max_raises(self): + with pytest.raises(ValueError, match="rounds must be between"): + PasswordHasher(rounds=32) - assert h1 != h2 + def test_rounds_zero_raises(self): + with pytest.raises(ValueError): + PasswordHasher(rounds=0) - def test_verify_correct_password(self): - """正确密码验证通过""" - hasher = PasswordHasher(rounds=4) - hashed = hasher.hash_password("Correct1!") + def test_rounds_negative_raises(self): + with pytest.raises(ValueError): + PasswordHasher(rounds=-5) - assert hasher.verify_password("Correct1!", hashed) is True - def test_verify_wrong_password(self): - """错误密码验证失败""" - hasher = PasswordHasher(rounds=4) - hashed = hasher.hash_password("Right123!") +# ── hash_password ────────────────────────────────────────────────────────── - assert hasher.verify_password("Wrong123!", hashed) is False - def test_hash_empty_password_raises(self): - """空密码哈希抛出 ValueError""" - hasher = PasswordHasher(rounds=4) +# 用 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_verify_empty_password_returns_false(self): - """空密码验证返回 False""" - hasher = PasswordHasher(rounds=4) - hashed = hasher.hash_password("TestPass1!") + 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_verify_empty_hash_returns_false(self): - """空哈希验证返回 False""" - hasher = PasswordHasher(rounds=4) + def test_empty_hash_returns_false(self): + hasher = _make_hasher() + assert hasher.verify_password("test", "") is False - assert hasher.verify_password("TestPass1!", "") 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_verify_invalid_hash_format(self): - """无效格式的哈希验证返回 False(不抛异常)""" - hasher = PasswordHasher(rounds=4) + def test_invalid_hash_format_returns_false(self): + hasher = _make_hasher() + assert hasher.verify_password("test", "not-a-valid-hash") is False - assert hasher.verify_password("TestPass1!", "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_needs_rehash_same_rounds(self): - """相同 rounds 不需要重新哈希""" - hasher = PasswordHasher(rounds=4) - hashed = hasher.hash_password("TestPass1!") + 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_needs_rehash_different_rounds(self): - """不同 rounds 需要重新哈希""" - hasher_low = PasswordHasher(rounds=4) - hasher_high = PasswordHasher(rounds=5) + 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 - hashed = hasher_low.hash_password("TestPass1!") - assert hasher_high.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_needs_rehash_invalid_hash(self): - """无效哈希格式返回 False(不抛异常)""" - hasher = PasswordHasher(rounds=4) + 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 - assert hasher.needs_rehash("invalid_hash") is False + def test_invalid_hash_returns_false(self): + hasher = _make_hasher() + 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) + def test_empty_hash_returns_false(self): + hasher = _make_hasher() + assert hasher.needs_rehash("") is False -class TestPasswordValidator: - """PasswordValidator 测试""" +# ── PasswordValidator 初始化 ──────────────────────────────────────────────── - def test_strong_password_passes(self): - """强密码通过验证""" + +class TestPasswordValidatorInit: + def test_default_settings(self): validator = PasswordValidator() - valid, error = validator.validate("Str0ngP@ss") + 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 - 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): - """自定义最小长度""" + def test_custom_settings(self): validator = PasswordValidator( min_length=12, require_uppercase=False, require_lowercase=False, require_digit=False, + require_special=True, ) - valid, _ = validator.validate("123456789012") # 12字符 - assert valid is True + assert validator.min_length == 12 + assert validator.require_uppercase is False + assert validator.require_special 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") +# ── 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_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" + 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_password_hasher_exists(self): - """全局 password_hasher 实例存在""" + 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_password_validator_exists(self): - """全局 password_validator 实例存在""" + def test_global_validator_exists(self): 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 + + 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