From deeed4b65bd758b9a93d8b65a910e2d9845fad42 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Thu, 9 Jun 2022 22:02:58 +0200 Subject: [PATCH] testes and fixes for space, userspace and invitelink apis --- .idea/dictionaries/vabene1111_PC.xml | 1 + cookbook/models.py | 3 + cookbook/serializer.py | 2 +- cookbook/tests/api/test_api_invitelinke.py | 119 ++++++++++++++++++ cookbook/tests/api/test_api_space.py | 102 +++++++++++++++ cookbook/tests/api/test_api_userspace.py | 113 +++++++++++++++++ .../tests/other/test_permission_helper.py | 30 ++++- cookbook/views/api.py | 20 +-- 8 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 cookbook/tests/api/test_api_invitelinke.py create mode 100644 cookbook/tests/api/test_api_space.py create mode 100644 cookbook/tests/api/test_api_userspace.py diff --git a/.idea/dictionaries/vabene1111_PC.xml b/.idea/dictionaries/vabene1111_PC.xml index bb97e302a..2bd9a7252 100644 --- a/.idea/dictionaries/vabene1111_PC.xml +++ b/.idea/dictionaries/vabene1111_PC.xml @@ -6,6 +6,7 @@ csrftoken gunicorn ical + invitelink mealie pepperplate safron diff --git a/cookbook/models.py b/cookbook/models.py index 36fe94fd1..22bdf90fd 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -300,6 +300,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model): def get_owner(self): return self.created_by + def get_space(self): + return self + def __str__(self): return self.name diff --git a/cookbook/serializer.py b/cookbook/serializer.py index ed4703588..118fdd941 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -200,7 +200,7 @@ class UserSpaceSerializer(WritableNestedModelSerializer): groups = GroupSerializer(many=True) def validate(self, data): - if self.instance.user == self.context['request'].space.created_by: # cant change space owner permission + if self.instance.user == self.context['request'].space.created_by: # can't change space owner permission raise serializers.ValidationError(_('Cannot modify Space owner permission.')) return super().validate(data) diff --git a/cookbook/tests/api/test_api_invitelinke.py b/cookbook/tests/api/test_api_invitelinke.py new file mode 100644 index 000000000..0c228e849 --- /dev/null +++ b/cookbook/tests/api/test_api_invitelinke.py @@ -0,0 +1,119 @@ +import json + +import pytest +from django.contrib import auth +from django.db.models import OuterRef, Subquery +from django.urls import reverse +from django_scopes import scopes_disabled + +from cookbook.models import Ingredient, Step, InviteLink + +LIST_URL = 'api:invitelink-list' +DETAIL_URL = 'api:invitelink-detail' + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403, 0], + ['g1_s1', 403, 0], + ['u1_s1', 403, 0], + ['a1_s1', 200, 1], + ['a2_s1', 200, 0], +]) +def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + InviteLink.objects.create(group_id=1, created_by=auth.get_user(a1_s1), space=space_1) + + c = request.getfixturevalue(arg[0]) + result = c.get(reverse(LIST_URL)) + assert result.status_code == arg[1] + if arg[1] == 200: + assert len(json.loads(result.content)) == arg[2] + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 403], + ['a1_s1', 200], + ['g1_s2', 403], + ['u1_s2', 403], + ['a1_s2', 404], +]) +def test_update(arg, request, space_1, u1_s1, a1_s1): + with scopes_disabled(): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + + il = InviteLink.objects.create(group_id=1, created_by=auth.get_user(a1_s1), space=space_1) + + c = request.getfixturevalue(arg[0]) + r = c.patch( + reverse( + DETAIL_URL, + args={il.id} + ), + {'email': 'test@mail.de'}, + content_type='application/json' + ) + response = json.loads(r.content) + print(response) + assert r.status_code == arg[1] + if r.status_code == 200: + assert response['email'] == 'test@mail.de' + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 403], + ['a1_s1', 201], + ['a2_s1', 403], +]) +def test_add(arg, request, a1_s1, space_1): + with scopes_disabled(): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + c = request.getfixturevalue(arg[0]) + r = c.post( + reverse(LIST_URL), + {'group': {'id': 3, 'name': 'admin'}}, + content_type='application/json' + ) + print(r.content) + assert r.status_code == arg[1] + + +def test_delete(u1_s1, u1_s2, a1_s1, a2_s1, space_1): + with scopes_disabled(): + il = InviteLink.objects.create(group_id=1, created_by=auth.get_user(a1_s1), space=space_1) + + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + + # user cant delete + r = u1_s1.delete( + reverse( + DETAIL_URL, + args={il.id} + ) + ) + assert r.status_code == 403 + + # admin cant delete + r = a2_s1.delete( + reverse( + DETAIL_URL, + args={il.id} + ) + ) + assert r.status_code == 404 + + # owner can delete + r = a1_s1.delete( + reverse( + DETAIL_URL, + args={il.id} + ) + ) + assert r.status_code == 204 diff --git a/cookbook/tests/api/test_api_space.py b/cookbook/tests/api/test_api_space.py new file mode 100644 index 000000000..bd9e326d2 --- /dev/null +++ b/cookbook/tests/api/test_api_space.py @@ -0,0 +1,102 @@ +import json + +import pytest +from django.contrib import auth +from django.db.models import OuterRef, Subquery +from django.urls import reverse +from django_scopes import scopes_disabled + +from cookbook.models import Ingredient, Step + +LIST_URL = 'api:space-list' +DETAIL_URL = 'api:space-detail' + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403, 0], + ['g1_s1', 403, 0], + ['u1_s1', 403, 0], + ['a1_s1', 200, 1], + ['a2_s1', 200, 0], +]) +def test_list_permission(arg, request, space_1, a1_s1): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + c = request.getfixturevalue(arg[0]) + result = c.get(reverse(LIST_URL)) + assert result.status_code == arg[1] + if arg[1] == 200: + assert len(json.loads(result.content)) == arg[2] + + +def test_list_permission_owner(u1_s1, space_1): + assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0 + space_1.created_by = auth.get_user(u1_s1) + space_1.save() + assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1 + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 404], + ['u1_s1', 404], + ['a1_s1', 200], + ['g1_s2', 404], + ['u1_s2', 404], + ['a1_s2', 404], +]) +def test_update(arg, request, space_1, a1_s1): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + with scopes_disabled(): + c = request.getfixturevalue(arg[0]) + r = c.patch( + reverse( + DETAIL_URL, + args={space_1.id} + ), + {'message': 'new'}, + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == arg[1] + if r.status_code == 200: + assert response['message'] == 'new' + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 405], + ['u1_s1', 405], + ['a1_s1', 405], +]) +def test_add(arg, request, u1_s2): + c = request.getfixturevalue(arg[0]) + r = c.post( + reverse(LIST_URL), + {'name': 'test'}, + content_type='application/json' + ) + assert r.status_code == arg[1] + + +def test_delete(u1_s1, u1_s2, a1_s1, space_1): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + # user cannot delete space + r = u1_s1.delete( + reverse( + DETAIL_URL, + args={space_1.id} + ) + ) + assert r.status_code == 405 + + # event the space owner cannot delete his space over the api (this might change later but for now it's only available in the UI) + r = a1_s1.delete( + reverse( + DETAIL_URL, + args={space_1.id} + ) + ) + assert r.status_code == 204 diff --git a/cookbook/tests/api/test_api_userspace.py b/cookbook/tests/api/test_api_userspace.py new file mode 100644 index 000000000..deccd7ed3 --- /dev/null +++ b/cookbook/tests/api/test_api_userspace.py @@ -0,0 +1,113 @@ +import json + +import pytest +from django.contrib import auth +from django.db.models import OuterRef, Subquery +from django.urls import reverse +from django_scopes import scopes_disabled + +from cookbook.models import Ingredient, Step + +LIST_URL = 'api:userspace-list' +DETAIL_URL = 'api:userspace-detail' + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403, 0], + ['g1_s1', 200, 1], # sees only own user space + ['u1_s1', 200, 1], + ['a1_s1', 200, 3], # sees user space of all users in space + ['a2_s1', 200, 1], +]) +def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + + c = request.getfixturevalue(arg[0]) + result = c.get(reverse(LIST_URL)) + assert result.status_code == arg[1] + if arg[1] == 200: + assert len(json.loads(result.content)) == arg[2] + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 403], + ['a1_s1', 200], + ['g1_s2', 403], + ['u1_s2', 403], + ['a1_s2', 403], +]) +def test_update(arg, request, space_1, u1_s1, a1_s1): + with scopes_disabled(): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + + user_space = auth.get_user(u1_s1).userspace_set.first() + + c = request.getfixturevalue(arg[0]) + r = c.patch( + reverse( + DETAIL_URL, + args={user_space.id} + ), + {'groups': [{'id': 3, 'name': 'admin'}]}, + content_type='application/json' + ) + response = json.loads(r.content) + assert r.status_code == arg[1] + if r.status_code == 200: + assert response['groups'] == [{'id': 3, 'name': 'admin'}] + + +def test_update_space_owner(a1_s1, space_1): + # space owners cannot modify their own permission so that they can't lock themselves out + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + r = a1_s1.patch( + reverse( + DETAIL_URL, + args={auth.get_user(a1_s1).userspace_set.first().id} + ), + {'groups': [{'id': 2, 'name': 'user'}]}, + content_type='application/json' + ) + assert r.status_code == 400 + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 405], + ['u1_s1', 405], + ['a1_s1', 405], +]) +def test_add(arg, request, u1_s1, space_1): + c = request.getfixturevalue(arg[0]) + r = c.post( + reverse(LIST_URL), + {'user': {'id': auth.get_user(u1_s1).id, 'space': space_1.id}}, + content_type='application/json' + ) + assert r.status_code == arg[1] + + +def test_delete(u1_s1, u1_s2, a1_s1, space_1): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + + r = u1_s1.delete( + reverse( + DETAIL_URL, + args={auth.get_user(u1_s1).userspace_set.first().id} + ) + ) + assert r.status_code == 403 + + r = a1_s1.delete( + reverse( + DETAIL_URL, + args={auth.get_user(u1_s1).userspace_set.first().id} + ) + ) + assert r.status_code == 204 diff --git a/cookbook/tests/other/test_permission_helper.py b/cookbook/tests/other/test_permission_helper.py index c2217f3c6..be407daf3 100644 --- a/cookbook/tests/other/test_permission_helper.py +++ b/cookbook/tests/other/test_permission_helper.py @@ -6,8 +6,8 @@ from django_scopes import scopes_disabled from cookbook.forms import ImportExportBase from cookbook.helper.ingredient_parser import IngredientParser -from cookbook.helper.permission_helper import has_group_permission, is_space_owner, switch_user_active_space -from cookbook.models import ExportLog, UserSpace, Food +from cookbook.helper.permission_helper import has_group_permission, is_space_owner, switch_user_active_space, is_object_owner +from cookbook.models import ExportLog, UserSpace, Food, Space, Comment, RecipeBook, RecipeBookEntry def test_has_group_permission(u1_s1, a_u, space_2): @@ -38,6 +38,30 @@ def test_has_group_permission(u1_s1, a_u, space_2): assert not has_group_permission(auth.get_user(a_u), ('admin',)) +def test_is_owner(u1_s1, u2_s1, u1_s2, a_u, space_1, recipe_1_s1): + with scopes_disabled(): + s = Space.objects.create(name='Test', created_by=auth.get_user(u1_s1)) + + assert is_object_owner(auth.get_user(u1_s1), s) + assert not is_object_owner(auth.get_user(u2_s1), s) + assert not is_object_owner(auth.get_user(u1_s2), s) + assert not is_object_owner(auth.get_user(a_u), s) + + rb = RecipeBook.objects.create(name='Test', created_by=auth.get_user(u1_s1), space=space_1) + + assert is_object_owner(auth.get_user(u1_s1), rb) + assert not is_object_owner(auth.get_user(u2_s1), rb) + assert not is_object_owner(auth.get_user(u1_s2), rb) + assert not is_object_owner(auth.get_user(a_u), rb) + + rbe = RecipeBookEntry.objects.create(book=rb, recipe=recipe_1_s1) + + assert is_object_owner(auth.get_user(u1_s1), rbe) + assert not is_object_owner(auth.get_user(u2_s1), rbe) + assert not is_object_owner(auth.get_user(u1_s2), rbe) + assert not is_object_owner(auth.get_user(a_u), rbe) + + def test_is_space_owner(u1_s1, u2_s1, space_1, space_2): with scopes_disabled(): f = Food.objects.create(name='Test', space=space_1) @@ -70,5 +94,3 @@ def test_switch_user_active_space(u1_s1, u1_s2, space_1, space_2): # can switch into newly created space assert switch_user_active_space(auth.get_user(u1_s1), space_2) == us - - diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 9ea68a284..b03b448aa 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -45,7 +45,7 @@ from cookbook.helper.image_processing import handle_image from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner, CustomIsShare, CustomIsShared, CustomIsUser, - group_required, CustomIsSpaceOwner, switch_user_active_space) + group_required, CustomIsSpaceOwner, switch_user_active_space, is_space_owner) from cookbook.helper.recipe_html_import import get_recipe_from_source from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper @@ -376,11 +376,11 @@ class GroupViewSet(viewsets.ModelViewSet): class SpaceViewSet(viewsets.ModelViewSet): queryset = Space.objects serializer_class = SpaceSerializer - permission_classes = [CustomIsOwner] + permission_classes = [CustomIsOwner & CustomIsAdmin] http_method_names = ['get', 'patch'] def get_queryset(self): - return self.queryset.filter(id=self.request.space.id) + return self.queryset.filter(id=self.request.space.id, created_by=self.request.user) class UserSpaceViewSet(viewsets.ModelViewSet): @@ -395,7 +395,10 @@ class UserSpaceViewSet(viewsets.ModelViewSet): return super().destroy(request, *args, **kwargs) def get_queryset(self): - return self.queryset.filter(user=self.request.user) + if is_space_owner(self.request.user, self.request.space): + return self.queryset.filter(space=self.request.space) + else: + return self.queryset.filter(user=self.request.user, space=self.request.space) class UserPreferenceViewSet(viewsets.ModelViewSet): @@ -1055,11 +1058,14 @@ class AutomationViewSet(viewsets.ModelViewSet, StandardFilterMixin): class InviteLinkViewSet(viewsets.ModelViewSet, StandardFilterMixin): queryset = InviteLink.objects serializer_class = InviteLinkSerializer - permission_classes = [CustomIsSpaceOwner] + permission_classes = [CustomIsSpaceOwner & CustomIsAdmin] def get_queryset(self): - self.queryset = self.queryset.filter(space=self.request.space).all() - return super().get_queryset() + if is_space_owner(self.request.user, self.request.space): + self.queryset = self.queryset.filter(space=self.request.space).all() + return super().get_queryset() + else: + return None class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):