""" 密码重置 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"