Files
xiaoxia-saas/tests/unit/test_password_reset_use_case.py
T

212 lines
7.0 KiB
Python

"""
密码重置 Use Case 测试
"""
from datetime import datetime, timedelta, timezone
from unittest.mock import Mock
import pytest
from packages.application.auth.password_reset_use_case import (
RequestPasswordResetRequest,
RequestPasswordResetUseCase,
ResetPasswordRequest,
ResetPasswordUseCase,
)
from packages.domain.entities import User
class TestRequestPasswordResetUseCase:
"""请求密码重置测试"""
@pytest.fixture
def mock_user_repo(self):
repo = Mock()
repo.find_by_email = Mock(return_value=None)
repo.save = Mock()
return repo
@pytest.fixture
def use_case(self, mock_user_repo):
email_service = Mock()
email_service.send_password_reset_email.return_value = (True, None)
return RequestPasswordResetUseCase(
user_repository=mock_user_repo,
base_url="https://test.com",
token_expire_hours=1,
email_service=email_service,
)
@pytest.fixture
def test_user(self):
return User(
id="user-123",
email="test@example.com",
username="testuser",
display_name="Test User",
password_hash="hash",
)
def test_request_reset_success(self, use_case, mock_user_repo, test_user):
"""测试请求重置成功"""
mock_user_repo.find_by_email.return_value = test_user
request = RequestPasswordResetRequest(email="test@example.com")
success, error = use_case.execute(request)
assert success is True
assert error is None
# 验证保存了用户
mock_user_repo.save.assert_called_once()
saved_user = mock_user_repo.save.call_args[0][0]
assert saved_user.password_reset_token is not None
assert saved_user.password_reset_expires_at is not None
# 验证发送了邮件
use_case.email_service.send_password_reset_email.assert_called_once()
def test_request_reset_user_not_exists(self, use_case, mock_user_repo):
"""测试用户不存在(仍返回成功,避免暴露)"""
mock_user_repo.find_by_email.return_value = None
request = RequestPasswordResetRequest(email="nonexistent@example.com")
success, error = use_case.execute(request)
assert success is True # 安全考虑,仍返回成功
assert error is None
# 不发送邮件
use_case.email_service.send_password_reset_email.assert_not_called()
def test_request_reset_missing_email(self, use_case):
"""测试缺少邮箱"""
request = RequestPasswordResetRequest(email="")
success, error = use_case.execute(request)
assert success is False
assert error == "Email is required"
class TestResetPasswordUseCase:
"""重置密码测试"""
@pytest.fixture
def mock_user_repo(self):
repo = Mock()
repo.find_by_password_reset_token = Mock(return_value=None)
repo.save = Mock()
return repo
@pytest.fixture
def use_case(self, mock_user_repo):
return ResetPasswordUseCase(user_repository=mock_user_repo)
@pytest.fixture
def test_user(self):
return User(
id="user-123",
email="test@example.com",
username="testuser",
display_name="Test User",
password_hash="old-hash",
password_reset_token="valid-token",
password_reset_expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
)
def test_reset_password_success(self, use_case, mock_user_repo, test_user):
"""测试重置密码成功"""
mock_user_repo.find_by_password_reset_token.return_value = test_user
request = ResetPasswordRequest(
token="valid-token",
new_password="NewSecurePass123",
)
success, error = use_case.execute(request)
assert success is True
assert error is None
# 验证密码已更新
assert test_user.password_hash != "old-hash"
assert test_user.password_reset_token is None
assert test_user.password_reset_expires_at is None
# 验证保存了用户
mock_user_repo.save.assert_called_once()
def test_reset_password_success_with_naive_database_datetime(self, use_case, mock_user_repo, test_user):
"""测试数据库返回 naive datetime 时仍可重置密码"""
test_user.password_reset_expires_at = (datetime.now(timezone.utc) + timedelta(hours=1)).replace(tzinfo=None)
mock_user_repo.find_by_password_reset_token.return_value = test_user
success, error = use_case.execute(ResetPasswordRequest(token="valid-token", new_password="NewSecurePass123"))
assert success is True
assert error is None
mock_user_repo.save.assert_called_once()
def test_reset_password_weak_password(self, use_case, mock_user_repo, test_user):
"""测试弱密码"""
mock_user_repo.find_by_password_reset_token.return_value = test_user
request = ResetPasswordRequest(
token="valid-token",
new_password="weak",
)
success, error = use_case.execute(request)
assert success is False
assert "at least 8 characters" in error
def test_reset_password_invalid_token(self, use_case, mock_user_repo):
"""测试无效令牌"""
mock_user_repo.find_by_password_reset_token.return_value = None
request = ResetPasswordRequest(
token="invalid-token",
new_password="NewSecurePass123",
)
success, error = use_case.execute(request)
assert success is False
assert error == "Invalid or expired reset token"
def test_reset_password_expired_token(self, use_case, mock_user_repo, test_user):
"""测试过期令牌"""
test_user.password_reset_expires_at = datetime.now(timezone.utc) - timedelta(hours=1)
mock_user_repo.find_by_password_reset_token.return_value = test_user
request = ResetPasswordRequest(
token="valid-token",
new_password="NewSecurePass123",
)
success, error = use_case.execute(request)
assert success is False
assert error == "Reset token has expired"
def test_reset_password_missing_token(self, use_case):
"""测试缺少令牌"""
request = ResetPasswordRequest(
token="",
new_password="NewSecurePass123",
)
success, error = use_case.execute(request)
assert success is False
assert error == "Reset token is required"
def test_reset_password_missing_password(self, use_case, mock_user_repo, test_user):
"""测试缺少新密码"""
mock_user_repo.find_by_password_reset_token.return_value = test_user
request = ResetPasswordRequest(
token="valid-token",
new_password="",
)
success, error = use_case.execute(request)
assert success is False
assert error == "New password is required"