This commit is contained in:
j3d1 2026-08-19 16:45:20 +02:00
parent 6d2167ac66
commit 4787acd8eb
23 changed files with 1241 additions and 25 deletions

View file

@ -0,0 +1,191 @@
from django.test import Client
from authentication.tests import SignatureAuthClient, UserTestMixin, GroupTestMixin, ToolshedTestCase, \
DummyExternalUser
from authentication.models import Group, GroupInvite, GroupInviteIncoming, KnownIdentity
client = SignatureAuthClient()
class GroupModelTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
self.prepare_groups()
def test_group_str(self):
self.assertEqual(str(self.f['group1']), '#group1@' + self.f['example_com'].name)
def test_is_member(self):
self.assertTrue(self.f['group1'].is_member(self.f['local_user1'].public_identity))
self.assertFalse(self.f['group1'].is_member(self.f['local_user2'].public_identity))
class GroupApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
def test_create_group(self):
reply = client.post('/api/groups/', self.f['local_user1'], {'name': 'workshop'})
self.assertEqual(reply.status_code, 201)
group = Group.objects.get(name='workshop')
self.assertEqual(group.domain, self.f['local_user1'].domain)
self.assertTrue(group.is_member(self.f['local_user1'].public_identity))
def test_create_group_duplicate_name(self):
client.post('/api/groups/', self.f['local_user1'], {'name': 'workshop'})
reply = client.post('/api/groups/', self.f['local_user1'], {'name': 'workshop'})
self.assertEqual(reply.status_code, 409)
def test_create_group_missing_name(self):
reply = client.post('/api/groups/', self.f['local_user1'], {})
self.assertEqual(reply.status_code, 400)
def test_list_groups(self):
self.prepare_groups()
reply = client.get('/api/groups/', self.f['local_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 1)
reply2 = client.get('/api/groups/', self.f['local_user2'])
self.assertEqual(reply2.status_code, 200)
self.assertEqual(len(reply2.json()), 0)
def test_group_detail_member(self):
self.prepare_groups()
reply = client.get('/api/groups/{}/'.format(self.f['group1'].id), self.f['local_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(reply.json()['handle'], str(self.f['group1']))
self.assertEqual(len(reply.json()['members']), 1)
def test_group_detail_non_member(self):
self.prepare_groups()
reply = client.get('/api/groups/{}/'.format(self.f['group1'].id), self.f['local_user2'])
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)
reply = client.delete('/api/groups/{}/members/{}/'.format(
self.f['group1'].id, self.f['local_user2'].public_identity.id), self.f['local_user1'])
self.assertEqual(reply.status_code, 204)
self.assertEqual(self.f['group1'].members.count(), 1)
def test_remove_last_member_blocked(self):
self.prepare_groups()
reply = client.delete('/api/groups/{}/members/{}/'.format(
self.f['group1'].id, self.f['local_user1'].public_identity.id), self.f['local_user1'])
self.assertEqual(reply.status_code, 400)
self.assertEqual(self.f['group1'].members.count(), 1)
def test_remove_member_non_member_denied(self):
self.prepare_groups()
self.f['group1'].members.add(self.f['local_user2'].public_identity)
reply = client.delete('/api/groups/{}/members/{}/'.format(
self.f['group1'].id, self.f['local_user1'].public_identity.id), self.f['ext_user1'])
self.assertEqual(reply.status_code, 404)
self.assertEqual(self.f['group1'].members.count(), 2)
class GroupInviteApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
self.prepare_groups()
def test_invite_local_member_full_flow(self):
group = self.f['group1']
inviter = self.f['local_user1']
invitee = self.f['local_user2']
reply1 = client.post('/api/groups/{}/invites/'.format(group.id), inviter, {'invitee': str(invitee)})
self.assertEqual(reply1.status_code, 201)
secret = reply1.json()['secret']
self.assertEqual(GroupInvite.objects.count(), 1)
reply2 = client.post('/api/groupinvites/', inviter, {
'group': str(group), 'inviter': str(inviter), 'inviter_key': inviter.public_key(),
'invitee': str(invitee), 'secret': secret
})
self.assertEqual(reply2.status_code, 201)
incoming = GroupInviteIncoming.objects.get()
self.assertEqual(incoming.invitee_user, invitee)
reply3 = client.get('/api/groupinvites/', invitee)
self.assertEqual(reply3.status_code, 200)
self.assertEqual(len(reply3.json()), 1)
self.assertEqual(reply3.json()[0]['group'], str(group))
self.assertEqual(reply3.json()[0]['inviter'], str(inviter))
reply4 = client.post('/api/group_invites/accept/', invitee, {
'group': str(group), 'invitee': str(invitee), 'invitee_key': invitee.public_key(), 'secret': secret
})
self.assertEqual(reply4.status_code, 201)
self.assertTrue(group.is_member(invitee.public_identity))
self.assertEqual(GroupInvite.objects.count(), 0)
reply5 = client.delete('/api/groupinvites/{}/'.format(incoming.id), invitee)
self.assertEqual(reply5.status_code, 204)
self.assertEqual(GroupInviteIncoming.objects.count(), 0)
def test_invite_non_member_denied(self):
group = self.f['group1']
reply = client.post('/api/groups/{}/invites/'.format(group.id), self.f['local_user2'],
{'invitee': str(self.f['ext_user1'])})
self.assertEqual(reply.status_code, 404)
self.assertEqual(GroupInvite.objects.count(), 0)
def test_invite_already_member(self):
group = self.f['group1']
group.members.add(self.f['local_user2'].public_identity)
reply = client.post('/api/groups/{}/invites/'.format(group.id), self.f['local_user1'],
{'invitee': str(self.f['local_user2'])})
self.assertEqual(reply.status_code, 208)
def test_decline_invite(self):
invite = GroupInviteIncoming.objects.create(
group_name=self.f['group1'].name, group_domain=self.f['group1'].domain,
inviter_username=self.f['local_user1'].username, inviter_domain=self.f['local_user1'].domain,
invitee_user=self.f['local_user2'], secret='some-secret')
reply = client.delete('/api/groupinvites/{}/'.format(invite.id), self.f['local_user2'])
self.assertEqual(reply.status_code, 204)
self.assertEqual(GroupInviteIncoming.objects.count(), 0)
def test_accept_wrong_secret(self):
group = self.f['group1']
inviter = self.f['local_user1']
invitee = self.f['local_user2']
client.post('/api/groups/{}/invites/'.format(group.id), inviter, {'invitee': str(invitee)})
reply = client.post('/api/group_invites/accept/', invitee, {
'group': str(group), 'invitee': str(invitee), 'invitee_key': invitee.public_key(),
'secret': 'not-the-right-secret'
})
self.assertEqual(reply.status_code, 404)
self.assertFalse(group.is_member(invitee.public_identity))
def test_accept_remote_member(self):
group = self.f['group1']
inviter = self.f['local_user1']
invitee = DummyExternalUser('newmember', 'remote.example', known=False)
reply1 = client.post('/api/groups/{}/invites/'.format(group.id), inviter, {'invitee': str(invitee)})
secret = reply1.json()['secret']
reply = client.post('/api/group_invites/accept/', invitee, {
'group': str(group), 'invitee': str(invitee), 'invitee_key': invitee.public_key(), 'secret': secret
})
self.assertEqual(reply.status_code, 201)
identity = KnownIdentity.objects.get(username='newmember', domain='remote.example')
self.assertTrue(group.is_member(identity))
def test_accept_bad_signature(self):
group = self.f['group1']
inviter = self.f['local_user1']
invitee = self.f['local_user2']
reply1 = client.post('/api/groups/{}/invites/'.format(group.id), inviter, {'invitee': str(invitee)})
secret = reply1.json()['secret']
bad_signature_client = SignatureAuthClient(bad_signature=True)
reply = bad_signature_client.post('/api/group_invites/accept/', invitee, {
'group': str(group), 'invitee': str(invitee), 'invitee_key': invitee.public_key(), 'secret': secret
})
self.assertEqual(reply.status_code, 401)
self.assertFalse(group.is_member(invitee.public_identity))

View file

@ -1,4 +1,4 @@
from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase
from authentication.tests import SignatureAuthClient, UserTestMixin, GroupTestMixin, ToolshedTestCase
from files.tests import FilesTestMixin
from toolshed.models import InventoryItem, Category
from toolshed.tests import InventoryTestMixin
@ -333,4 +333,87 @@ class TestInventoryItemWithFileApiTestCase(UserTestMixin, FilesTestMixin, Invent
'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}],
'files': [{'data': self.f['encoded_content3']}]
})
self.assertEqual(reply.status_code, 400)
self.assertEqual(reply.status_code, 400)
class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
self.prepare_groups()
self.f['group1'].members.add(self.f['local_user2'].public_identity)
def create_group_item(self, name='drill'):
return client.post('/api/inventory_items/', self.f['local_user1'], {
'name': name, 'owned_quantity': 1, 'availability_policy': 'private',
'owner_group': self.f['group1'].id,
})
def test_create_group_owned_item(self):
reply = self.create_group_item()
self.assertEqual(reply.status_code, 201)
item = InventoryItem.objects.get(name='drill')
self.assertIsNone(item.owner)
self.assertEqual(item.owner_group, self.f['group1'])
self.assertEqual(reply.json()['owner_group'], str(self.f['group1']))
self.assertIsNone(reply.json()['owner'])
def test_create_group_owned_item_non_member_denied(self):
reply = client.post('/api/inventory_items/', self.f['ext_user1'], {
'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private',
'owner_group': self.f['group1'].id,
})
self.assertEqual(reply.status_code, 403)
self.assertEqual(InventoryItem.objects.count(), 0)
def test_other_member_can_edit(self):
item_id = self.create_group_item().json()['id']
reply = client.patch('/api/inventory_items/{}/'.format(item_id), self.f['local_user2'], {
'name': 'drill-renamed'
})
self.assertEqual(reply.status_code, 200)
self.assertEqual(InventoryItem.objects.get(id=item_id).name, 'drill-renamed')
def test_other_member_can_delete(self):
item_id = self.create_group_item().json()['id']
reply = client.delete('/api/inventory_items/{}/'.format(item_id), self.f['local_user2'])
self.assertEqual(reply.status_code, 204)
self.assertEqual(InventoryItem.objects.filter(id=item_id).count(), 0)
def test_remote_member_without_local_account_can_edit(self):
# A remote member (no ToolshedUser row at all on this backend, only a KnownIdentity -
# see docs/design-in-progress/groups-mvp.md) must still be able to act on group-owned
# items here; it must not be treated as unauthorized just because .user.exists() is False.
self.f['group1'].members.add(self.f['ext_user1'].public_identity)
item_id = self.create_group_item().json()['id']
reply = client.get('/api/inventory_items/{}/'.format(item_id), self.f['ext_user1'])
self.assertEqual(reply.status_code, 200)
reply = client.patch('/api/inventory_items/{}/'.format(item_id), self.f['ext_user1'], {
'name': 'drill-renamed-by-remote-member'
})
self.assertEqual(reply.status_code, 200)
self.assertEqual(InventoryItem.objects.get(id=item_id).name, 'drill-renamed-by-remote-member')
def test_non_member_cannot_see_or_edit(self):
item_id = self.create_group_item().json()['id']
reply = client.get('/api/inventory_items/{}/'.format(item_id), self.f['ext_user1'])
self.assertEqual(reply.status_code, 404)
def test_group_items_excluded_from_personal_list(self):
self.create_group_item()
reply = client.get('/api/inventory_items/', self.f['local_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 0)
def test_group_items_listed_by_group_query_param(self):
self.create_group_item()
reply = client.get('/api/inventory_items/?group={}'.format(self.f['group1'].id), self.f['local_user2'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 1)
self.assertEqual(reply.json()[0]['name'], 'drill')
def test_group_items_not_listed_for_non_member_query_param(self):
self.create_group_item()
reply = client.get('/api/inventory_items/?group={}'.format(self.f['group1'].id), self.f['ext_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 0)