Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test permissions in django-rest-framework?

Hers is a sample permission that I want to unit test.

# permissions.py

from myapp.models import Membership

class IsOrganizationOwner(permissions.BasePermission):
    """
    Custom permission to allow only owner of the organization to do a certian task.
    """
    def has_object_permission(self, request, view, obj):
        try:
            membership = Membership.objects.get(user = request.user, organization = obj)
        except Membership.DoesNotExist:
            return False

        return membership.is_admin

and here is how it is applied

# viewsets.py
class OrganizationViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows Organizations to be viewed or edited.
    """
    permission_classes = (permissions.IsAuthenticated, IsOrganizationOwner,)
    queryset = Organization.objects.all().order_by('name')
    serializer_class = OrganizationSerializer

Now I am very new to testing in django and I don't know how to test this permission. Any help would be appreciated.

like image 695
Adil Malik Avatar asked May 11 '16 18:05

Adil Malik


People also ask

What are permissions in DRF?

DRF Permissions In DRF, permissions, along with authentication and throttling, are used to grant or deny access for different classes of users to different parts of an API. Authentication and authorization work hand in hand. Authentication is always executed before authorization.

What is permissions Safe_methods?

Requests for unauthorised users will only be permitted if the request method is one of the "safe" methods; GET , HEAD or OPTIONS . This permission is suitable if you want to your API to allow read permissions to anonymous users, and only allow write permissions to authenticated users.


2 Answers

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.

like image 185
fips Avatar answered Sep 18 '22 08:09

fips


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.

like image 44
Derek Avatar answered Sep 22 '22 08:09

Derek