This commit is contained in:
j3d1 2026-08-26 22:18:55 +02:00
parent 23185e1721
commit 9f3dc7c993
8 changed files with 335 additions and 103 deletions

View file

@ -53,56 +53,47 @@ class GroupApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
def test_group_detail_member(self):
self.prepare_groups()
reply = client.get('/api/groups/{}/'.format(self.f['group1'].id), self.f['local_user1'])
group = self.f['group1']
reply = client.get('/api/groups/{}@{}/'.format(group.name, group.domain), self.f['local_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(reply.json()['handle'], str(self.f['group1']))
self.assertEqual(reply.json()['handle'], str(group))
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'])
group = self.f['group1']
reply = client.get('/api/groups/{}@{}/'.format(group.name, group.domain), 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'])
def test_group_detail_no_such_group(self):
reply = client.get('/api/groups/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)
reply = client.delete('/api/groups/{}/members/{}/'.format(
self.f['group1'].id, self.f['local_user2'].public_identity.id), self.f['local_user1'])
group = self.f['group1']
group.members.add(self.f['local_user2'].public_identity)
reply = client.delete('/api/groups/{}@{}/members/{}/'.format(
group.name, group.domain, 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)
self.assertEqual(group.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'])
group = self.f['group1']
reply = client.delete('/api/groups/{}@{}/members/{}/'.format(
group.name, group.domain, 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)
self.assertEqual(group.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'])
group = self.f['group1']
group.members.add(self.f['local_user2'].public_identity)
reply = client.delete('/api/groups/{}@{}/members/{}/'.format(
group.name, group.domain, 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)
self.assertEqual(group.members.count(), 2)
class GroupInviteApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
@ -116,7 +107,8 @@ class GroupInviteApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
inviter = self.f['local_user1']
invitee = self.f['local_user2']
reply1 = client.post('/api/groups/{}/invites/'.format(group.id), inviter, {'invitee': str(invitee)})
reply1 = client.post('/api/groups/{}@{}/invites/'.format(group.name, group.domain), inviter,
{'invitee': str(invitee)})
self.assertEqual(reply1.status_code, 201)
secret = reply1.json()['secret']
self.assertEqual(GroupInvite.objects.count(), 1)
@ -152,7 +144,7 @@ class GroupInviteApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
def test_invite_non_member_denied(self):
group = self.f['group1']
reply = client.post('/api/groups/{}/invites/'.format(group.id), self.f['local_user2'],
reply = client.post('/api/groups/{}@{}/invites/'.format(group.name, group.domain), self.f['local_user2'],
{'invitee': str(self.f['ext_user1'])})
self.assertEqual(reply.status_code, 404)
self.assertEqual(GroupInvite.objects.count(), 0)
@ -160,7 +152,7 @@ class GroupInviteApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
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'],
reply = client.post('/api/groups/{}@{}/invites/'.format(group.name, group.domain), self.f['local_user1'],
{'invitee': str(self.f['local_user2'])})
self.assertEqual(reply.status_code, 208)
@ -177,7 +169,7 @@ class GroupInviteApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
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)})
client.post('/api/groups/{}@{}/invites/'.format(group.name, group.domain), 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'
@ -189,7 +181,8 @@ class GroupInviteApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
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)})
reply1 = client.post('/api/groups/{}@{}/invites/'.format(group.name, group.domain), inviter,
{'invitee': str(invitee)})
secret = reply1.json()['secret']
reply = client.post('/api/group_invites/accept/', invitee, {
@ -206,7 +199,8 @@ class GroupInviteApiTestCase(UserTestMixin, GroupTestMixin, ToolshedTestCase):
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)})
reply1 = client.post('/api/groups/{}@{}/invites/'.format(group.name, group.domain), 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, {

View file

@ -353,7 +353,7 @@ class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTest
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,
'owner_group': str(self.f['group1'])[1:],
})
def test_create_group_owned_item(self):
@ -368,7 +368,7 @@ class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTest
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,
'owner_group': str(self.f['group1'])[1:],
})
self.assertEqual(reply.status_code, 403)
self.assertEqual(InventoryItem.objects.count(), 0)
@ -413,14 +413,14 @@ class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTest
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'])
reply = client.get('/api/inventory_items/?group={}'.format(str(self.f['group1'])[1:]), 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'])
reply = client.get('/api/inventory_items/?group={}'.format(str(self.f['group1'])[1:]), self.f['ext_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 0)
@ -435,7 +435,7 @@ class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTest
'owned_quantity': 3,
'tags': ['tag1', 'tag2'],
'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}],
'owner_group': self.f['group1'].id,
'owner_group': str(self.f['group1'])[1:],
})
self.assertEqual(reply.status_code, 201)
item = InventoryItem.objects.get(name='drill')
@ -451,20 +451,21 @@ class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTest
def test_create_group_owned_item_empty_fails(self):
# Parity with InventoryApiTestCase.test_post_new_item_empty: clean()'s name-or-files validation still applies.
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'availability_policy': 'private', 'owned_quantity': 1, 'owner_group': self.f['group1'].id,
'availability_policy': 'private', 'owned_quantity': 1, 'owner_group': str(self.f['group1'])[1:],
})
self.assertEqual(reply.status_code, 400)
self.assertEqual(InventoryItem.objects.count(), 0)
def test_create_group_owned_item_nonexistent_group(self):
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private', 'owner_group': 999999,
'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private',
'owner_group': 'nonexistent@example.com',
})
self.assertEqual(reply.status_code, 404)
self.assertEqual(InventoryItem.objects.count(), 0)
def test_group_items_listed_for_nonexistent_group_query_param(self):
reply = client.get('/api/inventory_items/?group=999999', self.f['local_user1'])
reply = client.get('/api/inventory_items/?group=nonexistent@example.com', self.f['local_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 0)
@ -495,7 +496,7 @@ class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTest
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private',
'category': 'cat1', 'tags': ['tag1'],
'owner_group': self.f['group1'].id,
'owner_group': str(self.f['group1'])[1:],
})
item_id = reply.json()['id']
reply = client.patch('/api/inventory_items/{}/'.format(item_id), self.f['local_user2'], {
@ -509,7 +510,7 @@ class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTest
def test_group_item_storage_location(self):
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private',
'storage_location': self.f['loc1'].id, 'owner_group': self.f['group1'].id,
'storage_location': self.f['loc1'].id, 'owner_group': str(self.f['group1'])[1:],
})
self.assertEqual(reply.status_code, 201)
item = InventoryItem.objects.get(name='drill')
@ -519,7 +520,7 @@ class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTest
# Parity with TestInventoryItemWithFileApiTestCase.test_post_item_with_file_id.
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private',
'files': [self.f['test_file1'].id], 'owner_group': self.f['group1'].id,
'files': [self.f['test_file1'].id], 'owner_group': str(self.f['group1'])[1:],
})
self.assertEqual(reply.status_code, 201)
item = InventoryItem.objects.get(name='drill')
@ -529,7 +530,7 @@ class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTest
reply = client.post('/api/inventory_items/', self.f['local_user1'], {
'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private',
'files': [{'data': self.f['encoded_content3'], 'mime_type': 'text/plain'}],
'owner_group': self.f['group1'].id,
'owner_group': str(self.f['group1'])[1:],
})
self.assertEqual(reply.status_code, 201)
item = InventoryItem.objects.get(name='drill')

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, StorageLocation
from toolshed.tests import InventoryTestMixin, LocationTestMixin
@ -129,3 +129,110 @@ class LocationApiTestCase(UserTestMixin, InventoryTestMixin, LocationTestMixin,
self.assertEqual(reply.status_code, 404)
self.assertEqual(StorageLocation.objects.filter(id=self.f['loc1'].id).count(), 1)
class GroupOwnedLocationApiTestCase(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_location(self, name='shelf'):
return client.post('/api/storage_locations/', self.f['local_user1'], {
'name': name, 'owner_group': str(self.f['group1'])[1:],
})
def test_create_group_owned_location(self):
reply = self.create_group_location()
self.assertEqual(reply.status_code, 201)
location = StorageLocation.objects.get(name='shelf')
self.assertIsNone(location.owner)
self.assertEqual(location.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_location_non_member_denied(self):
reply = client.post('/api/storage_locations/', self.f['ext_user1'], {
'name': 'shelf', 'owner_group': str(self.f['group1'])[1:],
})
self.assertEqual(reply.status_code, 403)
self.assertEqual(StorageLocation.objects.count(), 0)
def test_other_member_can_edit(self):
location_id = self.create_group_location().json()['id']
reply = client.patch('/api/storage_locations/{}/'.format(location_id), self.f['local_user2'], {
'name': 'shelf-renamed'
})
self.assertEqual(reply.status_code, 200)
self.assertEqual(StorageLocation.objects.get(id=location_id).name, 'shelf-renamed')
def test_other_member_can_delete(self):
location_id = self.create_group_location().json()['id']
reply = client.delete('/api/storage_locations/{}/'.format(location_id), self.f['local_user2'])
self.assertEqual(reply.status_code, 204)
self.assertEqual(StorageLocation.objects.filter(id=location_id).count(), 0)
def test_remote_member_without_local_account_can_edit(self):
# A remote member (KnownIdentity, no ToolshedUser row) must still act on group-owned
# locations - not unauthorized just because .user.exists() is False.
self.f['group1'].members.add(self.f['ext_user1'].public_identity)
location_id = self.create_group_location().json()['id']
reply = client.get('/api/storage_locations/{}/'.format(location_id), self.f['ext_user1'])
self.assertEqual(reply.status_code, 200)
reply = client.patch('/api/storage_locations/{}/'.format(location_id), self.f['ext_user1'], {
'name': 'shelf-renamed-by-remote-member'
})
self.assertEqual(reply.status_code, 200)
self.assertEqual(StorageLocation.objects.get(id=location_id).name, 'shelf-renamed-by-remote-member')
def test_non_member_cannot_see_or_edit(self):
location_id = self.create_group_location().json()['id']
reply = client.get('/api/storage_locations/{}/'.format(location_id), self.f['ext_user1'])
self.assertEqual(reply.status_code, 404)
def test_group_locations_excluded_from_personal_list(self):
self.create_group_location()
reply = client.get('/api/storage_locations/', self.f['local_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 0)
def test_group_locations_listed_by_group_query_param(self):
self.create_group_location()
reply = client.get('/api/storage_locations/?group={}'.format(str(self.f['group1'])[1:]), self.f['local_user2'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 1)
self.assertEqual(reply.json()[0]['name'], 'shelf')
def test_group_locations_not_listed_for_non_member_query_param(self):
self.create_group_location()
reply = client.get('/api/storage_locations/?group={}'.format(str(self.f['group1'])[1:]), self.f['ext_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 0)
def test_group_location_can_be_parent_for_group_member(self):
parent_id = self.create_group_location('shelf').json()['id']
reply = client.post('/api/storage_locations/', self.f['local_user2'], {
'name': 'bin', 'owner_group': str(self.f['group1'])[1:], 'parent': parent_id,
})
self.assertEqual(reply.status_code, 201)
self.assertEqual(reply.json()['path'], 'shelf/bin')
def test_own_personal_location_can_be_parent_of_group_location(self):
# OwnerScopedPrimaryKeyRelatedField (see serializers.py) scopes `parent` to every
# location the requester can act on - own personal ones plus any group's - with no
# further check that it matches the new location's own owner_group. Same behavior
# InventoryItem.storage_location already had before group ownership existed here; not
# something this feature narrows.
personal = StorageLocation.create_for_owner(name='mine', owner=self.f['local_user1'])
reply = client.post('/api/storage_locations/', self.f['local_user1'], {
'name': 'bin', 'owner_group': str(self.f['group1'])[1:], 'parent': personal.id,
})
self.assertEqual(reply.status_code, 201)
def test_group_location_not_valid_parent_for_non_member(self):
parent_id = self.create_group_location('shelf').json()['id']
reply = client.post('/api/storage_locations/', self.f['ext_user1'], {
'name': 'bin', 'parent': parent_id,
})
self.assertEqual(reply.status_code, 400)