""" 接受/拒绝邀请 Use Case 测试 """ from datetime import datetime, timedelta, timezone from unittest.mock import Mock import pytest from packages.application.workspace.accept_invitation_use_case import ( AcceptInvitationRequest, AcceptInvitationUseCase, DeclineInvitationRequest, DeclineInvitationUseCase, ) from packages.domain.entities import ( InvitationStatus, User, Workspace, WorkspaceInvitation, WorkspaceMember, ) class TestAcceptInvitationUseCase: """接受邀请测试""" @pytest.fixture def mock_workspace_repo(self): repo = Mock() repo.find_by_id = Mock(return_value=None) return repo @pytest.fixture def mock_member_repo(self): repo = Mock() repo.find_by_workspace_and_user = Mock(return_value=None) repo.save = Mock() return repo @pytest.fixture def mock_invitation_repo(self): repo = Mock() repo.find_by_token = Mock(return_value=None) repo.save = Mock() return repo @pytest.fixture def mock_user_repo(self): repo = Mock() repo.find_by_id = Mock(return_value=None) return repo @pytest.fixture def use_case( self, mock_workspace_repo, mock_member_repo, mock_invitation_repo, mock_user_repo, ): return AcceptInvitationUseCase( workspace_repository=mock_workspace_repo, workspace_member_repository=mock_member_repo, workspace_invitation_repository=mock_invitation_repo, user_repository=mock_user_repo, ) @pytest.fixture def test_workspace(self): return Workspace( id="workspace-123", name="Test Workspace", owner_user_id="owner-id", ) @pytest.fixture def test_user(self): return User( id="user-123", email="invitee@test.com", username="invitee", display_name="Invitee User", ) @pytest.fixture def valid_invitation(self): return WorkspaceInvitation( id="invitation-1", workspace_id="workspace-123", inviter_user_id="inviter-id", invitee_email="invitee@test.com", role="member", invitation_token="valid-token", status=InvitationStatus.PENDING, expires_at=datetime.now(timezone.utc) + timedelta(days=7), ) def test_accept_invitation_success( self, use_case, mock_workspace_repo, mock_member_repo, mock_invitation_repo, mock_user_repo, test_workspace, test_user, valid_invitation, ): """测试接受邀请成功""" mock_invitation_repo.find_by_token.return_value = valid_invitation mock_user_repo.find_by_id.return_value = test_user mock_workspace_repo.find_by_id.return_value = test_workspace mock_member_repo.find_by_workspace_and_user.return_value = None request = AcceptInvitationRequest( invitation_token="valid-token", user_id="user-123", ) response, error = use_case.execute(request) assert error is None assert response is not None assert response.workspace_id == "workspace-123" assert response.workspace_name == "Test Workspace" assert response.role == "member" # 验证创建了成员记录 mock_member_repo.save.assert_called_once() member = mock_member_repo.save.call_args[0][0] assert member.user_id == "user-123" assert member.role == "member" assert member.invited_by == "inviter-id" # 验证更新了邀请状态 assert valid_invitation.status == InvitationStatus.ACCEPTED assert valid_invitation.accepted_at is not None def test_accept_invitation_success_with_naive_database_datetime( self, use_case, mock_workspace_repo, mock_member_repo, mock_invitation_repo, mock_user_repo, test_workspace, test_user, valid_invitation, ): """测试数据库返回 naive datetime 时仍可接受邀请""" valid_invitation.expires_at = (datetime.now(timezone.utc) + timedelta(days=7)).replace(tzinfo=None) mock_invitation_repo.find_by_token.return_value = valid_invitation mock_user_repo.find_by_id.return_value = test_user mock_workspace_repo.find_by_id.return_value = test_workspace mock_member_repo.find_by_workspace_and_user.return_value = None response, error = use_case.execute(AcceptInvitationRequest(invitation_token="valid-token", user_id="user-123")) assert error is None assert response is not None assert response.workspace_id == "workspace-123" def test_accept_invitation_invalid_token(self, use_case, mock_invitation_repo): """测试无效令牌""" mock_invitation_repo.find_by_token.return_value = None request = AcceptInvitationRequest( invitation_token="invalid-token", user_id="user-123", ) response, error = use_case.execute(request) assert response is None assert error == "Invalid invitation token" def test_accept_invitation_already_accepted( self, use_case, mock_invitation_repo, valid_invitation, ): """测试邀请已被接受""" valid_invitation.status = InvitationStatus.ACCEPTED mock_invitation_repo.find_by_token.return_value = valid_invitation request = AcceptInvitationRequest( invitation_token="valid-token", user_id="user-123", ) response, error = use_case.execute(request) assert response is None assert error == "Invitation has already been accepted" def test_accept_invitation_expired( self, use_case, mock_invitation_repo, valid_invitation, ): """测试邀请已过期""" valid_invitation.expires_at = datetime.now(timezone.utc) - timedelta(days=1) mock_invitation_repo.find_by_token.return_value = valid_invitation request = AcceptInvitationRequest( invitation_token="valid-token", user_id="user-123", ) response, error = use_case.execute(request) assert response is None assert error == "Invitation has expired" assert valid_invitation.status == InvitationStatus.EXPIRED def test_accept_invitation_email_mismatch( self, use_case, mock_invitation_repo, mock_user_repo, valid_invitation, ): """测试邮箱不匹配""" mock_invitation_repo.find_by_token.return_value = valid_invitation different_user = User( id="user-123", email="different@test.com", username="different", display_name="Different User", ) mock_user_repo.find_by_id.return_value = different_user request = AcceptInvitationRequest( invitation_token="valid-token", user_id="user-123", ) response, error = use_case.execute(request) assert response is None assert error == "This invitation is for a different email address" def test_accept_invitation_already_member( self, use_case, mock_workspace_repo, mock_member_repo, mock_invitation_repo, mock_user_repo, test_workspace, test_user, valid_invitation, ): """测试用户已经是成员""" mock_invitation_repo.find_by_token.return_value = valid_invitation mock_user_repo.find_by_id.return_value = test_user mock_workspace_repo.find_by_id.return_value = test_workspace existing_member = WorkspaceMember( id="member-1", workspace_id="workspace-123", user_id="user-123", role="admin", ) mock_member_repo.find_by_workspace_and_user.return_value = existing_member request = AcceptInvitationRequest( invitation_token="valid-token", user_id="user-123", ) response, error = use_case.execute(request) assert error is None assert response is not None assert response.role == "admin" # 返回现有角色 # 不创建新成员记录 mock_member_repo.save.assert_not_called() # 但仍标记邀请为已接受 assert valid_invitation.status == InvitationStatus.ACCEPTED class TestDeclineInvitationUseCase: """拒绝邀请测试""" @pytest.fixture def mock_invitation_repo(self): repo = Mock() repo.find_by_token = Mock(return_value=None) repo.save = Mock() return repo @pytest.fixture def use_case(self, mock_invitation_repo): return DeclineInvitationUseCase( workspace_invitation_repository=mock_invitation_repo, ) @pytest.fixture def valid_invitation(self): return WorkspaceInvitation( id="invitation-1", workspace_id="workspace-123", inviter_user_id="inviter-id", invitee_email="invitee@test.com", role="member", invitation_token="valid-token", status=InvitationStatus.PENDING, expires_at=datetime.now(timezone.utc) + timedelta(days=7), ) def test_decline_invitation_success(self, use_case, mock_invitation_repo, valid_invitation): """测试拒绝邀请成功""" mock_invitation_repo.find_by_token.return_value = valid_invitation request = DeclineInvitationRequest(invitation_token="valid-token") success, error = use_case.execute(request) assert success is True assert error is None assert valid_invitation.status == InvitationStatus.DECLINED def test_decline_invitation_invalid_token(self, use_case, mock_invitation_repo): """测试无效令牌""" mock_invitation_repo.find_by_token.return_value = None request = DeclineInvitationRequest(invitation_token="invalid-token") success, error = use_case.execute(request) assert success is False assert error == "Invalid invitation token" def test_decline_invitation_already_accepted(self, use_case, mock_invitation_repo, valid_invitation): """测试邀请已被接受""" valid_invitation.status = InvitationStatus.ACCEPTED mock_invitation_repo.find_by_token.return_value = valid_invitation request = DeclineInvitationRequest(invitation_token="valid-token") success, error = use_case.execute(request) assert success is False assert error == "Invitation has already been accepted"