@@ -229,6 +229,64 @@ def test_create_project_with_all_optional_fields(self, session_client, workspace
229229 assert response_data ["network" ] == project_data ["network" ]
230230
231231
232+ @pytest .mark .contract
233+ class TestProjectMemberAPI :
234+ """Test project member role operations"""
235+
236+ def get_project_member_url (self , workspace_slug : str , project_id : uuid .UUID , pk : uuid .UUID ) -> str :
237+ return f"/api/workspaces/{ workspace_slug } /projects/{ project_id } /members/{ pk } /"
238+
239+ @pytest .mark .django_db
240+ def test_workspace_admin_can_promote_member_above_project_role (self , session_client , workspace , create_user ):
241+ """Workspace admins can assign project roles above their own project role."""
242+ project = Project .objects .create (name = "Role Project" , identifier = "RP" , workspace = workspace )
243+ requesting_project_member = ProjectMember .objects .create (
244+ project = project , member = create_user , role = 5 , is_active = True
245+ )
246+
247+ target_user = User .objects .create_user (email = "target@example.com" , username = "target" )
248+ WorkspaceMember .objects .create (workspace = workspace , member = target_user , role = 15 , is_active = True )
249+ target_project_member = ProjectMember .objects .create (
250+ project = project , member = target_user , role = 15 , is_active = True
251+ )
252+
253+ url = self .get_project_member_url (workspace .slug , project .id , target_project_member .id )
254+ response = session_client .patch (url , {"role" : 20 }, format = "json" )
255+
256+ assert response .status_code == status .HTTP_200_OK
257+ target_project_member .refresh_from_db ()
258+ assert target_project_member .role == 20
259+
260+ requesting_project_member .refresh_from_db ()
261+ assert requesting_project_member .role == 5
262+
263+ @pytest .mark .django_db
264+ def test_project_member_cannot_promote_member_above_own_project_role (self , api_client , workspace ):
265+ """Non-workspace-admin project members cannot assign roles above their own project role."""
266+ project = Project .objects .create (name = "Protected Role Project" , identifier = "PRP" , workspace = workspace )
267+
268+ requesting_user = User .objects .create_user (email = "requester@example.com" , username = "requester" )
269+ WorkspaceMember .objects .create (workspace = workspace , member = requesting_user , role = 15 , is_active = True )
270+ ProjectMember .objects .create (project = project , member = requesting_user , role = 15 , is_active = True )
271+
272+ target_user = User .objects .create_user (email = "member-target@example.com" , username = "member-target" )
273+ WorkspaceMember .objects .create (workspace = workspace , member = target_user , role = 15 , is_active = True )
274+ target_project_member = ProjectMember .objects .create (
275+ project = project , member = target_user , role = 15 , is_active = True
276+ )
277+
278+ api_client .force_authenticate (user = requesting_user )
279+
280+ url = self .get_project_member_url (workspace .slug , project .id , target_project_member .id )
281+ response = api_client .patch (url , {"role" : 20 }, format = "json" )
282+
283+ assert response .status_code == status .HTTP_400_BAD_REQUEST
284+ assert response .data ["error" ] == "You cannot update a role that is higher than your own role"
285+
286+ target_project_member .refresh_from_db ()
287+ assert target_project_member .role == 15
288+
289+
232290@pytest .mark .contract
233291class TestProjectAPIGet (TestProjectBase ):
234292 """Test project GET operations"""
0 commit comments