Files
xiaoxia-saas/tests/unit/test_password_auth.py
xiaoxia 2273fb329f
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
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
test(wave208): 密码哈希与验证模块单测 +50测 (#1175)
2026-07-30 01:13:58 +08:00

319 lines
11 KiB
Python
Executable File

"""密码哈希与验证模块单元测试."""
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