In this article, we're going to look at how to test a Django TemplateView
class-based view that requires staff privileges. Imagine you have a dashboard built on a TemplateView
. You don't want the general public to be able to access this resource so you restrict access using the Django UserPassesTestMixin
. Given the sensitive nature of this privileged resource, we want to have proper tests structured so that we know permissions are being enforced.
If you'd like to follow along on your local machine, you can clone the repo for this project "django-staff-permissions-testing." This article builds on the previous article How to Test a Django TemplateView and will use the utility mixin produced in the article Template Tests for Django TemplateView. This mixin will allow us to skip a few basic tests and get right to looking at permissions. If you haven't already, I recommend taking a look through these two articles first.
Set Up
Let's create a view that requires staff permissions.
# demo.views.py
from django.views.generic import TemplateView
from django.contrib.auth.mixins import UserPassesTestMixin
class DashboardTemplateView(UserPassesTestMixin, TemplateView):
template_name = 'demo/dashboard.html'
raise_exception = True
def test_func(self):
return self.request.user.is_staff
In addition to the TemplateView, we're using the UserPassesTestMixin
to enforce a rule determined by the test_func
method. This method requires a statement that evaluates to True
when conditions are met. In our case, we want our user to have the attribute is_staff
set True
so that only staff users have access to this resource.
You'll notice that raise_exception
is set True
. If it weren't, the mixin would attempt to redirect us to the login page. This demo doesn't have a login page so we'd get a 404 error. Instead, what we're looking for is a 403, or "permission denied," status code with our response. This attribute set to True
will accomplish that.
Development Server
Let's now check we get a 403 status code when we try to access the page. To spin up a development server run python manage.py migrate
followed by python manage.py runserver
. The URL pattern is located at the projects URL root, so simply navigate to http://127.0.0.1:8000/
once the development server starts up. If all is well, we'll receive this log to the console: [9/Nov/2023 18:24:19] "GET / HTTP/1.1" 403 135
.
Testing
With a view requiring permissions implemented, we're now ready to begin testing.
Mixin
As mentioned above, we'll use a mixin we created in an earlier article that already checks "template_name," "response_code," and the "url" so that way we can jump right to testing permissions.
# utils.test.view_test_mixins.py
from django.views.generic import TemplateView
# noinspection PyUnresolvedReferences
class TemplateViewTestMixin:
error_msg = 'The required attribute %s has not been set.'
def _check_attr(self, attr_name):
if not getattr(self, attr_name, None):
raise AttributeError(self.error_msg % attr_name)
def test_correct_template(self):
self._check_attr('view_class')
self._check_attr('template_name')
if not issubclass(getattr(self, 'view_class'), TemplateView):
raise TypeError("Attr 'view_class' not a subclass of 'TemplateView.'")
view = self.view_class()
self.assertEqual(view.template_name, self.template_name)
def test_good_status_code(self):
self._check_attr('url')
# check if there's a user attached to the test that may have
# required permissions
if hasattr(self, 'user'):
# assumes user was already assigned required permissions
self.client.force_login(getattr(self, 'user'))
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
def test_good_location(self):
self._check_attr('url')
self._check_attr('static_url')
self.assertEqual(self.url, self.static_url)
Let's now structure a simple test with this mixin, including the required attributes, and ensure we're passing our basic tests before continuing.
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from utils.test.view_test_mixins import TemplateViewTestMixin
from ..views import DashboardTemplateView
User = get_user_model()
class TestDashboardTemplateView(TemplateViewTestMixin, TestCase):
@classmethod
def setUpTestData(cls):
cls.view_class = DashboardTemplateView
cls.template_name = 'demo/dashboard.html'
cls.url = reverse('dashboard')
cls.static_url = '/'
# we need a user that has staff permissions for the client get
# request to succeed
cls.user = User.objects.create(username='example', is_staff=True)
Unlike the implementation in the article Template Tests for Django TemplateView, we need a user with permission or the "GET" request used for checking the response_code
will fail. Otherwise, all else is the same.
Now we'll run the test and make sure everything passes.
python manage.py test --verbosity 2
# output
test_correct_template (demo.tests.test_dashboard_template_view.TestDashboardTemplateView.test_correct_template) ... ok
test_good_location (demo.tests.test_dashboard_template_view.TestDashboardTemplateView.test_good_location) ... ok
test_good_status_code (demo.tests.test_dashboard_template_view.TestDashboardTemplateView.test_good_status_code) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.026s
OK
Looks good.
Test Staff Required
We've already tested one side of "staff required" since the "GET" request on the client needed permission to run. A redundant check for a good status code will just be a drag on performance. Additionally, we're more interested that a user without permissions is denied rather than one with permissions is accepted.
To run this check, we need to create a fresh user without permissions, run a "GET" request on the URL, and test for a 403 (permission denied) status_code
.
# demo.tests.test_dashboard_template_view.py
class TestDashboardTemplateView(TemplateViewTestMixin, TestCase):
...
def test_permission_denied(self):
# make sure usernames don't collide
user = User.object.create(username="different-user")
self.client.force_login(user)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 403)
Now let's check that this test passes.
python manage.py test --verbosity 2
# output
test_correct_template (demo.tests.test_dashboard_template_view.TestDashboardTemplateView.test_correct_template) ... ok
test_good_location (demo.tests.test_dashboard_template_view.TestDashboardTemplateView.test_good_location) ... ok
test_good_status_code (demo.tests.test_dashboard_template_view.TestDashboardTemplateView.test_good_status_code) ... ok
test_permission_denied (demo.tests.test_dashboard_template_view.TestDashboardTemplateView.test_permission_denied) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.022s
OK
Another good test run. We can now be confident that our view is protected from non-staff users gaining access.
Final Thoughts
Testing a staff-only view with required permissions adds complexity over testing a publicly accessible view. But it's important to make sure that the views we expect to have access restricted are protected as we intended. With the above test, we cover that non-staff users are denied in addition to the tests that the mixin already checked. Now we can sleep well knowing our app will function properly and is secure.
Source code for this project can be found on Github.