Compare commits

...

2 Commits

Author SHA1 Message Date
CI Bot 70c8bb7022 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 44s
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
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m30s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m22s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 54s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m17s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m53s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m39s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m28s
AI Code Review / AI Code Review (pull_request) Successful in 3m56s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m57s
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 Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m51s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 12s
2026-07-29 16:46:35 +00:00
xiaoxia 5eb1213041 test(wave208): 密码哈希与验证模块单测 +50测
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 17s
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
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m25s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m22s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
- PasswordValidator: 24测(长度/大小写/数字/特殊字符/各种配置组合)
- PasswordHasher: 18测(哈希/验证/needs_rehash/rounds边界)
- PasswordHandler: 8测(委托层/全局实例配置)
2026-07-30 00:42:44 +08:00
+318
View File
@@ -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