File size: 3,856 Bytes
7cc81cb
 
20ed87f
7cc81cb
20ed87f
7cc81cb
20ed87f
7cc81cb
 
20ed87f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d93a0c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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")