Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70c8bb7022 | |||
| 5eb1213041 |
Executable
+318
@@ -0,0 +1,318 @@
|
||||
"""密码哈希与验证模块单元测试."""
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.application.auth.password_handler import (
|
||||
PasswordHandler,
|
||||
configure_password_handler,
|
||||
get_password_handler,
|
||||
)
|
||||
from packages.application.auth.password_hasher import (
|
||||
PasswordHasher,
|
||||
PasswordValidator,
|
||||
)
|
||||
|
||||
# ==================== PasswordValidator ====================
|
||||
|
||||
|
||||
class TestPasswordValidator:
|
||||
@pytest.fixture
|
||||
def default_validator(self):
|
||||
return PasswordValidator()
|
||||
|
||||
@pytest.fixture
|
||||
def strict_validator(self):
|
||||
return PasswordValidator(
|
||||
min_length=12,
|
||||
require_uppercase=True,
|
||||
require_lowercase=True,
|
||||
require_digit=True,
|
||||
require_special=True,
|
||||
)
|
||||
|
||||
class TestBasicValidation:
|
||||
def test_valid_password(self, default_validator):
|
||||
ok, msg = default_validator.validate("SecurePass123")
|
||||
assert ok is True
|
||||
assert msg is None
|
||||
|
||||
def test_empty_password(self, default_validator):
|
||||
ok, msg = default_validator.validate("")
|
||||
assert ok is False
|
||||
assert "empty" in msg.lower()
|
||||
|
||||
def test_none_password_treated_as_empty(self, default_validator):
|
||||
# None被not判定为falsy,返回空密码错误
|
||||
ok, msg = default_validator.validate(None)
|
||||
assert ok is False
|
||||
assert "empty" in msg.lower()
|
||||
|
||||
class TestMinLength:
|
||||
def test_too_short(self, default_validator):
|
||||
ok, msg = default_validator.validate("Ab1")
|
||||
assert ok is False
|
||||
assert "8" in msg
|
||||
|
||||
def test_exact_min_length(self, default_validator):
|
||||
# 刚好8个字符
|
||||
ok, _ = default_validator.validate("Abcdefg1")
|
||||
assert ok is True
|
||||
|
||||
def test_custom_min_length(self, strict_validator):
|
||||
ok, msg = strict_validator.validate("Short1!")
|
||||
assert ok is False
|
||||
assert "12" in msg
|
||||
|
||||
class TestUppercase:
|
||||
def test_no_uppercase(self, default_validator):
|
||||
ok, msg = default_validator.validate("password123")
|
||||
assert ok is False
|
||||
assert "uppercase" in msg.lower()
|
||||
|
||||
def test_with_uppercase(self, default_validator):
|
||||
ok, _ = default_validator.validate("Password123")
|
||||
assert ok is True
|
||||
|
||||
def test_disabled_requirement(self):
|
||||
v = PasswordValidator(require_uppercase=False)
|
||||
ok, _ = v.validate("password123")
|
||||
assert ok is True
|
||||
|
||||
class TestLowercase:
|
||||
def test_no_lowercase(self, default_validator):
|
||||
ok, msg = default_validator.validate("PASSWORD123")
|
||||
assert ok is False
|
||||
assert "lowercase" in msg.lower()
|
||||
|
||||
def test_with_lowercase(self, default_validator):
|
||||
ok, _ = default_validator.validate("Password123")
|
||||
assert ok is True
|
||||
|
||||
def test_disabled_requirement(self):
|
||||
v = PasswordValidator(require_lowercase=False)
|
||||
ok, _ = v.validate("PASSWORD123")
|
||||
assert ok is True
|
||||
|
||||
class TestDigit:
|
||||
def test_no_digit(self, default_validator):
|
||||
ok, msg = default_validator.validate("Passworddd")
|
||||
assert ok is False
|
||||
assert "digit" in msg.lower()
|
||||
|
||||
def test_with_digit(self, default_validator):
|
||||
ok, _ = default_validator.validate("Password1")
|
||||
assert ok is True
|
||||
|
||||
def test_disabled_requirement(self):
|
||||
v = PasswordValidator(require_digit=False)
|
||||
ok, _ = v.validate("Passworddd")
|
||||
assert ok is True
|
||||
|
||||
class TestSpecialChar:
|
||||
def test_no_special_when_not_required(self, default_validator):
|
||||
ok, _ = default_validator.validate("Password123")
|
||||
assert ok is True
|
||||
|
||||
def test_no_special_when_required(self, strict_validator):
|
||||
ok, msg = strict_validator.validate("Password1234")
|
||||
assert ok is False
|
||||
assert "special" in msg.lower()
|
||||
|
||||
def test_with_special(self, strict_validator):
|
||||
ok, _ = strict_validator.validate("Password123!")
|
||||
assert ok is True
|
||||
|
||||
def test_various_special_chars(self, strict_validator):
|
||||
for char in "!@#$%^&*()_+-=[]{}|;:,.<>?~":
|
||||
pw = f"LongPassword1{char}" # 13字符,含大小写数字特殊
|
||||
ok, msg = strict_validator.validate(pw)
|
||||
assert ok is True, f"special char {char} should be valid: {msg}"
|
||||
|
||||
class TestAllDisabled:
|
||||
def test_all_disabled_min_length_only(self):
|
||||
v = PasswordValidator(
|
||||
min_length=1,
|
||||
require_uppercase=False,
|
||||
require_lowercase=False,
|
||||
require_digit=False,
|
||||
require_special=False,
|
||||
)
|
||||
ok, _ = v.validate("a")
|
||||
assert ok is True
|
||||
|
||||
def test_all_disabled_empty_still_fails(self):
|
||||
v = PasswordValidator(
|
||||
min_length=1,
|
||||
require_uppercase=False,
|
||||
require_lowercase=False,
|
||||
require_digit=False,
|
||||
require_special=False,
|
||||
)
|
||||
ok, _ = v.validate("")
|
||||
assert ok is False
|
||||
|
||||
|
||||
# ==================== PasswordHasher ====================
|
||||
|
||||
|
||||
class TestPasswordHasher:
|
||||
@pytest.fixture
|
||||
def hasher(self):
|
||||
return PasswordHasher(rounds=4) # 用最低rounds加速测试
|
||||
|
||||
class TestHashPassword:
|
||||
def test_hash_returns_string(self, hasher):
|
||||
result = hasher.hash_password("testpassword")
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_hash_starts_with_bcrypt_prefix(self, hasher):
|
||||
result = hasher.hash_password("testpassword")
|
||||
assert result.startswith("$2")
|
||||
|
||||
def test_hash_contains_rounds(self, hasher):
|
||||
result = hasher.hash_password("testpassword")
|
||||
parts = result.split("$")
|
||||
assert parts[2] == "04" # bcrypt rounds格式是两位数
|
||||
|
||||
def test_hash_different_salts(self, hasher):
|
||||
# 同一密码两次哈希结果不同(因为salt随机)
|
||||
h1 = hasher.hash_password("samepassword")
|
||||
h2 = hasher.hash_password("samepassword")
|
||||
assert h1 != h2
|
||||
|
||||
def test_hash_empty_password_raises(self, hasher):
|
||||
with pytest.raises(ValueError, match="empty"):
|
||||
hasher.hash_password("")
|
||||
|
||||
def test_hash_unicode_password(self, hasher):
|
||||
result = hasher.hash_password("密码Pass123!")
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_hash_long_password(self, hasher):
|
||||
long_pw = "a" * 72 # bcrypt最大72字节
|
||||
result = hasher.hash_password(long_pw)
|
||||
assert isinstance(result, str)
|
||||
|
||||
class TestVerifyPassword:
|
||||
def test_verify_correct_password(self, hasher):
|
||||
hashed = hasher.hash_password("CorrectPass123")
|
||||
assert hasher.verify_password("CorrectPass123", hashed) is True
|
||||
|
||||
def test_verify_wrong_password(self, hasher):
|
||||
hashed = hasher.hash_password("CorrectPass123")
|
||||
assert hasher.verify_password("WrongPass123", hashed) is False
|
||||
|
||||
def test_verify_empty_password(self, hasher):
|
||||
hashed = hasher.hash_password("testpass")
|
||||
assert hasher.verify_password("", hashed) is False
|
||||
|
||||
def test_verify_empty_hash(self, hasher):
|
||||
assert hasher.verify_password("testpass", "") is False
|
||||
|
||||
def test_verify_invalid_hash_format(self, hasher):
|
||||
assert hasher.verify_password("testpass", "invalid-hash-format") is False
|
||||
|
||||
def test_verify_none_hash(self, hasher):
|
||||
assert hasher.verify_password("testpass", None) is False
|
||||
|
||||
def test_verify_unicode_password(self, hasher):
|
||||
pw = "密码Pass123!"
|
||||
hashed = hasher.hash_password(pw)
|
||||
assert hasher.verify_password(pw, hashed) is True
|
||||
|
||||
class TestNeedsRehash:
|
||||
def test_same_rounds_no_rehash(self, hasher):
|
||||
hashed = hasher.hash_password("testpass")
|
||||
assert hasher.needs_rehash(hashed) is False
|
||||
|
||||
def test_lower_rounds_needs_rehash(self):
|
||||
hasher_low = PasswordHasher(rounds=4)
|
||||
hashed = hasher_low.hash_password("testpass")
|
||||
|
||||
hasher_high = PasswordHasher(rounds=5)
|
||||
assert hasher_high.needs_rehash(hashed) is True
|
||||
|
||||
def test_higher_rounds_needs_rehash(self):
|
||||
hasher_high = PasswordHasher(rounds=5)
|
||||
hashed = hasher_high.hash_password("testpass")
|
||||
|
||||
hasher_low = PasswordHasher(rounds=4)
|
||||
assert hasher_low.needs_rehash(hashed) is True
|
||||
|
||||
def test_invalid_hash_no_rehash(self, hasher):
|
||||
assert hasher.needs_rehash("invalid-format") is False
|
||||
|
||||
def test_empty_hash_no_rehash(self, hasher):
|
||||
assert hasher.needs_rehash("") is False
|
||||
|
||||
class TestInit:
|
||||
def test_rounds_below_min_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
PasswordHasher(rounds=3)
|
||||
|
||||
def test_rounds_above_max_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
PasswordHasher(rounds=32)
|
||||
|
||||
def test_min_rounds_ok(self):
|
||||
h = PasswordHasher(rounds=4)
|
||||
assert h.rounds == 4
|
||||
|
||||
def test_max_rounds_ok(self):
|
||||
h = PasswordHasher(rounds=31)
|
||||
assert h.rounds == 31
|
||||
|
||||
|
||||
# ==================== PasswordHandler ====================
|
||||
|
||||
|
||||
class TestPasswordHandler:
|
||||
@pytest.fixture
|
||||
def handler(self):
|
||||
return PasswordHandler(rounds=4)
|
||||
|
||||
def test_hash_and_verify_roundtrip(self, handler):
|
||||
hashed = handler.hash_password("MySecurePass123")
|
||||
assert handler.verify_password("MySecurePass123", hashed) is True
|
||||
assert handler.verify_password("WrongPass", hashed) is False
|
||||
|
||||
def test_needs_rehash(self, handler):
|
||||
# 用当前rounds哈希,不需要rehash
|
||||
hashed = handler.hash_password("testpass")
|
||||
assert handler.needs_rehash(hashed) is False
|
||||
|
||||
def test_validate_strength(self, handler):
|
||||
# 强密码通过
|
||||
ok, msg = handler.validate_strength("StrongPass123")
|
||||
assert ok is True
|
||||
assert msg is None
|
||||
|
||||
# 弱密码不通过
|
||||
ok, msg = handler.validate_strength("weak")
|
||||
assert ok is False
|
||||
assert msg is not None
|
||||
|
||||
def test_hash_empty_raises(self, handler):
|
||||
with pytest.raises(ValueError):
|
||||
handler.hash_password("")
|
||||
|
||||
|
||||
class TestGlobalHandler:
|
||||
def test_get_password_handler_returns_instance(self):
|
||||
# 重置全局实例
|
||||
import packages.application.auth.password_handler as ph
|
||||
|
||||
ph._default_handler = None
|
||||
|
||||
handler = get_password_handler()
|
||||
assert isinstance(handler, PasswordHandler)
|
||||
|
||||
def test_configure_password_handler(self):
|
||||
handler = configure_password_handler(rounds=4)
|
||||
assert isinstance(handler, PasswordHandler)
|
||||
|
||||
# get应该返回同一个配置好的实例
|
||||
same_handler = get_password_handler()
|
||||
assert same_handler is handler
|
||||
Reference in New Issue
Block a user