Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add User's Group Membership Details to User Info (Fixes #49) #76

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
43 changes: 41 additions & 2 deletions fedora/clients/fasjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async def get_group(self, groupname, params=None):
return response.json().get("result")

async def get_user(self, username, params=None):
"""looks up a group by the groupname"""
"""looks up a user by username"""
try:
response = await self._get("/".join(["users", username]), params=params)
except NoResult as e:
Expand All @@ -64,7 +64,7 @@ async def get_user(self, username, params=None):
return response.json().get("result")

async def search_users(self, params=None):
"""looks up a group by the groupname"""
"""looks up users given a search term"""
response = await self._get("/".join(["search", "users"]), params=params)
return response.json().get("result")

Expand Down Expand Up @@ -101,3 +101,42 @@ async def get_users_by_matrix_id(self, matrix_id: str) -> dict | str:
)

return searchresult[0]

async def get_user_groups(self, username, params=None):
"""
Retrieves all groups a user belongs to, including membership type (member or sponsor).

Args:
username (str): Username of the user to query.
params (dict, optional): Additional query parameters to include in the request.

Returns:
List[dict] or None:
- A list of dictionaries containing group information:
- groupname (str): Name of the group.
- membership_type (str): "member" or "sponsor" depending on the user's role.

Raises
InfoGatherError: If there's an error fetching data from Fasjson or
if the user is not found (404).
"""

try:
response = await self._get("/".join(["users", username, "groups"]), params=params)
except NoResult as e:
KimFarida marked this conversation as resolved.
Show resolved Hide resolved
raise InfoGatherError(
f"Sorry, but Fedora Accounts user '{username}' does not exist"
) from e
user_groups = response.json().get("groups", [])

group_details = []
for groupname in user_groups:
sponsors = await self.get_group_membership(groupname, "sponsors", params=params)

membership_type = "Member"
if sponsors is not None and username in sponsors:
membership_type = "Sponsor"

group_details.append({"groupname": groupname, "membership_type": membership_type})

return group_details
13 changes: 11 additions & 2 deletions fedora/fas.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,20 +120,29 @@ async def _user_info(self, evt: MessageEvent, username: str | None) -> None:
await evt.mark_read()
try:
user = await get_fasuser(username or evt.sender, evt, self.plugin.fasjsonclient)
groups = await self.plugin.fasjsonclient.get_user_groups(user.get("username"))
except InfoGatherError as e:
await evt.respond(e.message)
return

await evt.respond(
respond_message = (
f"User: {user.get('username')},{NL}"
f"Name: {user.get('human_name')},{NL}"
f"Pronouns: {' or '.join(user.get('pronouns') or ['unset'])},{NL}"
f"Creation: {user.get('creation')},{NL}"
f"Timezone: {user.get('timezone')},{NL}"
f"Locale: {user.get('locale')},{NL}"
f"GPG Key IDs: {' and '.join(k for k in user['gpgkeyids'] or ['None'])}{NL}"
f"GPG Key IDs: {' and '.join(k for k in user['gpgkeyids'] or ['None'])},{NL}"
)

group_info = [
f"Group: {group['groupname']} - Role: {group['membership_type']}" for group in groups
]

respond_message += f"User Groups: {', '.join(group_info) if groups else 'None'}{NL}"

await evt.respond(respond_message)

async def _user_localtime(self, evt: MessageEvent, username: str | None) -> None:
await evt.mark_read()
try:
Expand Down
64 changes: 64 additions & 0 deletions tests/clients/test_fasjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,67 @@ async def test_get_users_by_matrix_id_multiple_users(monkeypatch):
),
):
await client.get_users_by_matrix_id("@cookie:biscuit.test")


@pytest.mark.parametrize(
"username,expected_url,expected_groups",
[
(
"biscuit_eater",
"users/biscuit_eater/groups",
[
{"groupname": "group1", "membership_type": "Member"},
{"groupname": "group2", "membership_type": "Member"},
],
),
(
"sponsor_user",
"users/sponsor_user/groups",
[
{"groupname": "group1", "membership_type": "Sponsor"},
{"groupname": "group2", "membership_type": "Member"},
],
),
],
)
async def test_get_user_groups(monkeypatch, username, expected_url, expected_groups):
client = FasjsonClient("http://fasjson.example.com")

mock__get = mock.AsyncMock(
return_value=httpx.Response(
200,
json={"groups": ["group1", "group2"]},
)
)
monkeypatch.setattr(client, "_get", mock__get)

async def mock_get_group_membership(groupname, membership_type, params=None):
if username == "sponsor_user" and groupname == "group1":
return [username]
else:
return None

monkeypatch.setattr(client, "get_group_membership", mock_get_group_membership)

response = await client.get_user_groups(username)

assert response == expected_groups


@pytest.mark.parametrize(
"errorcode,expected_result",
[
(404, "Sorry, but Fedora Accounts user 'biscuit_eater' does not exist"),
(403, "Sorry, could not get info from FASJSON (code 403)"),
],
)
async def test_get_user_groups_errors(respx_mock, errorcode, expected_result):
client = FasjsonClient("http://fasjson.example.com")
respx_mock.get("http://fasjson.example.com").mock(
return_value=httpx.Response(
errorcode,
json={"result": "biscuits"},
)
)
with pytest.raises(InfoGatherError, match=(re.escape(expected_result))):
await client.get_user_groups("biscuit_eater")
42 changes: 41 additions & 1 deletion tests/test_fas.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,34 @@ async def test_localtime(bot, plugin, respx_mock, monkeypatch, tz, response, com
assert bot.sent[0].content.body == expected


@pytest.mark.parametrize(
"groups, expected_message",
[
([], "User Groups: None\n"),
(
[{"groupname": "Fedora Apps", "membership_type": "Member"}],
"User Groups: Group: Fedora Apps - Role: Member\n",
),
(
[
{"groupname": "Nmstate", "membership_type": "Member"},
{"groupname": "Fedora Apps", "membership_type": "Sponsor"},
],
"User Groups: Group: Nmstate - Role: Member, Group: Fedora Apps - Role: Sponsor\n",
),
],
)
def test_build_user_groups_message(groups, expected_message):
NL = "\n"
respond_message = ""
group_info = [
f"Group: {group['groupname']} - Role: {group['membership_type']}" for group in groups
]
respond_message += f"User Groups: {', '.join(group_info) if groups else 'None'}{NL}"

assert respond_message == expected_message


@pytest.mark.parametrize("alias", [None, "fasinfo"])
async def test_user_info(bot, plugin, respx_mock, alias):
respx_mock.get("http://fasjson.example.com/v1/users/dummy/").mock(
Expand All @@ -303,6 +331,13 @@ async def test_user_info(bot, plugin, respx_mock, alias):
},
)
)

respx_mock.get("http://fasjson.example.com/v1/users/dummy/groups/").mock(
return_value=httpx.Response(
200,
json={"groups": []},
)
)
if not alias:
await bot.send("!user info dummy")
else:
Expand All @@ -315,6 +350,11 @@ async def test_user_info(bot, plugin, respx_mock, alias):
"Creation: None,\n "
"Timezone: None,\n "
"Locale: None,\n "
"GPG Key IDs: None"
"GPG Key IDs: None,\n "
"User Groups: None"
)

print(bot.sent[0].content.body + "\n")
print(expected)

assert bot.sent[0].content.body == expected