7e505a04c7
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
251 lines
8.3 KiB
Python
Executable File
251 lines
8.3 KiB
Python
Executable File
"""密码哈希与验证器单元测试."""
|
|
|
|
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
|