Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions apps/api/plane/tests/contract/app/test_project_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
WorkspaceMember,
User,
)
from plane.app.permissions import ROLE


class TestProjectBase:
Expand Down Expand Up @@ -229,6 +230,100 @@ def test_create_project_with_all_optional_fields(self, session_client, workspace
assert response_data["network"] == project_data["network"]


@pytest.mark.contract
class TestProjectMemberAPI:
"""Test project member role operations"""

def get_project_member_url(self, workspace_slug: str, project_id: uuid.UUID, pk: uuid.UUID) -> str:
return f"/api/workspaces/{workspace_slug}/projects/{project_id}/members/{pk}/"

@pytest.mark.django_db
def test_workspace_admin_can_promote_member_above_project_role(self, session_client, workspace, create_user):
"""Workspace admins can assign project roles above their own project role."""
project = Project.objects.create(name="Role Project", identifier="RP", workspace=workspace)
requesting_project_member = ProjectMember.objects.create(
project=project, member=create_user, role=ROLE.GUEST.value, is_active=True
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes on preview without the PR. The bug it documents (workspace admin with low project role unable to promote) was already fixed by #9014 (is_workspace_admin = requester_workspace_role == ROLE.ADMIN.value). The test is still valuable as a regression guard, but it shouldn't be framed as proving this PR fixes anything — the production fix is already shipped.

target_user = User.objects.create_user(email="target@example.com", username="target")
WorkspaceMember.objects.create(
workspace=workspace, member=target_user, role=ROLE.MEMBER.value, is_active=True
)
target_project_member = ProjectMember.objects.create(
project=project, member=target_user, role=ROLE.MEMBER.value, is_active=True
)

url = self.get_project_member_url(workspace.slug, project.id, target_project_member.id)
response = session_client.patch(url, {"role": ROLE.ADMIN.value}, format="json")

assert response.status_code == status.HTTP_200_OK
target_project_member.refresh_from_db()
assert target_project_member.role == ROLE.ADMIN.value

requesting_project_member.refresh_from_db()
assert requesting_project_member.role == ROLE.GUEST.value

@pytest.mark.django_db
def test_non_admin_project_member_cannot_promote_member_to_admin(self, api_client, workspace):
"""Non-admin project members cannot promote project members."""
project = Project.objects.create(name="Protected Role Project", identifier="PRP", workspace=workspace)

requesting_user = User.objects.create_user(email="requester@example.com", username="requester")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion will fail.

With requester project_role=15 patching a target whose project_role=15, the first guard in partial_update fires:

if project_member.role >= requested_project_member.role and not is_workspace_admin:
    return Response({"error": "You cannot update the role of a member with a role equal to or higher than your own"},
                    status=status.HTTP_403_FORBIDDEN)

15 >= 15 is True, not False is True → returns 403 with the equal-to-or-higher message. The new 400 branch this test targets (member.py:277) is unreachable (see comment there).

Both assert response.status_code == 400 and the error string assertion will fail. To make the assertion match the new branch's message, the scenario needs the requester's project_role to be strictly greater than new_role for both prior guards — which contradicts the test name. Either redesign the scenario or assert against the actual 403 + message that the existing guard returns.

WorkspaceMember.objects.create(
workspace=workspace, member=requesting_user, role=ROLE.MEMBER.value, is_active=True
)
ProjectMember.objects.create(project=project, member=requesting_user, role=ROLE.MEMBER.value, is_active=True)

target_user = User.objects.create_user(email="member-target@example.com", username="member-target")
WorkspaceMember.objects.create(
workspace=workspace, member=target_user, role=ROLE.MEMBER.value, is_active=True
)
target_project_member = ProjectMember.objects.create(
project=project, member=target_user, role=ROLE.MEMBER.value, is_active=True
)

api_client.force_authenticate(user=requesting_user)

url = self.get_project_member_url(workspace.slug, project.id, target_project_member.id)
response = api_client.patch(url, {"role": ROLE.ADMIN.value}, format="json")

assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.data["error"] == "You do not have permission to update roles"

target_project_member.refresh_from_db()
assert target_project_member.role == ROLE.MEMBER.value

@pytest.mark.django_db
def test_project_member_cannot_promote_lower_project_member(self, api_client, workspace):
"""Non-admin project members cannot promote lower project members."""
project = Project.objects.create(name="No Expansion Project", identifier="NEP", workspace=workspace)

requesting_user = User.objects.create_user(email="role-member@example.com", username="role-member")
WorkspaceMember.objects.create(
workspace=workspace, member=requesting_user, role=ROLE.MEMBER.value, is_active=True
)
ProjectMember.objects.create(project=project, member=requesting_user, role=ROLE.MEMBER.value, is_active=True)

target_user = User.objects.create_user(email="lower-target@example.com", username="lower-target")
WorkspaceMember.objects.create(
workspace=workspace, member=target_user, role=ROLE.MEMBER.value, is_active=True
)
target_project_member = ProjectMember.objects.create(
project=project, member=target_user, role=ROLE.GUEST.value, is_active=True
)

api_client.force_authenticate(user=requesting_user)

url = self.get_project_member_url(workspace.slug, project.id, target_project_member.id)
response = api_client.patch(url, {"role": ROLE.MEMBER.value}, format="json")

assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.data["error"] == "You do not have permission to update roles"

target_project_member.refresh_from_db()
assert target_project_member.role == ROLE.GUEST.value


@pytest.mark.contract
class TestProjectAPIGet(TestProjectBase):
"""Test project GET operations"""
Expand Down
Loading