24👍
Here is one approach:
from django_mock_queries.query import MockSet
from mock import patch, MagicMock
from unittest import TestCase
class TestPermissions(TestCase):
memberships = MockSet()
patch_memberships = patch('myapp.models.Membership.objects', memberships)
def setUp(self):
self.permission = IsOrganizationOwner()
self.memberships.clear()
self.request = MagicMock(user=MagicMock())
self.view = MagicMock()
def create_membership(self, organization, is_admin):
self.request.user.is_admin = is_admin
self.memberships.add(
MagicMock(user=self.request.user, organization=organization)
)
@patch_memberships
def test_permissions_is_organization_owner_returns_false_when_membership_does_not_exist(self):
org = MagicMock()
self.assertFalse(self.permission.has_object_permission(self.request, self.view, org))
@patch_memberships
def test_permissions_is_organization_owner_returns_false_when_membership_is_not_admin(self):
org = MagicMock()
self.create_membership(org, False)
self.assertFalse(self.permission.has_object_permission(self.request, self.view, org))
@patch_memberships
def test_permissions_is_organization_owner_returns_true_when_membership_is_admin(self):
org = MagicMock()
self.create_membership(org, True)
self.assertTrue(self.permission.has_object_permission(self.request, self.view, org))
I used a library that I wrote that mocks django queryset functions to make the tests smaller and more readable. But if you prefer you can use Mock
or MagicMock
to patch only the things you need.
EDIT: For the sake of completeness let’s assume you also wanted to integration test OrganizationViewSet
, here’s some tests for that:
from django.contrib.auth.models import User
from django.test import TestCase, Client
from model_mommy import mommy
class TestOrganizationViewSet(TestCase):
url = '/organizations/'
def create_user(self, is_admin):
password = 'password'
user = mommy.prepare(User, is_admin=is_admin)
user.set_password(password)
user.save()
return user, password
def get_organizations_as(self, user=None, password=None):
api = Client()
if user:
mommy.make(Membership, user=user, organization=mommy.make(Organization))
api.login(username=user.username, password=password)
return api.get(self.url)
def test_organizations_viewset_returns_200_for_admins(self):
response = self.get_organizations_as(*self.create_user(True))
self.assertEqual(response.status_code, 200)
def test_organizations_viewset_returns_403_for_non_admins(self):
response = self.get_organizations_as(*self.create_user(False))
self.assertEqual(response.status_code, 403)
def test_organizations_viewset_returns_403_for_anonymous(self):
response = self.get_organizations_as()
self.assertEqual(response.status_code, 403)
As others have pointed out, you need those tests too, just not for every possible test case. Unit tests are best for that, as integration tests will write to the database etc. and will make your CI procedures slower – in case that sort of thing is relevant to you.
10👍
I was wrangling with this myself, and I think I found a simple solution that tests the behavior of the permissions check in isolation without having to mock everything out. Since this response is coming 4 years after the original answer, Django may have evolved considerably since then.
Testing a permission seems to be as easy as instantiating the permission and testing its has_permission
method with contrived objects. For instance, I tested this out with the IsAdminUser
permission, and the test passed:
from django.contrib.auth.models import User
from django.test import RequestFactory, TestCase
from rest_framework.permissions import IsAdminUser
class IsAdminUserTest(TestCase):
def test_admin_user_returns_true(self):
admin_user = User.objects.create(username='foo', is_staff=True)
factory = RequestFactory()
request = factory.delete('/')
request.user = admin_user
permission_check = IsAdminUser()
permission = permission_check.has_permission(request, None)
self.assertTrue(permission)
Changing is_staff
to False
in the User instantiation causes the test to fail as I’d expect.
Updating this with an actual example
I wrote my own custom permission to check if a user is an admin (staff user) and to allow only read-only operations otherwise. Note that, since this is a unit test, it doesn’t interface with any endpoint or even seek to mock those out; it just tests the expected behavior of the permissions check.
Here’s the permission:
from rest_framework import permissions
class IsAdminUserOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user.is_staff
And here’s the full unit test suite:
from django.contrib.auth.models import User
from django.test import RequestFactory, TestCase
from community.permissions import IsAdminUserOrReadOnly
class IsAdminOrReadOnlyTest(TestCase):
def setUp(self):
self.admin_user = User.objects.create(username='foo', is_staff=True)
self.non_admin_user = User.objects.create(username='bar')
self.factory = RequestFactory()
def test_admin_user_returns_true(self):
request = self.factory.delete('/')
request.user = self.admin_user
permission_check = IsAdminUserOrReadOnly()
permission = permission_check.has_permission(request, None)
self.assertTrue(permission)
def test_admin_user_returns_true_on_safe_method(self):
request = self.factory.get('/')
request.user = self.admin_user
permission_check = IsAdminUserOrReadOnly()
permission = permission_check.has_permission(request, None)
self.assertTrue(permission)
def test_non_admin_user_returns_false(self):
request = self.factory.delete('/')
request.user = self.non_admin_user
permission_check = IsAdminUserOrReadOnly()
permission = permission_check.has_permission(request, None)
self.assertFalse(permission)
def test_non_admin_user_returns_true_on_safe_method(self):
request = self.factory.get('/')
request.user = self.non_admin_user
permission_check = IsAdminUserOrReadOnly()
permission = permission_check.has_permission(request, None)
self.assertTrue(permission)
I assume you could use a similar pattern for just about any user attribute you wanted to write a permission against.
When I get to integration/functional testing, only then will I worry about how this permission affects the interfaces of the API.
- [Django]-Django: does the ORM support the SQL "IN" operator?
- [Django]-Django testing: Test the initial value of a form field
- [Django]-Django Left Outer Join
1👍
As a small enhancement/alternative for the approach outlined above, I would use pytest’s admin_user
fixture provided for django support:
https://pytest-django.readthedocs.io/en/latest/helpers.html#admin-user-an-admin-user-superuser
So a happy path test could look something like this:
def test_returns_200_when_user_is_authenticated(self,
client, admin_user):
client.force_login(admin_user)
response = client.put('/my_url/pk/', data={'some data'},
content_type='application/json'))
assert response.status_code == 200
And you could also test the opposite scenario where the user is not logged in:
def test_returns_403_when_user_is_not_authenticated(
self, client):
response = client.put('/my_url/pk/', data={'some data'},
content_type='application/json'))
assert response.status_code == 403
- [Django]-Import error django corsheaders
- [Django]-Django admin: how to sort by one of the custom list_display fields that has no database field
- [Django]-How can I set two primary key fields for my models in Django?