from authentication.models import Group 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, CategoryTestMixin, TagTestMixin, PropertyTestMixin, LocationTestMixin client = SignatureAuthClient() class InventoryApiTestCase(UserTestMixin, InventoryTestMixin, ToolshedTestCase): def setUp(self): super().setUp() self.prepare_users() self.prepare_categories() self.prepare_tags() self.prepare_properties() self.prepare_inventory() self.own_handle = str(self.f['local_user1']) def test_get_inventory(self): reply = client.get('/api/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_post_new_item(self): reply = client.post('/api/inventory_items/{}/'.format(self.own_handle), self.f['local_user1'], { 'availability_policy': 'rent', 'category': 'cat2', 'name': 'test3', 'description': 'test', 'owned_quantity': 1, 'image': '', 'tags': ['tag1', 'tag2'], 'properties': [{'name': 'prop1', 'value': 'value3'}, {'name': 'prop2', 'value': 'value4'}] }) self.assertEqual(reply.status_code, 201) self.assertEqual(InventoryItem.objects.count(), 3) item = InventoryItem.objects.get(name='test3') self.assertEqual(item.availability_policy, 'rent') self.assertEqual(item.category, Category.objects.get(name='cat2')) self.assertEqual(item.name, 'test3') self.assertEqual(item.description, 'test') self.assertEqual(item.owned_quantity, 1) self.assertEqual([t for t in item.tags.all()], [self.f['tag1'], self.f['tag2']]) self.assertEqual([p for p in item.properties.all()], [self.f['prop1'], self.f['prop2']]) self.assertEqual([p.value for p in item.itemproperty_set.all()], ['value3', 'value4']) def test_post_new_item2(self): reply = client.post('/api/inventory_items/{}/'.format(self.own_handle), self.f['local_user1'], { 'availability_policy': 'share', 'name': 'test3', 'description': 'test', 'owned_quantity': 1, 'image': '', }) self.assertEqual(reply.status_code, 201) self.assertEqual(InventoryItem.objects.count(), 3) item = InventoryItem.objects.get(name='test3') self.assertEqual(item.availability_policy, 'share') self.assertEqual(item.category, None) self.assertEqual(item.name, 'test3') self.assertEqual(item.description, 'test') self.assertEqual(item.owned_quantity, 1) self.assertEqual([t for t in item.tags.all()], []) self.assertEqual([p for p in item.properties.all()], []) def test_post_new_item_empty(self): reply = client.post('/api/inventory_items/{}/'.format(self.own_handle), self.f['local_user1'], { 'availability_policy': 'rent', 'owned_quantity': 1, 'image': '', }) self.assertEqual(reply.status_code, 400) self.assertEqual(InventoryItem.objects.count(), 2) def test_post_new_item3(self): reply = client.post('/api/inventory_items/{}/'.format(self.own_handle), self.f['local_user1'], { 'availability_policy': 'private', 'name': 'test3', 'description': 'test', 'owned_quantity': 1, 'image': '', 'category': None, }) self.assertEqual(reply.status_code, 201) self.assertEqual(InventoryItem.objects.count(), 3) item = InventoryItem.objects.get(name='test3') self.assertEqual(item.availability_policy, 'private') self.assertEqual(item.category, None) self.assertEqual(item.name, 'test3') self.assertEqual(item.description, 'test') self.assertEqual(item.owned_quantity, 1) self.assertEqual([t for t in item.tags.all()], []) self.assertEqual([p for p in item.properties.all()], []) def test_put_item(self): reply = client.put('/api/inventory_items/{}/1/'.format(self.own_handle), self.f['local_user1'], { 'availability_policy': 'sell', 'name': 'test4', 'description': 'new description', 'owned_quantity': 100, 'image': '', 'tags': ['tag1', 'tag2', 'tag3'], 'properties': [{'name': 'prop1', 'value': 'value5'}, {'name': 'prop2', 'value': 'value6'}, {'name': 'prop3', 'value': 'value7'}] }) self.assertEqual(reply.status_code, 200) self.assertEqual(InventoryItem.objects.count(), 2) item = InventoryItem.objects.get(id=1) self.assertEqual(item.availability_policy, 'sell') self.assertEqual(item.category, None) self.assertEqual(item.name, 'test4') self.assertEqual(item.description, 'new description') self.assertEqual(item.owned_quantity, 100) self.assertEqual([t for t in item.tags.all()], [self.f['tag1'], self.f['tag2'], self.f['tag3']]) self.assertEqual([p for p in item.properties.all()], [self.f['prop1'], self.f['prop2'], self.f['prop3']]) self.assertEqual([p.value for p in item.itemproperty_set.all()], ['value5', 'value6', 'value7']) def test_patch_item(self): reply = client.patch('/api/inventory_items/{}/1/'.format(self.own_handle), self.f['local_user1'], { 'description': 'new description2', 'category': 'cat1', 'owned_quantity': 100, 'tags': ['tag3'], 'properties': [{'name': 'prop3', 'value': 'value8'}] }) self.assertEqual(reply.status_code, 200) self.assertEqual(InventoryItem.objects.count(), 2) item = InventoryItem.objects.get(id=1) self.assertEqual(item.availability_policy, 'friends') self.assertEqual(item.category, Category.objects.get(name='cat1')) self.assertEqual(item.name, 'test1') self.assertEqual(item.description, 'new description2') self.assertEqual(item.owned_quantity, 100) self.assertEqual([t for t in item.tags.all()], [self.f['tag3']]) self.assertEqual([p for p in item.properties.all()], [self.f['prop3']]) self.assertEqual([p.value for p in item.itemproperty_set.all()], ['value8']) def test_patch_item2(self): reply = client.patch('/api/inventory_items/{}/1/'.format(self.own_handle), self.f['local_user1'], { 'description': 'new description2', 'category': None, 'owned_quantity': 100, 'tags': [], 'properties': [] }) self.assertEqual(reply.status_code, 200) self.assertEqual(InventoryItem.objects.count(), 2) item = InventoryItem.objects.get(id=1) self.assertEqual(item.availability_policy, 'friends') self.assertEqual(item.category, None) self.assertEqual(item.name, 'test1') self.assertEqual(item.description, 'new description2') self.assertEqual(item.owned_quantity, 100) self.assertEqual([t for t in item.tags.all()], []) self.assertEqual([p for p in item.properties.all()], []) def test_delete_item(self): reply = client.delete('/api/inventory_items/{}/1/'.format(self.own_handle), self.f['local_user1']) self.assertEqual(reply.status_code, 204) self.assertEqual(InventoryItem.objects.count(), 1) self.assertEqual(InventoryItem.objects.get(id=2).name, 'test2') self.assertEqual(InventoryItem.objects.filter(name='test1').count(), 0) def test_search_items(self): reply = client.get('/api/search/?query=test', 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_search_items2(self): reply = client.get('/api/search/?query=test', self.f['local_user2']) self.assertEqual(reply.status_code, 200) self.assertEqual(len(reply.json()), 2) self.assertEqual(reply.json()[0]['name'], 'test1') self.assertEqual(reply.json()[1]['name'], 'test2') def test_search_items_fail(self): reply = client.get('/api/search/', self.f['local_user1']) self.assertEqual(reply.status_code, 400) self.assertEqual(reply.json()['error'], 'No query provided.') def test_search_items_fail2(self): reply = client.get('/api/search/?query=test', self.f['ext_user1']) self.assertEqual(reply.status_code, 200) self.assertEqual(len(reply.json()), 0) def test_get_shared_item_as_friend(self): reply = client.get('/api/inventory_items/testuser1@example.com/' + str(self.f['item1'].id) + '/', self.f['local_user2']) self.assertEqual(reply.status_code, 200) self.assertEqual(reply.json()['name'], 'test1') self.assertEqual(reply.json()['owner'], 'testuser1@example.com') def test_get_shared_item_as_owner(self): reply = client.get('/api/inventory_items/testuser1@example.com/' + str(self.f['item1'].id) + '/', self.f['local_user1']) self.assertEqual(reply.status_code, 200) self.assertEqual(reply.json()['name'], 'test1') def test_get_shared_item_not_friend(self): # Non-visible resolves the same way a non-member's group access does now (see # GroupOwnedInventoryApiTestCase.test_non_member_cannot_see_or_edit) - 404, not a # separate 403 special case for "not a friend". reply = client.get('/api/inventory_items/testuser1@example.com/' + str(self.f['item1'].id) + '/', self.f['ext_user1']) self.assertEqual(reply.status_code, 404) def test_get_shared_item_private(self): private_item = InventoryItem.create_for_owner( owner=self.f['local_user1'], owned_quantity=1, name='secret', availability_policy='private') reply = client.get('/api/inventory_items/testuser1@example.com/' + str(private_item.id) + '/', self.f['local_user2']) self.assertEqual(reply.status_code, 404) reply = client.get('/api/inventory_items/testuser1@example.com/' + str(private_item.id) + '/', self.f['local_user1']) self.assertEqual(reply.status_code, 200) def test_get_shared_item_unknown_handle(self): reply = client.get('/api/inventory_items/nobody@example.com/' + str(self.f['item1'].id) + '/', self.f['local_user2']) self.assertEqual(reply.status_code, 404) def test_get_shared_item_unknown_id(self): reply = client.get('/api/inventory_items/testuser1@example.com/99999/', self.f['local_user2']) self.assertEqual(reply.status_code, 404) def test_get_shared_item_bad_handle(self): reply = client.get('/api/inventory_items/testuser1/' + str(self.f['item1'].id) + '/', self.f['local_user2']) self.assertEqual(reply.status_code, 400) class ResolveShortIdApiTestCase(UserTestMixin, InventoryTestMixin, GroupTestMixin, LocationTestMixin, ToolshedTestCase): def setUp(self): super().setUp() self.prepare_users() self.prepare_categories() self.prepare_tags() self.prepare_properties() self.prepare_inventory() self.prepare_groups() self.prepare_locations() self.owner_id = self.f['local_user1'].public_identity.pk def test_resolve_item_as_owner(self): reply = client.get(f'/api/resolve_short_id/item/{self.owner_id}/{self.f["item1"].id}/', self.f['local_user1']) self.assertEqual(reply.status_code, 200) self.assertEqual(reply.json(), {'handle': 'testuser1@example.com', 'id': self.f['item1'].id}) def test_resolve_item_as_friend(self): reply = client.get(f'/api/resolve_short_id/item/{self.owner_id}/{self.f["item1"].id}/', self.f['local_user2']) self.assertEqual(reply.status_code, 200) def test_resolve_item_not_friend(self): reply = client.get(f'/api/resolve_short_id/item/{self.owner_id}/{self.f["item1"].id}/', self.f['ext_user1']) self.assertEqual(reply.status_code, 403) def test_resolve_item_private_not_owner(self): private_item = InventoryItem.create_for_owner( owner=self.f['local_user1'], owned_quantity=1, name='secret', availability_policy='private') reply = client.get(f'/api/resolve_short_id/item/{self.owner_id}/{private_item.id}/', self.f['local_user2']) self.assertEqual(reply.status_code, 403) reply = client.get(f'/api/resolve_short_id/item/{self.owner_id}/{private_item.id}/', self.f['local_user1']) self.assertEqual(reply.status_code, 200) def test_resolve_item_unknown_owner(self): reply = client.get('/api/resolve_short_id/item/999999/1/', self.f['local_user2']) self.assertEqual(reply.status_code, 404) def test_resolve_item_unknown_local_id(self): reply = client.get(f'/api/resolve_short_id/item/{self.owner_id}/999999/', self.f['local_user2']) self.assertEqual(reply.status_code, 404) def test_resolve_unsupported_kind(self): # workflow/category/file/group aren't wired up yet - see # docs/handles-and-shortids.md#domain-qualified-short-id's note on the remaining gap. reply = client.get(f'/api/resolve_short_id/workflow/{self.owner_id}/1/', self.f['local_user2']) self.assertEqual(reply.status_code, 400) def test_resolve_storage_location_as_owner(self): reply = client.get(f'/api/resolve_short_id/storage_location/{self.owner_id}/{self.f["loc1"].id}/', self.f['local_user1']) self.assertEqual(reply.status_code, 200) self.assertEqual(reply.json(), {'handle': 'testuser1@example.com', 'id': self.f['loc1'].id}) def test_resolve_storage_location_unknown_local_id(self): reply = client.get(f'/api/resolve_short_id/storage_location/{self.owner_id}/999999/', self.f['local_user1']) self.assertEqual(reply.status_code, 404) def test_resolve_group_item_as_member(self): item = InventoryItem.create_for_owner( owner_group=self.f['group1'], owned_quantity=1, name='drill', availability_policy='private') reply = client.get(f'/api/resolve_short_id/group_item/{self.f["group1"].pk}/{item.id}/', self.f['local_user1']) self.assertEqual(reply.status_code, 200) self.assertEqual(reply.json(), {'handle': str(self.f['group1']), 'id': item.id}) def test_resolve_group_item_non_member_denied(self): item = InventoryItem.create_for_owner( owner_group=self.f['group1'], owned_quantity=1, name='drill', availability_policy='private') reply = client.get(f'/api/resolve_short_id/group_item/{self.f["group1"].pk}/{item.id}/', self.f['local_user2']) self.assertEqual(reply.status_code, 403) def test_resolve_group_storage_location_as_member(self): location = StorageLocation.create_for_owner(owner_group=self.f['group1'], name='shelf') reply = client.get( f'/api/resolve_short_id/group_storage_location/{self.f["group1"].pk}/{location.id}/', self.f['local_user1']) self.assertEqual(reply.status_code, 200) self.assertEqual(reply.json(), {'handle': str(self.f['group1']), 'id': location.id}) def test_resolve_unknown_group(self): reply = client.get('/api/resolve_short_id/group_item/999999/1/', self.f['local_user1']) self.assertEqual(reply.status_code, 404) class TestInventoryItemWithFileApiTestCase(UserTestMixin, FilesTestMixin, InventoryTestMixin, ToolshedTestCase): def setUp(self): super().setUp() self.prepare_users() self.prepare_categories() self.prepare_tags() self.prepare_properties() self.prepare_files() self.prepare_inventory() self.own_handle = str(self.f['local_user1']) def test_post_item_with_file_id(self): reply = client.post('/api/inventory_items/{}/'.format(self.own_handle), self.f['local_user1'], { 'name': 'test4', 'description': 'test', 'category': 'cat1', 'owned_quantity': 1, 'tags': ['tag1', 'tag2'], 'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}], 'files': [self.f['test_file1'].id] }) self.assertEqual(reply.status_code, 201) self.assertEqual(InventoryItem.objects.count(), 3) item = InventoryItem.objects.get(id=3) self.assertEqual(item.availability_policy, 'private') self.assertEqual(item.category, Category.objects.get(name='cat1')) self.assertEqual(item.name, 'test4') self.assertEqual(item.description, 'test') self.assertEqual(item.owned_quantity, 1) self.assertEqual([t for t in item.tags.all()], [self.f['tag1'], self.f['tag2']]) self.assertEqual([p for p in item.properties.all()], [self.f['prop1'], self.f['prop2']]) self.assertEqual([p.value for p in item.itemproperty_set.all()], ['value1', 'value2']) self.assertEqual([f for f in item.files.all()], [self.f['test_file1']]) def test_post_item_with_encoded_file(self): reply = client.post('/api/inventory_items/{}/'.format(self.own_handle), self.f['local_user1'], { 'name': 'test4', 'description': 'test', 'category': 'cat1', 'owned_quantity': 1, 'tags': ['tag1', 'tag2'], 'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}], 'files': [{'data': self.f['encoded_content3'], 'mime_type': 'text/plain'}] }) self.assertEqual(reply.status_code, 201) self.assertEqual(InventoryItem.objects.count(), 3) item = InventoryItem.objects.get(id=3) self.assertEqual(item.availability_policy, 'private') self.assertEqual(item.category, Category.objects.get(name='cat1')) self.assertEqual(item.name, 'test4') self.assertEqual(item.description, 'test') self.assertEqual(item.owned_quantity, 1) self.assertEqual([t for t in item.tags.all()], [self.f['tag1'], self.f['tag2']]) self.assertEqual([p for p in item.properties.all()], [self.f['prop1'], self.f['prop2']]) self.assertEqual([p.value for p in item.itemproperty_set.all()], ['value1', 'value2']) self.assertEqual([f for f in item.files.all()], [self.f['test_file3']]) def test_post_item_with_file_id_fail(self): reply = client.post('/api/inventory_items/{}/'.format(self.own_handle), self.f['local_user1'], { 'name': 'test4', 'description': 'test', 'category': 'cat1', 'owned_quantity': 1, 'tags': ['tag1', 'tag2'], 'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}], 'files': [99999] }) self.assertEqual(reply.status_code, 400) def test_post_item_with_encoded_file_fail(self): reply = client.post('/api/inventory_items/{}/'.format(self.own_handle), self.f['local_user1'], { 'name': 'test4', 'description': 'test', 'category': 'cat1', 'owned_quantity': 1, 'tags': ['tag1', 'tag2'], 'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}], 'files': [{'data': self.f['encoded_content3']}] }) self.assertEqual(reply.status_code, 400) class GroupOwnedInventoryApiTestCase(UserTestMixin, GroupTestMixin, CategoryTestMixin, TagTestMixin, PropertyTestMixin, FilesTestMixin, LocationTestMixin, ToolshedTestCase): def setUp(self): super().setUp() self.prepare_users() self.prepare_groups() self.prepare_categories() self.prepare_tags() self.prepare_properties() self.prepare_files() self.prepare_locations() self.f['group1'].members.add(self.f['local_user2'].public_identity) self.group_handle = '+' + str(self.f['group1'])[1:] def create_group_item(self, name='drill'): return client.post('/api/inventory_items/{}/'.format(self.group_handle), self.f['local_user1'], { 'name': name, 'owned_quantity': 1, 'availability_policy': 'private', }) 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/{}/'.format(self.group_handle), self.f['ext_user1'], { 'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private', }) 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(self.group_handle, 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(self.group_handle, item_id), self.f['local_user2']) self.assertEqual(reply.status_code, 204) self.assertEqual(InventoryItem.objects.filter(id=item_id).count(), 0) def test_delete_group_item_when_id_collides_with_own_personal_item(self): own_handle = str(self.f['local_user2']) personal_reply = client.post('/api/inventory_items/{}/'.format(own_handle), self.f['local_user2'], { 'name': 'personal-drill', 'owned_quantity': 1, 'availability_policy': 'private', }) self.assertEqual(personal_reply.json()['id'], 1) item_id = self.create_group_item().json()['id'] self.assertEqual(item_id, 1) reply = client.delete('/api/inventory_items/{}/{}/'.format(self.group_handle, item_id), self.f['local_user2']) self.assertEqual(reply.status_code, 204) self.assertEqual(InventoryItem.objects.filter(name='drill').count(), 0) self.assertEqual(InventoryItem.objects.filter(name='personal-drill').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 # items - not 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(self.group_handle, item_id), self.f['ext_user1']) self.assertEqual(reply.status_code, 200) reply = client.patch('/api/inventory_items/{}/{}/'.format(self.group_handle, 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(self.group_handle, 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/{}/'.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_items_listed_by_owner_handle(self): self.create_group_item() reply = client.get('/api/inventory_items/{}/'.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'], 'drill') def test_group_items_not_listed_for_non_member(self): self.create_group_item() reply = client.get('/api/inventory_items/{}/'.format(self.group_handle), self.f['ext_user1']) self.assertEqual(reply.status_code, 200) self.assertEqual(len(reply.json()), 0) def test_create_group_owned_item_with_full_fields(self): # Parity with InventoryApiTestCase.test_post_new_item: tags/properties/category attach # to a group-owned item the same way as a personal one. reply = client.post('/api/inventory_items/{}/'.format(self.group_handle), self.f['local_user1'], { 'availability_policy': 'rent', 'category': 'cat2', 'name': 'drill', 'description': 'test', 'owned_quantity': 3, 'tags': ['tag1', 'tag2'], 'properties': [{'name': 'prop1', 'value': 'value1'}, {'name': 'prop2', 'value': 'value2'}], }) 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(item.availability_policy, 'rent') self.assertEqual(item.category, Category.objects.get(name='cat2')) self.assertEqual(item.owned_quantity, 3) self.assertEqual([t for t in item.tags.all()], [self.f['tag1'], self.f['tag2']]) self.assertEqual([p for p in item.properties.all()], [self.f['prop1'], self.f['prop2']]) self.assertEqual([p.value for p in item.itemproperty_set.all()], ['value1', 'value2']) 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/{}/'.format(self.group_handle), self.f['local_user1'], { 'availability_policy': 'private', 'owned_quantity': 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/+nonexistent@example.com/', self.f['local_user1'], { 'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private', }) self.assertEqual(reply.status_code, 404) self.assertEqual(InventoryItem.objects.count(), 0) def test_group_items_listed_for_nonexistent_group(self): reply = client.get('/api/inventory_items/+nonexistent@example.com/', self.f['local_user1']) self.assertEqual(reply.status_code, 200) self.assertEqual(len(reply.json()), 0) def test_put_group_item(self): # Parity with InventoryApiTestCase.test_put_item, but as a PUT by a different member than # the creator, to exercise the _is_authorized branch in perform_update for PUT too. item_id = self.create_group_item().json()['id'] reply = client.put('/api/inventory_items/{}/{}/'.format(self.group_handle, item_id), self.f['local_user2'], { 'availability_policy': 'sell', 'name': 'drill-4000', 'description': 'new description', 'owned_quantity': 100, 'tags': ['tag1', 'tag3'], 'properties': [{'name': 'prop1', 'value': 'value5'}], }) self.assertEqual(reply.status_code, 200) item = InventoryItem.objects.get(id=item_id) self.assertEqual(item.owner_group, self.f['group1']) self.assertEqual(item.availability_policy, 'sell') self.assertEqual(item.name, 'drill-4000') self.assertEqual(item.description, 'new description') self.assertEqual(item.owned_quantity, 100) self.assertEqual([t for t in item.tags.all()], [self.f['tag1'], self.f['tag3']]) self.assertEqual([p.value for p in item.itemproperty_set.all()], ['value5']) def test_patch_group_item_clears_fields(self): # Parity with InventoryApiTestCase.test_patch_item2 - clearing category/tags/properties. reply = client.post('/api/inventory_items/{}/'.format(self.group_handle), self.f['local_user1'], { 'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private', 'category': 'cat1', 'tags': ['tag1'], }) item_id = reply.json()['id'] reply = client.patch('/api/inventory_items/{}/{}/'.format(self.group_handle, item_id), self.f['local_user2'], { 'category': None, 'tags': [], 'properties': [] }) self.assertEqual(reply.status_code, 200) item = InventoryItem.objects.get(id=item_id) self.assertEqual(item.category, None) self.assertEqual([t for t in item.tags.all()], []) def test_group_item_storage_location(self): reply = client.post('/api/inventory_items/{}/'.format(self.group_handle), self.f['local_user1'], { 'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private', 'storage_location': self.f['loc1'].id, }) self.assertEqual(reply.status_code, 201) item = InventoryItem.objects.get(name='drill') self.assertEqual(item.storage_location, self.f['loc1']) def test_post_group_item_with_file_id(self): # Parity with TestInventoryItemWithFileApiTestCase.test_post_item_with_file_id. reply = client.post('/api/inventory_items/{}/'.format(self.group_handle), self.f['local_user1'], { 'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private', 'files': [self.f['test_file1'].id], }) self.assertEqual(reply.status_code, 201) item = InventoryItem.objects.get(name='drill') self.assertEqual([f for f in item.files.all()], [self.f['test_file1']]) def test_post_group_item_with_encoded_file(self): reply = client.post('/api/inventory_items/{}/'.format(self.group_handle), self.f['local_user1'], { 'name': 'drill', 'owned_quantity': 1, 'availability_policy': 'private', 'files': [{'data': self.f['encoded_content3'], 'mime_type': 'text/plain'}], }) self.assertEqual(reply.status_code, 201) item = InventoryItem.objects.get(name='drill') self.assertEqual([f for f in item.files.all()], [self.f['test_file3']]) def test_group_items_excluded_from_search(self): # Group-owned items are reachable only via the group's own detail page for MVP (see # docs/design-in-progress/groups-mvp.md) - search must not surface them either. self.create_group_item(name='searchable-drill') InventoryItem.create_for_owner(owner=self.f['local_user1'], owned_quantity=1, name='searchable-personal') reply = client.get('/api/search/?query=searchable', self.f['local_user1']) self.assertEqual(reply.status_code, 200) names = [item['name'] for item in reply.json()] self.assertEqual(names, ['searchable-personal']) class InventoryItemIdAllocationTestCase(UserTestMixin, ToolshedTestCase): """InventoryItem.id is sequential, gapless, and never reused within each owner/owner_group's own items, allocated independently per scope (see OwnerItemSequence).""" def setUp(self): super().setUp() self.prepare_users() def test_ids_are_sequential_and_independent_per_owner(self): user1_items = [InventoryItem.create_for_owner(owner=self.f['local_user1'], name=f'u1-{i}') for i in range(3)] user2_items = [InventoryItem.create_for_owner(owner=self.f['local_user2'], name=f'u2-{i}') for i in range(2)] self.assertEqual([item.id for item in user1_items], [1, 2, 3]) self.assertEqual([item.id for item in user2_items], [1, 2]) def test_deleted_item_id_is_never_reused(self): item1 = InventoryItem.create_for_owner(owner=self.f['local_user1'], name='first') item2 = InventoryItem.create_for_owner(owner=self.f['local_user1'], name='second') self.assertEqual((item1.id, item2.id), (1, 2)) item2.delete() # soft delete - item2's row (and its id) stays in the table item3 = InventoryItem.create_for_owner(owner=self.f['local_user1'], name='third') self.assertEqual(item3.id, 3) self.assertFalse(InventoryItem.objects.filter(owner=self.f['local_user1'], id=2).exists()) self.assertTrue(InventoryItem.global_objects.filter(owner=self.f['local_user1'], id=2).exists()) def test_group_scope_has_independent_sequence(self): group = Group.objects.create(name='alloc-test-group', domain=self.f['example_com'].name) personal_item = InventoryItem.create_for_owner(owner=self.f['local_user1'], name='personal') group_item = InventoryItem.create_for_owner(owner_group=group, name='group-owned') self.assertEqual(personal_item.id, 1) self.assertEqual(group_item.id, 1)