diff --git a/README.md b/README.md index 6f15b53..4a7908f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![PyPI - Version](https://img.shields.io/pypi/v/drf-rules.svg)](https://pypi.org/project/drf-rules) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/drf-rules.svg)](https://pypi.org/project/drf-rules) +[![Coverage Status](https://coveralls.io/repos/github/lsaavedr/drf-rules/badge.svg)](https://coveralls.io/github/lsaavedr/drf-rules) --- diff --git a/src/drf_rules/permissions.py b/src/drf_rules/permissions.py index 7e62a87..967a048 100644 --- a/src/drf_rules/permissions.py +++ b/src/drf_rules/permissions.py @@ -130,11 +130,15 @@ def has_object_permission( if not perm_exists(name=perm): logger.warning(error_message.format(perm)) - if method_name not in crud_method_names: - raise ImproperlyConfigured(error_message.format(perm)) + # already evaluated in has_permission + # if method_name not in crud_method_names: + # raise ImproperlyConfigured(error_message.format(perm)) + assert method_name in crud_method_names perm = self._permission(":default:", view) - if not perm_exists(name=perm): - raise ImproperlyConfigured(error_message.format(perm)) + # already evaluated in has_permission + # if not perm_exists(name=perm): + # raise ImproperlyConfigured(error_message.format(perm)) + assert perm_exists(name=perm) return user.has_perm(perm, obj) diff --git a/tests/testapp/migrations/0001_initial.py b/tests/testapp/migrations/0001_initial.py index d613603..72c40a0 100644 --- a/tests/testapp/migrations/0001_initial.py +++ b/tests/testapp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.2 on 2024-10-24 01:30 +# Generated by Django 5.1.2 on 2024-10-25 14:20 from typing import List, Tuple @@ -27,7 +27,37 @@ class Migration(migrations.Migration): ), ("name", models.CharField(max_length=64)), ("age", models.IntegerField()), - ("gender", models.CharField(max_length=32)), + ( + "gender", + models.CharField( + choices=[("MALE", "Male"), ("FEMALE", "Female")], + max_length=6, + ), + ), + ], + bases=(rules.contrib.models.RulesModelMixin, models.Model), + ), + migrations.CreateModel( + name="Dog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=64)), + ("age", models.IntegerField()), + ( + "gender", + models.CharField( + choices=[("MALE", "Male"), ("FEMALE", "Female")], + max_length=6, + ), + ), ], bases=(rules.contrib.models.RulesModelMixin, models.Model), ), diff --git a/tests/testapp/models.py b/tests/testapp/models.py index e3d5415..95bb251 100644 --- a/tests/testapp/models.py +++ b/tests/testapp/models.py @@ -3,21 +3,44 @@ import rules from django.db import models from rules.contrib.models import RulesModel +from .rules import is_adult_cat + + +class Gender(models.TextChoices): + MALE = "MALE", "Male" + FEMALE = "FEMALE", "Female" class Cat(RulesModel): name = models.CharField(max_length=64) age = models.IntegerField() - gender = models.CharField(max_length=32) + gender = models.CharField(max_length=6, choices=Gender.choices) class Meta: rules_permissions = { "create": rules.always_true, "retrieve": rules.always_true, "destroy": rules.is_staff, + "partial_update": rules.always_true, "custom_detail": rules.always_true, "custom_nodetail": rules.always_true, - ":default:": rules.is_staff, + ":default:": is_adult_cat, + } + + def __str__(self) -> str: + return self.name + + +class Dog(RulesModel): + name = models.CharField(max_length=64) + age = models.IntegerField() + gender = models.CharField(max_length=6, choices=Gender.choices) + + class Meta: + rules_permissions = { + "create": rules.always_true, + "retrieve": rules.always_true, + "destroy": rules.is_staff, } def __str__(self) -> str: diff --git a/tests/testapp/rules.py b/tests/testapp/rules.py new file mode 100644 index 0000000..d876dd7 --- /dev/null +++ b/tests/testapp/rules.py @@ -0,0 +1,9 @@ +import rules + + +@rules.predicate +def is_adult_cat(user, cat=None): + if cat is None: + return True + + return cat.age >= 3 diff --git a/tests/testapp/tests.py b/tests/testapp/tests.py index 1f171f8..d46e967 100644 --- a/tests/testapp/tests.py +++ b/tests/testapp/tests.py @@ -12,7 +12,7 @@ from rest_framework.serializers import ModelSerializer from rest_framework.test import APITestCase, URLPatternsTestCase from rest_framework.viewsets import ModelViewSet -from testapp.models import Cat +from testapp.models import Cat, Dog, Gender from drf_rules.permissions import AutoRulesPermission @@ -27,6 +27,11 @@ class Meta: model = Cat fields = "__all__" + class DogSerializer(ModelSerializer): + class Meta: + model = Dog + fields = "__all__" + class CatViewSet(ModelViewSet): queryset = Cat.objects.all() serializer_class = CatSerializer @@ -40,51 +45,134 @@ def custom_detail(self, request, pk): def custom_nodetail(self, request): return Response() + @action(detail=True) + def unknown_detail(self, request, pk): + return Response() + @action(detail=False) - def unknown(self, request): + def unknown_nodetail(self, request): return Response() + class DogViewSet(ModelViewSet): + queryset = Dog.objects.all() + serializer_class = DogSerializer + permission_classes = [AutoRulesPermission] + router = SimpleRouter() router.register("cats", CatViewSet) + router.register("dogs", DogViewSet) cls.urlpatterns = router.get_urls() return super().setUpClass() - def test_predefined_actions(self): + def test_predefined_cat_actions(self): url = reverse("cat-list") url_1 = reverse("cat-detail", [1]) - # Create should be allowed due to the create permission + # create response = self.client.post( url, - {"name": "michi", "age": 3, "gender": "femenino"}, + {"name": "michi", "age": 3, "gender": Gender.FEMALE}, format="json", ) self.assertEqual(response.status_code, 201) - # List should be forbidden due to missing list permission + # update + response = self.client.put( + url_1, + {"name": "michi", "age": 2, "gender": Gender.MALE}, + format="json", + ) + self.assertEqual(response.status_code, 200) + + # update + response = self.client.put( + url_1, + {"name": "michi", "age": 4, "gender": Gender.MALE}, + format="json", + ) + self.assertEqual(response.status_code, 403) + + # partial_update + response = self.client.patch( + url_1, + {"age": 6}, + format="json", + ) + self.assertEqual(response.status_code, 200) + + # list response = self.client.get(url, format="json") + self.assertEqual(response.status_code, 200) + + # retrieve + response = self.client.get(url_1, format="json") + self.assertEqual(response.status_code, 200) + + # destroy + response = self.client.delete(url_1, format="json") self.assertEqual(response.status_code, 403) - # Retrieve should be allowed due to the view permission + def test_predefined_dog_actions(self): + url = reverse("dog-list") + url_1 = reverse("dog-detail", [1]) + + # create + response = self.client.post( + url, + {"name": "puppy", "age": 3, "gender": Gender.FEMALE}, + format="json", + ) + self.assertEqual(response.status_code, 201) + + # update + with self.assertRaises(ImproperlyConfigured): + self.client.put( + url_1, + {"name": "puppy", "age": 2, "gender": Gender.MALE}, + format="json", + ) + + # partial_update + with self.assertRaises(ImproperlyConfigured): + self.client.patch( + url_1, + {"age": 6}, + format="json", + ) + + # list + with self.assertRaises(ImproperlyConfigured): + self.client.get(url, format="json") + + # retrieve response = self.client.get(url_1, format="json") self.assertEqual(response.status_code, 200) - # Destroy should be forbidden due to the destroy permission + # destroy response = self.client.delete(url_1, format="json") self.assertEqual(response.status_code, 403) - def test_custom_actions(self): + def test_custom_cat_actions(self): url = reverse("cat-custom-nodetail") url_1 = reverse("cat-custom-detail", [1]) + # custom_nodetail response = self.client.get(url, format="json") self.assertEqual(response.status_code, 200) + # custom_detail response = self.client.get(url_1, format="json") self.assertEqual(response.status_code, 200) - def test_unknown_action(self): - url = reverse("cat-unknown") + def test_unknown_cat_action(self): + url = reverse("cat-unknown-nodetail") + url_1 = reverse("cat-unknown-detail", [1]) + + # unknow_nodetail with self.assertRaises(ImproperlyConfigured): self.client.get(url, format="json") + + # unkown_detail + with self.assertRaises(ImproperlyConfigured): + self.client.get(url_1, format="json")