from __future__ import annotations from unittest.mock import AsyncMock import pytest from app.projects.services.collaboration_service import CollaborationService @pytest.mark.asyncio async def test_project_collaborator_listing_passes_authenticated_workspace() -> None: repository = type("Repository", (), {})() repository.list_project_collaborators = AsyncMock(return_value=[]) service = CollaborationService(repository) assert await service.list_project_collaborators("workspace-a", "project-a") == [] repository.list_project_collaborators.assert_awaited_once_with("workspace-a", "project-a") @pytest.mark.asyncio async def test_project_collaborator_removal_passes_authenticated_workspace() -> None: repository = type("Repository", (), {})() repository.remove_project_collaborator = AsyncMock() service = CollaborationService(repository) await service.remove_project_collaborator("workspace-a", "project-a", "user-b") repository.remove_project_collaborator.assert_awaited_once_with( "workspace-a", "project-a", "user-b" ) @pytest.mark.asyncio async def test_invitation_requires_workspace_admin_or_owner() -> None: repository = type("Repository", (), {})() repository.get_membership = AsyncMock(return_value=type( "Membership", (), {"role": "member", "status": "active"} )()) service = CollaborationService(repository) with pytest.raises(Exception, match="Only workspace owners or admins"): await service.invite_member("workspace-a", "user-a", "new@example.com", "member") repository.invite_member_with_hash = AsyncMock() repository.invite_member_with_hash.assert_not_awaited() @pytest.mark.asyncio async def test_member_role_changes_reject_privileged_unknown_roles() -> None: repository = type("Repository", (), {})() repository.get_membership = AsyncMock(return_value=type( "Membership", (), {"role": "admin", "status": "active", "user_id": "admin-a"} )()) repository.update_member_role = AsyncMock() service = CollaborationService(repository) with pytest.raises(ValueError, match="Member role must be admin or member"): await service.update_member_role("workspace-a", "admin-a", "user-b", "owner") repository.update_member_role.assert_not_awaited() @pytest.mark.asyncio @pytest.mark.parametrize("role", ["owner", "admin"]) async def test_workspace_admin_guard_allows_active_admin_roles(role: str) -> None: from types import SimpleNamespace from app.projects.api import _require_workspace_admin membership = type("Membership", (), {"role": role, "status": "active"})() collaboration = SimpleNamespace(get_membership=AsyncMock(return_value=membership)) request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace( container=SimpleNamespace(collaboration=collaboration) ))) await _require_workspace_admin(request, "workspace-a", "user-a") collaboration.get_membership.assert_awaited_once_with("workspace-a", "user-a") @pytest.mark.asyncio @pytest.mark.parametrize( "membership", [ None, type("Membership", (), {"role": "member", "status": "active"})(), type("Membership", (), {"role": "admin", "status": "inactive"})(), ], ) async def test_workspace_admin_guard_rejects_non_admin_or_inactive_membership(membership) -> None: from types import SimpleNamespace from app.projects.api import _require_workspace_admin from app.security.errors import ForbiddenError collaboration = SimpleNamespace(get_membership=AsyncMock(return_value=membership)) request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace( container=SimpleNamespace(collaboration=collaboration) ))) with pytest.raises(ForbiddenError): await _require_workspace_admin(request, "workspace-a", "user-a")