Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6300acf164 |
+274
-182
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user