df38101bd9
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 40s
CI/CD Pipeline / Frontend Lint (push) Successful in 2m26s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m55s
CI/CD Pipeline / Unit Tests (push) Successful in 4m6s
CI/CD Pipeline / Integration Tests (push) Successful in 1m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 15m52s
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 / ACR Image Cleanup (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
270 lines
8.9 KiB
Python
Executable File
270 lines
8.9 KiB
Python
Executable File
"""
|
|
密码哈希工具测试
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from packages.application.auth.password_hasher import PasswordHasher, PasswordValidator
|
|
|
|
|
|
class TestPasswordHasher:
|
|
"""密码哈希测试"""
|
|
|
|
@pytest.fixture
|
|
def hasher(self):
|
|
"""创建密码哈希器"""
|
|
return PasswordHasher(rounds=4) # 测试用低 cost,加快速度
|
|
|
|
def test_hash_password(self, hasher):
|
|
"""测试密码哈希"""
|
|
password = "MySecurePassword123"
|
|
hashed = hasher.hash_password(password)
|
|
|
|
assert isinstance(hashed, str)
|
|
assert len(hashed) > 0
|
|
assert hashed != password # 哈希后不等于原文
|
|
assert hashed.startswith("$2b$") # bcrypt 格式
|
|
|
|
def test_hash_same_password_different_result(self, hasher):
|
|
"""测试相同密码每次哈希结果不同(因为 salt 不同)"""
|
|
password = "MySecurePassword123"
|
|
hash1 = hasher.hash_password(password)
|
|
hash2 = hasher.hash_password(password)
|
|
|
|
assert hash1 != hash2 # salt 不同,哈希不同
|
|
|
|
def test_verify_correct_password(self, hasher):
|
|
"""测试验证正确的密码"""
|
|
password = "MySecurePassword123"
|
|
hashed = hasher.hash_password(password)
|
|
|
|
assert hasher.verify_password(password, hashed) is True
|
|
|
|
def test_verify_incorrect_password(self, hasher):
|
|
"""测试验证错误的密码"""
|
|
password = "MySecurePassword123"
|
|
hashed = hasher.hash_password(password)
|
|
|
|
assert hasher.verify_password("WrongPassword", hashed) is False
|
|
|
|
def test_verify_empty_password(self, hasher):
|
|
"""测试空密码验证"""
|
|
hashed = hasher.hash_password("test")
|
|
|
|
assert hasher.verify_password("", hashed) is False
|
|
|
|
def test_verify_empty_hash(self, hasher):
|
|
"""测试空哈希验证"""
|
|
assert hasher.verify_password("test", "") is False
|
|
|
|
def test_verify_invalid_hash(self, hasher):
|
|
"""测试无效的哈希"""
|
|
assert hasher.verify_password("test", "invalid-hash") is False
|
|
|
|
def test_hash_empty_password(self, hasher):
|
|
"""测试哈希空密码应该失败"""
|
|
with pytest.raises(ValueError, match="Password cannot be empty"):
|
|
hasher.hash_password("")
|
|
|
|
def test_invalid_rounds(self):
|
|
"""测试无效的 rounds 参数"""
|
|
with pytest.raises(ValueError, match="rounds must be between 4 and 31"):
|
|
PasswordHasher(rounds=2)
|
|
|
|
with pytest.raises(ValueError, match="rounds must be between 4 and 31"):
|
|
PasswordHasher(rounds=50)
|
|
|
|
def test_unicode_password(self, hasher):
|
|
"""测试 Unicode 密码"""
|
|
password = "密码123!@#"
|
|
hashed = hasher.hash_password(password)
|
|
|
|
assert hasher.verify_password(password, hashed) is True
|
|
assert hasher.verify_password("错误密码", hashed) is False
|
|
|
|
|
|
class TestPasswordValidator:
|
|
"""密码验证器测试"""
|
|
|
|
@pytest.fixture
|
|
def validator(self):
|
|
"""创建密码验证器"""
|
|
return PasswordValidator(
|
|
min_length=8,
|
|
require_uppercase=True,
|
|
require_lowercase=True,
|
|
require_digit=True,
|
|
require_special=False,
|
|
)
|
|
|
|
def test_valid_password(self, validator):
|
|
"""测试有效密码"""
|
|
valid, error = validator.validate("MyPassword123")
|
|
assert valid is True
|
|
assert error is None
|
|
|
|
def test_password_too_short(self, validator):
|
|
"""测试密码太短"""
|
|
valid, error = validator.validate("Pass1")
|
|
assert valid is False
|
|
assert "at least 8 characters" in error
|
|
|
|
def test_password_no_uppercase(self, validator):
|
|
"""测试没有大写字母"""
|
|
valid, error = validator.validate("mypassword123")
|
|
assert valid is False
|
|
assert "uppercase letter" in error
|
|
|
|
def test_password_no_lowercase(self, validator):
|
|
"""测试没有小写字母"""
|
|
valid, error = validator.validate("MYPASSWORD123")
|
|
assert valid is False
|
|
assert "lowercase letter" in error
|
|
|
|
def test_password_no_digit(self, validator):
|
|
"""测试没有数字"""
|
|
valid, error = validator.validate("MyPassword")
|
|
assert valid is False
|
|
assert "digit" in error
|
|
|
|
def test_password_with_special_chars(self):
|
|
"""测试要求特殊字符"""
|
|
validator = PasswordValidator(
|
|
min_length=8,
|
|
require_uppercase=True,
|
|
require_lowercase=True,
|
|
require_digit=True,
|
|
require_special=True,
|
|
)
|
|
|
|
# 没有特殊字符
|
|
valid, error = validator.validate("MyPassword123")
|
|
assert valid is False
|
|
assert "special character" in error
|
|
|
|
# 有特殊字符
|
|
valid, error = validator.validate("MyPassword123!")
|
|
assert valid is True
|
|
assert error is None
|
|
|
|
def test_empty_password(self, validator):
|
|
"""测试空密码"""
|
|
valid, error = validator.validate("")
|
|
assert valid is False
|
|
assert "cannot be empty" in error
|
|
|
|
def test_custom_min_length(self):
|
|
"""测试自定义最小长度"""
|
|
validator = PasswordValidator(
|
|
min_length=12,
|
|
require_uppercase=False,
|
|
require_lowercase=False,
|
|
require_digit=False,
|
|
require_special=False,
|
|
)
|
|
|
|
valid, error = validator.validate("short")
|
|
assert valid is False
|
|
assert "at least 12 characters" in error
|
|
|
|
valid, error = validator.validate("longenoughpassword")
|
|
assert valid is True
|
|
assert error is None
|
|
|
|
|
|
class TestPasswordHandler:
|
|
"""Password Handler 委托层测试"""
|
|
|
|
def test_hash_and_verify_password(self):
|
|
"""测试哈希和验证密码"""
|
|
from packages.application.auth.password_handler import PasswordHandler
|
|
|
|
handler = PasswordHandler(rounds=4)
|
|
hashed = handler.hash_password("MySecurePass123")
|
|
|
|
assert hashed != "MySecurePass123"
|
|
assert len(hashed) > 20
|
|
assert handler.verify_password("MySecurePass123", hashed) is True
|
|
assert handler.verify_password("WrongPassword", hashed) is False
|
|
|
|
def test_hash_empty_password_raises(self):
|
|
"""测试空密码抛出异常"""
|
|
from packages.application.auth.password_handler import PasswordHandler
|
|
|
|
handler = PasswordHandler(rounds=4)
|
|
with pytest.raises(ValueError):
|
|
handler.hash_password("")
|
|
|
|
def test_needs_rehash(self):
|
|
"""测试检测需要重新哈希"""
|
|
from packages.application.auth.password_handler import PasswordHandler
|
|
|
|
handler = PasswordHandler(rounds=4)
|
|
hashed = handler.hash_password("TestPass123")
|
|
|
|
# 相同 rounds 不需要重新哈希
|
|
assert handler.needs_rehash(hashed) is False
|
|
|
|
# 用更高 rounds 的 handler 检查,应该需要重新哈希
|
|
# 注意:bcrypt 的 rounds 体现在 hash 中,这里用不同 rounds 测试
|
|
high_rounds_handler = PasswordHandler(rounds=5)
|
|
# 低 rounds 的 hash 在高 rounds 配置下应该需要 rehash
|
|
assert high_rounds_handler.needs_rehash(hashed) is True
|
|
|
|
def test_validate_strength(self):
|
|
"""测试密码强度验证"""
|
|
from packages.application.auth.password_handler import PasswordHandler
|
|
|
|
handler = PasswordHandler(rounds=4)
|
|
|
|
# 弱密码
|
|
valid, error = handler.validate_strength("weak")
|
|
assert valid is False
|
|
assert error is not None
|
|
|
|
# 强密码
|
|
valid, error = handler.validate_strength("StrongPass123")
|
|
assert valid is True
|
|
assert error is None
|
|
|
|
def test_configure_and_get_default_handler(self):
|
|
"""测试配置和获取全局默认 handler"""
|
|
from packages.application.auth import password_handler as handler_module
|
|
from packages.application.auth.password_handler import (
|
|
configure_password_handler,
|
|
get_password_handler,
|
|
)
|
|
|
|
# 重置全局状态
|
|
handler_module._default_handler = None
|
|
|
|
# 配置
|
|
handler = configure_password_handler(rounds=4)
|
|
assert handler is not None
|
|
|
|
# 获取
|
|
same_handler = get_password_handler()
|
|
assert same_handler is handler
|
|
|
|
# 验证能正常工作
|
|
hashed = same_handler.hash_password("TestPass123")
|
|
assert same_handler.verify_password("TestPass123", hashed) is True
|
|
|
|
# 重置全局状态,避免影响其他测试
|
|
handler_module._default_handler = None
|
|
|
|
def test_get_password_handler_auto_creates_default(self):
|
|
"""测试未配置时获取 handler 会自动创建默认实例"""
|
|
from packages.application.auth import password_handler as handler_module
|
|
from packages.application.auth.password_handler import get_password_handler
|
|
|
|
# 重置全局状态
|
|
handler_module._default_handler = None
|
|
|
|
# 自动创建默认实例
|
|
handler = get_password_handler()
|
|
assert handler is not None
|
|
|
|
# 重置
|
|
handler_module._default_handler = None
|