Skip to content

Commit

Permalink
Add GET membership API
Browse files Browse the repository at this point in the history
Add an API for getting a single membership.
  • Loading branch information
seanh committed Dec 9, 2024
1 parent 2ebfb06 commit 0617567
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 2 deletions.
24 changes: 24 additions & 0 deletions docs/_extra/api-reference/hypothesis-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,30 @@ paths:
$ref: '#/components/schemas/User'

/groups/{id}/members/{user}:
# ----------------------------------------------------------
# GET groups/{id}/members/{user} - Get group member
# ----------------------------------------------------------
get:
tags:
- groups
summary: Get group member
description: Get a user's membership of a group.
Authenticated user must have read access to the group.
Does not require authentication for reading members of public groups.
security:
- ApiKey: []
- {}
parameters:
- $ref: '#/components/parameters/GroupID'
- $ref: '#/components/parameters/UserID'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Membership'

# ----------------------------------------------------------
# POST groups/{id}/members/{user} - Add user to group
# ----------------------------------------------------------
Expand Down
26 changes: 25 additions & 1 deletion docs/_extra/api-reference/hypothesis-v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,7 @@ paths:
Fetch a list of all members (users) in a group. Returned user resource only
contains public-facing user data. Authenticated user must have read access
to the group. Does not require authentication for reading members of
public groups. Returned members are unsorted.
public groups.
security:
- AuthClient: []
- ApiKey: []
Expand All @@ -878,6 +878,30 @@ paths:
$ref: '#/components/schemas/User'

/groups/{id}/members/{user}:
# ----------------------------------------------------------
# GET groups/{id}/members/{user} - Get group member
# ----------------------------------------------------------
get:
tags:
- groups
summary: Get group member
description: Fetch a user's membership of a group.
Authenticated user must have read access to the group.
Does not require authentication for reading members of public groups.
security:
- ApiKey: []
- {}
parameters:
- $ref: '#/components/parameters/GroupID'
- $ref: '#/components/parameters/UserID'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Membership'

# ----------------------------------------------------------
# POST groups/{id}/members/{user} - Add user to group
# ----------------------------------------------------------
Expand Down
1 change: 0 additions & 1 deletion docs/_extra/api-reference/schemas/membership.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,3 @@ Membership:
format: date-time
description: "When this membership was last updated (for example to change the role)"
example: "1970-01-01T00:00:00.000000+00:00"

12 changes: 12 additions & 0 deletions h/views/api/group_members.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ def list_members(context: GroupContext, request):
return {"meta": {"page": {"total": total}}, "data": membership_dicts}


@api_config(
versions=["v1", "v2"],
route_name="api.group_member",
request_method="GET",
link_name="group.member.read",
description="Fetch a group membership",
permission=Permission.Group.READ,
)
def get_member(context: GroupMembershipContext, request):
return GroupMembershipJSONPresenter(request, context.membership).asdict()


@api_config(
versions=["v1", "v2"],
route_name="api.group_member",
Expand Down
113 changes: 113 additions & 0 deletions tests/functional/api/groups/members_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,119 @@ def test_it_returns_an_error_if_offset_and_limit_are_invalid(
}


class TestGetMember:
def test_it(self, app, db_session, do_request, group, target_user):
response = do_request()

assert response.json == {
"authority": group.authority,
"userid": target_user.userid,
"username": target_user.username,
"display_name": target_user.display_name,
"roles": [GroupMembershipRoles.MEMBER],
"actions": [
"delete",
"updates.roles.member",
"updates.roles.moderator",
"updates.roles.admin",
"updates.roles.owner",
],
"created": f"1970-01-01T00:00:00.000000+00:00",
"updated": f"1970-01-01T00:00:01.000000+00:00",
}

def test_it_when_group_doesnt_exist(self, do_request):
response = do_request(pubid="doesnt_exist", status=404)

def test_it_when_target_user_doesnt_exist(self, do_request):
response = do_request(userid="doesnt_exist", status=404)

def test_it_when_authenticated_user_isnt_a_member_of_the_group(
self, do_request, factories, headers
):
headers.update(
**token_authorization_header(
factories.DeveloperToken(user=factories.User())
)
)

do_request(status=404)

def test_it_when_not_authenticated(self, do_request, headers):
del headers["Authorization"]

do_request(status=404)

def test_it_with_an_open_group(self, do_request, factories, headers, target_user):
group = factories.OpenGroup(memberships=[GroupMembership(user=target_user)])
# Non-group members and unauthenticated requests can read the
# memberships of open groups.
del headers["Authorization"]

do_request(pubid=group.pubid)

def test_it_with_a_restricted_group(
self, do_request, factories, headers, target_user
):
group = factories.RestrictedGroup(
memberships=[GroupMembership(user=target_user)]
)
# Non-group members and unauthenticated requests can read the
# memberships of restricted groups.
del headers["Authorization"]

do_request(pubid=group.pubid)

@pytest.fixture(autouse=True)
def group(self, factories):
return factories.Group()

@pytest.fixture(autouse=True)
def target_user(self, factories, group):
target_user = factories.User()
group.memberships.append(
GroupMembership(
user=target_user,
created=datetime(1970, 1, 1, 0, 0, 0),
updated=datetime(1970, 1, 1, 0, 0, 1),
)
)
return target_user

@pytest.fixture(autouse=True)
def authenticated_user(self, factories, group):
authenticated_user = factories.User()
group.memberships.append(
GroupMembership(
user=authenticated_user,
roles=[GroupMembershipRoles.OWNER],
created=datetime(1971, 1, 1, 0, 0, 0),
updated=datetime(1971, 1, 1, 0, 0, 1),
)
)
return authenticated_user

@pytest.fixture(autouse=True)
def token(self, factories, authenticated_user):
return factories.DeveloperToken(user=authenticated_user)

@pytest.fixture
def headers(self, factories, token):
return token_authorization_header(token)

@pytest.fixture
def do_request(self, app, db_session, group, target_user, headers):
def do_request(
pubid=group.pubid, userid=target_user.userid, headers=headers, status=200
):
db_session.commit()
return app.get(
f"/api/groups/{pubid}/members/{userid}", headers=headers, status=status
)

return do_request


class TestAddMember:
def test_it(self, do_request, group, user):
do_request()
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/h/views/api/group_members_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,23 @@ def context(self, factories):
)


class TestGetMember:
def test_it(self, context, pyramid_request, GroupMembershipJSONPresenter):
response = views.get_member(context, pyramid_request)

GroupMembershipJSONPresenter.assert_called_once_with(
pyramid_request, sentinel.membership
)
GroupMembershipJSONPresenter.return_value.asdict.assert_called_once_with()
assert response == GroupMembershipJSONPresenter.return_value.asdict.return_value

@pytest.fixture
def context(self):
return GroupMembershipContext(
group=sentinel.group, user=sentinel.user, membership=sentinel.membership
)


class TestRemoveMember:
def test_it(self, context, pyramid_request, group_members_service):
response = views.remove_member(context, pyramid_request)
Expand Down

0 comments on commit 0617567

Please sign in to comment.