290 lines
15 KiB
Python
290 lines
15 KiB
Python
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
|
|
|
|
client = SignatureAuthClient()
|
|
|
|
|
|
class LocationApiTestCase(UserTestMixin, InventoryTestMixin, LocationTestMixin, ToolshedTestCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.prepare_users()
|
|
self.prepare_categories()
|
|
self.prepare_tags()
|
|
self.prepare_properties()
|
|
self.prepare_locations()
|
|
self.prepare_inventory()
|
|
self.own_handle = str(self.f['local_user1'])
|
|
|
|
def test_locations(self):
|
|
self.assertEqual("loc1", str(self.f['loc1']))
|
|
self.assertEqual("loc1", self.f['loc1'].name)
|
|
self.assertEqual("loc2", str(self.f['loc2']))
|
|
self.assertEqual("loc2", self.f['loc2'].name)
|
|
self.assertEqual("loc1/loc3", str(self.f['loc3']))
|
|
self.assertEqual("loc3", self.f['loc3'].name)
|
|
self.assertEqual(self.f['loc1'], self.f['loc3'].parent)
|
|
self.assertEqual("loc1/loc4", str(self.f['loc4']))
|
|
self.assertEqual("loc4", self.f['loc4'].name)
|
|
self.assertEqual(self.f['loc1'], self.f['loc4'].parent)
|
|
|
|
def test_get_inventory(self):
|
|
reply = client.get('/api/v1/inventory_items/{}/'.format(self.own_handle), self.f['local_user1'])
|
|
self.assertEqual(reply.status_code, 200)
|
|
self.assertEqual(len(reply.json()), 2)
|
|
self.assertEqual(reply.json()[0]['name'], 'test1')
|
|
self.assertEqual(reply.json()[0]['description'], 'test')
|
|
self.assertEqual(reply.json()[0]['owned_quantity'], 1)
|
|
self.assertEqual(reply.json()[0]['tags'], [])
|
|
self.assertEqual(reply.json()[0]['properties'], [])
|
|
self.assertEqual(reply.json()[0]['category'], 'cat1')
|
|
self.assertEqual(reply.json()[0]['availability_policy'], 'friends')
|
|
self.assertEqual(reply.json()[1]['name'], 'test2')
|
|
self.assertEqual(reply.json()[1]['description'], 'test2')
|
|
self.assertEqual(reply.json()[1]['owned_quantity'], 1)
|
|
self.assertEqual(reply.json()[1]['tags'], ['tag1', 'tag2'])
|
|
self.assertEqual(reply.json()[1]['properties'],
|
|
[{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}])
|
|
self.assertEqual(reply.json()[1]['category'], 'cat1')
|
|
self.assertEqual(reply.json()[1]['availability_policy'], 'friends')
|
|
|
|
def test_get_inventory_item(self):
|
|
reply = client.get('/api/v1/storage_locations/{}/'.format(self.own_handle), self.f['local_user1'])
|
|
self.assertEqual(reply.status_code, 200)
|
|
self.assertEqual(len(reply.json()), 4)
|
|
self.assertEqual(reply.json()[0]['name'], 'loc1')
|
|
self.assertEqual(reply.json()[0]['description'], None)
|
|
self.assertEqual(reply.json()[0]['category'], None)
|
|
self.assertEqual(reply.json()[0]['path'], 'loc1')
|
|
self.assertEqual(reply.json()[1]['name'], 'loc2')
|
|
self.assertEqual(reply.json()[1]['description'], None)
|
|
self.assertEqual(reply.json()[1]['category'], 'cat1')
|
|
self.assertEqual(reply.json()[1]['path'], 'loc2')
|
|
self.assertEqual(reply.json()[2]['name'], 'loc3')
|
|
self.assertEqual(reply.json()[2]['description'], None)
|
|
self.assertEqual(reply.json()[2]['category'], None)
|
|
self.assertEqual(reply.json()[2]['path'], 'loc1/loc3')
|
|
self.assertEqual(reply.json()[3]['name'], 'loc4')
|
|
self.assertEqual(reply.json()[3]['description'], None)
|
|
self.assertEqual(reply.json()[3]['category'], 'cat1')
|
|
self.assertEqual(reply.json()[3]['path'], 'loc1/loc4')
|
|
|
|
def test_post_new_location(self):
|
|
reply = client.post('/api/v1/storage_locations/{}/'.format(self.own_handle), self.f['local_user1'], {
|
|
'name': 'loc5',
|
|
'description': 'a new location',
|
|
})
|
|
self.assertEqual(reply.status_code, 201)
|
|
self.assertEqual(StorageLocation.objects.count(), 5)
|
|
location = StorageLocation.objects.get(name='loc5')
|
|
self.assertEqual(location.description, 'a new location')
|
|
self.assertEqual(location.owner, self.f['local_user1'])
|
|
self.assertEqual(location.parent, None)
|
|
self.assertEqual(reply.json()['path'], 'loc5')
|
|
|
|
def test_post_new_nested_location(self):
|
|
reply = client.post('/api/v1/storage_locations/{}/'.format(self.own_handle), self.f['local_user1'], {
|
|
'name': 'loc5',
|
|
'parent': self.f['loc3'].id,
|
|
})
|
|
self.assertEqual(reply.status_code, 201)
|
|
location = StorageLocation.objects.get(name='loc5')
|
|
self.assertEqual(location.parent, self.f['loc3'])
|
|
self.assertEqual(reply.json()['path'], 'loc1/loc3/loc5')
|
|
|
|
def test_patch_location(self):
|
|
reply = client.patch(
|
|
'/api/v1/storage_locations/{}/{}/'.format(self.own_handle, self.f['loc2'].id), self.f['local_user1'], {
|
|
'name': 'loc2-renamed',
|
|
'parent': self.f['loc1'].id,
|
|
})
|
|
self.assertEqual(reply.status_code, 200)
|
|
location = StorageLocation.objects.get(id=self.f['loc2'].id)
|
|
self.assertEqual(location.name, 'loc2-renamed')
|
|
self.assertEqual(location.parent, self.f['loc1'])
|
|
self.assertEqual(reply.json()['path'], 'loc1/loc2-renamed')
|
|
|
|
def test_delete_location(self):
|
|
reply = client.delete(
|
|
'/api/v1/storage_locations/{}/{}/'.format(self.own_handle, self.f['loc4'].id), self.f['local_user1'])
|
|
self.assertEqual(reply.status_code, 204)
|
|
self.assertEqual(StorageLocation.objects.count(), 3)
|
|
self.assertEqual(StorageLocation.objects.filter(id=self.f['loc4'].id).count(), 0)
|
|
|
|
def test_delete_location_with_items_sets_null(self):
|
|
item = InventoryItem.create_for_owner(
|
|
owner=self.f['local_user1'], name='located_item', storage_location=self.f['loc3'])
|
|
reply = client.delete(
|
|
'/api/v1/storage_locations/{}/{}/'.format(self.own_handle, self.f['loc3'].id), self.f['local_user1'])
|
|
self.assertEqual(reply.status_code, 204)
|
|
item.refresh_from_db()
|
|
self.assertIsNone(item.storage_location)
|
|
self.assertEqual(InventoryItem.objects.filter(id=item.id).count(), 1)
|
|
|
|
def test_locations_are_owner_scoped(self):
|
|
reply = client.get('/api/v1/storage_locations/{}/'.format(str(self.f['local_user2'])), self.f['local_user2'])
|
|
self.assertEqual(reply.status_code, 200)
|
|
self.assertEqual(len(reply.json()), 0)
|
|
|
|
def test_get_shared_location_visibility_private(self):
|
|
private_location = StorageLocation.create_for_owner(
|
|
owner=self.f['local_user1'], name='secret-loc', visibility_policy='private')
|
|
reply = client.get(
|
|
'/api/v1/storage_locations/{}/{}/'.format(self.own_handle, private_location.id), self.f['local_user2'])
|
|
self.assertEqual(reply.status_code, 404)
|
|
reply = client.get(
|
|
'/api/v1/storage_locations/{}/{}/'.format(self.own_handle, private_location.id), self.f['local_user1'])
|
|
self.assertEqual(reply.status_code, 200)
|
|
|
|
def test_list_locations_excludes_visibility_private_for_friend(self):
|
|
StorageLocation.create_for_owner(owner=self.f['local_user1'], name='secret-loc', visibility_policy='private')
|
|
reply = client.get('/api/v1/storage_locations/{}/'.format(self.own_handle), self.f['local_user2'])
|
|
self.assertEqual(reply.status_code, 200)
|
|
self.assertEqual(len(reply.json()), 4)
|
|
self.assertNotIn('secret-loc', [loc['name'] for loc in reply.json()])
|
|
|
|
def test_cannot_delete_other_users_location(self):
|
|
# local_user2 is a friend of local_user1 (see prepare_inventory), so local_user1's own
|
|
# handle resolves and the location is visible - friends can read but never write, so this
|
|
# is a 403 (found, not authorized), not a 404.
|
|
reply = client.delete(
|
|
'/api/v1/storage_locations/{}/{}/'.format(self.own_handle, self.f['loc1'].id), self.f['local_user2'])
|
|
self.assertEqual(reply.status_code, 403)
|
|
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)
|
|
# The '+' prefix is the '#'->'+' escape resolve_owner_handle expects for a group handle in
|
|
# the owner_handle URL path parameter - the same escape encodeHandleForUrl uses.
|
|
self.group_handle = '+' + str(self.f['group1'])[1:]
|
|
|
|
def create_group_location(self, name='shelf'):
|
|
return client.post('/api/v1/storage_locations/{}/'.format(self.group_handle), self.f['local_user1'], {
|
|
'name': name,
|
|
})
|
|
|
|
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/v1/storage_locations/{}/'.format(self.group_handle), self.f['ext_user1'], {
|
|
'name': 'shelf',
|
|
})
|
|
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/v1/storage_locations/{}/{}/'.format(self.group_handle, 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/v1/storage_locations/{}/{}/'.format(self.group_handle, location_id), self.f['local_user2'])
|
|
self.assertEqual(reply.status_code, 204)
|
|
self.assertEqual(StorageLocation.objects.filter(id=location_id).count(), 0)
|
|
|
|
def test_delete_group_location_when_id_collides_with_own_personal_location(self):
|
|
# Mirrors InventoryItemViewSet's own regression test - id is only unique within its own
|
|
# owner/owner_group scope, so a member's personal location and their group's location can
|
|
# land on the same id. Every route being scoped by an explicit owner_handle - never a
|
|
# bare, unscoped id - means this can no longer raise MultipleObjectsReturned.
|
|
own_handle = str(self.f['local_user2'])
|
|
personal_reply = client.post('/api/v1/storage_locations/{}/'.format(own_handle), self.f['local_user2'],
|
|
{'name': 'personal-shelf'})
|
|
self.assertEqual(personal_reply.json()['id'], 1)
|
|
location_id = self.create_group_location().json()['id']
|
|
self.assertEqual(location_id, 1)
|
|
|
|
reply = client.delete(
|
|
'/api/v1/storage_locations/{}/{}/'.format(self.group_handle, location_id), self.f['local_user2'])
|
|
|
|
self.assertEqual(reply.status_code, 204)
|
|
self.assertEqual(StorageLocation.objects.filter(name='shelf').count(), 0)
|
|
self.assertEqual(StorageLocation.objects.filter(name='personal-shelf').count(), 1)
|
|
|
|
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/v1/storage_locations/{}/{}/'.format(self.group_handle, location_id), self.f['ext_user1'])
|
|
self.assertEqual(reply.status_code, 200)
|
|
reply = client.patch(
|
|
'/api/v1/storage_locations/{}/{}/'.format(self.group_handle, 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/v1/storage_locations/{}/{}/'.format(self.group_handle, 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/v1/storage_locations/{}/'.format(str(self.f['local_user1'])), self.f['local_user1'])
|
|
self.assertEqual(reply.status_code, 200)
|
|
self.assertEqual(len(reply.json()), 0)
|
|
|
|
def test_group_locations_listed_by_owner_handle(self):
|
|
self.create_group_location()
|
|
reply = client.get('/api/v1/storage_locations/{}/'.format(self.group_handle), 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(self):
|
|
self.create_group_location()
|
|
reply = client.get('/api/v1/storage_locations/{}/'.format(self.group_handle), 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/v1/storage_locations/{}/'.format(self.group_handle), self.f['local_user2'], {
|
|
'name': 'bin', '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/v1/storage_locations/{}/'.format(self.group_handle), self.f['local_user1'], {
|
|
'name': 'bin', '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/v1/storage_locations/{}/'.format(str(self.f['ext_user1'])), self.f['ext_user1'], {
|
|
'name': 'bin', 'parent': parent_id,
|
|
})
|
|
self.assertEqual(reply.status_code, 400)
|