Compare commits

...

1 Commits

Author SHA1 Message Date
AI Bot 6300acf164 test(wave131): password_hasher密码哈希+验证+55单测
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (pull_request) Failing after 1s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 0s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Failing after 0s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 0s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Failing after 0s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 0s
AI Code Review / AI Code Review (pull_request) Failing after 1s
PR Automation / Auto Approve on CI Green (pull_request) Failing after 1s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 0s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 0s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 0s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m8s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 27s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 3m25s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 9m19s
CI/CD Pipeline / CI Gate (pull_request) Failing after 0s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
- PasswordHasher: 初始化(8)/hash_password(9)/verify_password(9)/needs_rehash(6)
- PasswordValidator: 初始化(2)/validate(16)
- 全局实例: 4
- 覆盖场景: bcrypt格式/盐值随机性/大小写敏感/Unicode/特殊字符/
  空密码/无效hash/不同rounds重哈希/长度/大小写/数字/特殊字符
2026-07-27 18:28:53 +08:00
+274 -182
View File
@@ -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