stash
This commit is contained in:
parent
8de6ae8d8b
commit
4c9e8f942e
2 changed files with 63 additions and 1 deletions
|
|
@ -47,6 +47,17 @@ class GroupDetail(APIView, ViewSetMixin):
|
|||
return Response(GroupSerializer(group).data)
|
||||
|
||||
|
||||
class GroupDetailByHandle(APIView, ViewSetMixin):
|
||||
authentication_classes = [SignatureAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, name, domain, format=None): # /api/groups/handle/<name>/<domain>/
|
||||
group = get_object_or_404(Group, name=name, domain=domain)
|
||||
if not group.is_member(request.user):
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(GroupSerializer(group).data)
|
||||
|
||||
|
||||
@api_view(['DELETE'])
|
||||
@authentication_classes([SignatureAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
|
|
@ -80,7 +91,7 @@ def createGroupInvite(request, pk, format=None): # /api/groups/<pk>/invites/
|
|||
return Response(status=status.HTTP_208_ALREADY_REPORTED, data={'status': 'already a member'})
|
||||
secret = secrets.token_hex(64)
|
||||
GroupInvite.objects.create(group=group, invitee_username=invitee_username, invitee_domain=invitee_domain,
|
||||
secret=secret)
|
||||
secret=secret)
|
||||
return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': 'pending'})
|
||||
|
||||
|
||||
|
|
@ -88,6 +99,7 @@ class GroupInvitesIncoming(APIView, ViewSetMixin):
|
|||
"""/api/groupinvites/ - the invitee's own view of their pending invites, and the delivery
|
||||
endpoint an inviter's client posts to directly on the invitee's own home backend (see
|
||||
docs/design-in-progress/groups-mvp.md's invite/accept dance)."""
|
||||
|
||||
def get(self, request, format=None): # /api/groupinvites/ - only ever a local user checking their own invites
|
||||
raw_request = request.body.decode('utf-8')
|
||||
if not (user := authenticate_request_against_local_users(request, raw_request)):
|
||||
|
|
@ -186,6 +198,7 @@ def acceptGroupInvite(request, format=None): # /api/group_invites/accept/ - lan
|
|||
urlpatterns = [
|
||||
path('groups/', Groups.as_view(), name='groups'),
|
||||
path('groups/<int:pk>/', GroupDetail.as_view(), name='group_detail'),
|
||||
path('groups/handle/<str:name>/<str:domain>/', GroupDetailByHandle.as_view(), name='group_detail_by_handle'),
|
||||
path('groups/<int:pk>/members/<int:identity_id>/', removeGroupMember, name='remove_group_member'),
|
||||
path('groups/<int:pk>/invites/', createGroupInvite, name='create_group_invite'),
|
||||
path('groupinvites/', GroupInvitesIncoming.as_view(), name='group_invites_incoming'),
|
||||
|
|
|
|||
|
|
@ -63,6 +63,24 @@ class GroupApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
|
|||
reply = client.get('/api/groups/{}/'.format(self.f['group1'].id), self.f['local_user2'])
|
||||
self.assertEqual(reply.status_code, 404)
|
||||
|
||||
def test_group_detail_by_handle_member(self):
|
||||
self.prepare_groups()
|
||||
group = self.f['group1']
|
||||
reply = client.get('/api/groups/handle/{}/{}/'.format(group.name, group.domain), self.f['local_user1'])
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
self.assertEqual(reply.json()['id'], group.id)
|
||||
self.assertEqual(reply.json()['handle'], str(group))
|
||||
|
||||
def test_group_detail_by_handle_non_member(self):
|
||||
self.prepare_groups()
|
||||
group = self.f['group1']
|
||||
reply = client.get('/api/groups/handle/{}/{}/'.format(group.name, group.domain), self.f['local_user2'])
|
||||
self.assertEqual(reply.status_code, 404)
|
||||
|
||||
def test_group_detail_by_handle_no_such_group(self):
|
||||
reply = client.get('/api/groups/handle/nonexistent/example.com/', self.f['local_user1'])
|
||||
self.assertEqual(reply.status_code, 404)
|
||||
|
||||
def test_remove_member(self):
|
||||
self.prepare_groups()
|
||||
self.f['group1'].members.add(self.f['local_user2'].public_identity)
|
||||
|
|
@ -180,6 +198,9 @@ class GroupInviteApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
|
|||
self.assertEqual(reply.status_code, 201)
|
||||
identity = KnownIdentity.objects.get(username='newmember', domain='remote.example')
|
||||
self.assertTrue(group.is_member(identity))
|
||||
# This lands on the group's own home backend, not the invitee's -- the invitee here isn't
|
||||
# even a local ToolshedUser on this backend, so there's nothing to point at locally.
|
||||
self.assertEqual(GroupMembership.objects.count(), 0)
|
||||
|
||||
def test_accept_bad_signature(self):
|
||||
group = self.f['group1']
|
||||
|
|
@ -247,6 +268,23 @@ class GroupMembershipApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase
|
|||
self.assertEqual(reply.status_code, 204)
|
||||
self.assertEqual(GroupMembership.objects.count(), 0)
|
||||
|
||||
def test_record_membership_already_a_member_is_idempotent(self):
|
||||
# Re-invited (or re-accepting) into a group we already have a pointer for shouldn't blow
|
||||
# up on the unique_together constraint, and shouldn't duplicate the pointer either.
|
||||
invitee = self.f['local_user2']
|
||||
GroupMembership.objects.create(
|
||||
user=invitee, group_name='remoteworkshop', group_domain='other.example')
|
||||
incoming = GroupInviteIncoming.objects.create(
|
||||
group_name='remoteworkshop', group_domain='other.example',
|
||||
inviter_username='someone', inviter_domain='other.example',
|
||||
invitee_user=invitee, secret='some-secret')
|
||||
|
||||
reply = client.post('/api/groupinvites/{}/accept/'.format(incoming.id), invitee)
|
||||
|
||||
self.assertEqual(reply.status_code, 201)
|
||||
self.assertEqual(GroupMembership.objects.filter(user=invitee).count(), 1)
|
||||
self.assertEqual(GroupInviteIncoming.objects.count(), 0)
|
||||
|
||||
def test_list_memberships(self):
|
||||
GroupMembership.objects.create(
|
||||
user=self.f['local_user1'], group_name='remoteworkshop', group_domain='other.example')
|
||||
|
|
@ -259,3 +297,14 @@ class GroupMembershipApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase
|
|||
reply2 = client.get('/api/groupmemberships/', self.f['local_user2'])
|
||||
self.assertEqual(reply2.status_code, 200)
|
||||
self.assertEqual(len(reply2.json()), 0)
|
||||
|
||||
def test_list_memberships_unauthorized(self):
|
||||
GroupMembership.objects.create(
|
||||
user=self.f['local_user1'], group_name='remoteworkshop', group_domain='other.example')
|
||||
|
||||
reply = client.get('/api/groupmemberships/', self.f['ext_user1'])
|
||||
|
||||
# authenticate() returns bare None (not raise) for a caller with no local ToolshedUser, so
|
||||
# DRF falls through to IsAuthenticated denying an anonymous request -- 403, not 401 (same
|
||||
# as any other SignatureAuthenticationLocal-only endpoint, see e.g. dropFriend).
|
||||
self.assertEqual(reply.status_code, 403)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue