From 4a5a61033199a19d02e43e532da5ce7903c860d0 Mon Sep 17 00:00:00 2001 From: Aman Gupta Date: Wed, 13 May 2026 00:55:26 +0530 Subject: [PATCH 1/2] feat(groups): implement change_group_member_permission and fix type hints --- fossology/groups.py | 51 ++++++++++++++++++++++---- fossology/users.py | 10 +++--- tests/test_groups.py | 85 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 11 deletions(-) diff --git a/fossology/groups.py b/fossology/groups.py index b569509..5582128 100644 --- a/fossology/groups.py +++ b/fossology/groups.py @@ -16,6 +16,7 @@ class Groups: """Class dedicated to all "groups" related endpoints""" def list_groups(self, deletable: bool = False) -> list[Group]: + """Get the list of groups (accessible groups for user, all groups for admin) If parameter deletable is True, the method will return only deletable groups. @@ -63,7 +64,7 @@ def list_group_members(self, group_id: int) -> list[UserGroupMember]: description = f"Unable to get a list of members for group {group_id}" raise FossologyApiError(description, response) - def create_group(self, name: str): + def create_group(self, name: str) -> None: """Create a group API Endpoint: POST /groups @@ -81,8 +82,8 @@ def create_group(self, name: str): description = f"Group {name} already exists, failed to create group or no group name provided" raise FossologyApiError(description, response) - def delete_group(self, group_id: int): - """Create a group + def delete_group(self, group_id: int) -> None: + """Delete a group API Endpoint: DELETE /groups/{group_id} @@ -100,7 +101,7 @@ def delete_group(self, group_id: int): def add_group_member( self, group_id: int, user_id: int, perm: MemberPerm = MemberPerm.USER - ): + ) -> None: """Add a user to a group API Endpoint: POST /groups/{group_id}/user/{user_id} @@ -128,7 +129,45 @@ def add_group_member( ) raise FossologyApiError(description, response) - def delete_group_member(self, group_id: int, user_id: int): + def change_group_member_permission( + self, group_id: int, user_id: int, perm: MemberPerm + ) -> None: + """Change the permission of a user in a group + + API Endpoint: PATCH /groups/{group_id}/user/{user_id} + + :param group_id: the id of the group + :param user_id: the id of the user + :param perm: the new permission level for the user + :type group_id: int + :type user_id: int + :type perm: MemberPerm + :raises FossologyApiError: if the REST call failed + """ + data = {"perm": perm.value} + response = self.session.patch( + f"{self.api}/groups/{group_id}/user/{user_id}", json=data + ) + if response.status_code == 200: + logger.info( + f"Permission of user {user_id} in group {group_id} has been updated to {perm.name}." + ) + elif response.status_code == 400: + description = f"Validation error while changing permission of user {user_id} in group {group_id}." + raise FossologyApiError(description, response) + elif response.status_code == 403: + description = f"Not authorized to change permission of user {user_id} in group {group_id}." + raise FossologyApiError(description, response) + elif response.status_code == 404: + description = f"User {user_id} or group {group_id} not found." + raise FossologyApiError(description, response) + else: + description = ( + f"An error occurred while changing permission of user {user_id} in group {group_id}" + ) + raise FossologyApiError(description, response) + + def delete_group_member(self, group_id: int, user_id: int) -> None: """Delete a user from a group API Endpoint: DELETE /groups/{group_id}/user/{user_id} @@ -152,4 +191,4 @@ def delete_group_member(self, group_id: int, user_id: int): description = ( f"An error occurred while deleting user {user_id} from group {group_id}" ) - raise FossologyApiError(description, response) + raise FossologyApiError(description, response) \ No newline at end of file diff --git a/fossology/users.py b/fossology/users.py index ce10fba..3b42259 100644 --- a/fossology/users.py +++ b/fossology/users.py @@ -13,7 +13,7 @@ class Users: """Class dedicated to all "users" related endpoints""" - def detail_user(self, user_id): + def detail_user(self, user_id: int) -> "User": """Get details of Fossology user. API Endpoint: GET /users/{id} @@ -38,7 +38,7 @@ def detail_user(self, user_id): description = f"Error while getting details for user {user_id}" raise FossologyApiError(description, response) - def list_users(self): + def list_users(self) -> list: """List all users from the Fossology instance API Endpoint: GET /users @@ -64,7 +64,7 @@ def list_users(self): description = f"Unable to get a list of users from {self.host}" # type: ignore raise FossologyApiError(description, response) - def create_user(self, user_spec): + def create_user(self, user_spec: dict) -> None: """Create a new Fossology user API Endpoint: POST /users @@ -114,7 +114,7 @@ def create_user(self, user_spec): description = f"Error while creating user {user_spec['name']}" raise FossologyApiError(description, response) - def delete_user(self, user): + def delete_user(self, user: "User") -> None: """Delete a Fossology user. API Endpoint: DELETE /users/{id} @@ -129,4 +129,4 @@ def delete_user(self, user): return else: description = f"Error while deleting user {user.name} ({user.id})" - raise FossologyApiError(description, response) + raise FossologyApiError(description, response) \ No newline at end of file diff --git a/tests/test_groups.py b/tests/test_groups.py index f16ba9b..4379a89 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -179,3 +179,88 @@ def test_delete_group_member_if_group_does_not_exists_raises_fossology_api_error with pytest.raises(FossologyApiError) as excinfo: foss.delete_group_member(42, created_foss_user.id) assert f"Member {created_foss_user.id} or group 42 not found." in str(excinfo.value) + + +@responses.activate +def test_change_group_member_permission_validation_error( + foss_server: str, foss: fossology.Fossology +): + responses.add( + responses.PATCH, f"{foss_server}/api/v1/groups/42/user/42", status=400 + ) + with pytest.raises(FossologyApiError) as excinfo: + foss.change_group_member_permission(42, 42, MemberPerm.ADMIN) + assert "Validation error while changing permission of user 42 in group 42." in str( + excinfo.value + ) + + +@responses.activate +def test_change_group_member_permission_not_authorized( + foss_server: str, foss: fossology.Fossology +): + responses.add( + responses.PATCH, f"{foss_server}/api/v1/groups/42/user/42", status=403 + ) + with pytest.raises(FossologyApiError) as excinfo: + foss.change_group_member_permission(42, 42, MemberPerm.ADMIN) + assert "Not authorized to change permission of user 42 in group 42." in str( + excinfo.value + ) + + +@responses.activate +def test_change_group_member_permission_not_found( + foss_server: str, foss: fossology.Fossology +): + responses.add( + responses.PATCH, f"{foss_server}/api/v1/groups/42/user/42", status=404 + ) + with pytest.raises(FossologyApiError) as excinfo: + foss.change_group_member_permission(42, 42, MemberPerm.ADMIN) + assert "User 42 or group 42 not found." in str(excinfo.value) + + +@responses.activate +def test_change_group_member_permission_server_error( + foss_server: str, foss: fossology.Fossology +): + responses.add( + responses.PATCH, f"{foss_server}/api/v1/groups/42/user/42", status=500 + ) + with pytest.raises(FossologyApiError) as excinfo: + foss.change_group_member_permission(42, 42, MemberPerm.ADMIN) + assert "An error occurred while changing permission of user 42 in group 42" in str( + excinfo.value + ) + + +def test_change_group_member_permission( + foss: fossology.Fossology, + created_foss_user: User, + monkeypatch: pytest.MonkeyPatch, +): + mocked_logger = Mock() + monkeypatch.setattr("fossology.groups.logger", mocked_logger) + name = secrets.token_urlsafe(8) + foss.create_group(name) + group = get_group(foss, name) + foss.add_group_member(group.id, created_foss_user.id, MemberPerm.USER) + + # Change permission from USER to ADMIN + foss.change_group_member_permission(group.id, created_foss_user.id, MemberPerm.ADMIN) + assert ( + call( + f"Permission of user {created_foss_user.id} in group {group.id} has been updated to ADMIN." + ) + in mocked_logger.info.mock_calls + ) + + # Verify the new permission is reflected + members = foss.list_group_members(group.id) + for member in members: + if member.user.id == created_foss_user.id: + assert member.group_perm == MemberPerm.ADMIN.value + + # Cleanup + foss.delete_group(group.id) \ No newline at end of file From 116eadc8c6b3865419fffb7fc66fc7257483f67d Mon Sep 17 00:00:00 2001 From: Aman Gupta Date: Wed, 13 May 2026 21:03:07 +0530 Subject: [PATCH 2/2] test(groups): mark live PATCH test as xfail - endpoint not in v1 server --- tests/test_groups.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_groups.py b/tests/test_groups.py index 4379a89..eb98ff0 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -235,6 +235,11 @@ def test_change_group_member_permission_server_error( ) +@pytest.mark.xfail( + reason="PATCH /groups/{id}/user/{id} is not yet available in FOSSology API v1 server", + raises=FossologyApiError, + strict=True, +) def test_change_group_member_permission( foss: fossology.Fossology, created_foss_user: User,