Last active
September 16, 2020 14:32
-
-
Save benoitbryon/5156512 to your computer and use it in GitHub Desktop.
Sample code to illustrate a blog post about testing view decorators in Django apps.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Testing Django view decorators. | |
This code was originally published as part of an article at | |
http://tech.novapost.fr/django-testing-view-decorators-en.html | |
To run this file: | |
.. code-block:: sh | |
virtualenv --distribute --no-site-packages testing | |
cd testing/ | |
bin/pip install --index-url=https://simple.crate.io django mock | |
wget -O sample.py https://gist.github.com/benoitbryon/5156512/raw/ | |
bin/python -m unittest sample | |
""" | |
from functools import wraps | |
import unittest | |
try: | |
from unittest import mock | |
except ImportError: | |
import mock | |
from django.conf import settings | |
# Django needs some early configuration. | |
settings.configure( | |
DATABASES={}, # Required to load ``django.views.generic``. | |
DEFAULT_CHARSET='utf-8', # Required to instanciate an HttpResponse. | |
) | |
from django.http import HttpResponse, HttpResponseForbidden | |
from django.utils.decorators import available_attrs | |
from django.views.generic.base import TemplateView | |
class MockViewTestCase(unittest.TestCase): | |
def test_stub(self): | |
# Setup. | |
request = 'fake request' | |
view = mock.MagicMock(return_value='fake response') | |
# Run. | |
response = view(request) | |
# Check. | |
view.assert_called_once_with(request) | |
self.assertEqual(response, view.return_value) | |
def hello_world(view_func): | |
"""Run the decorated view, but return "Hello world!".""" | |
def decorated_view(request, *args, **kwargs): | |
view_func(request, *args, **kwargs) | |
return HttpResponse(u'Hello world!') | |
return decorated_view | |
class HelloWorldTestCase(unittest.TestCase): | |
def test_hello_decorator(self): | |
"""hello_world decorator runs view and returns greetings.""" | |
# Setup. | |
request = 'fake request' | |
request_args = ('foo', ) | |
request_kwargs = {'bar': 'baz'} | |
view = mock.MagicMock(return_value='fake response') | |
# Run. | |
decorated = hello_world(view) | |
response = decorated(request, *request_args, **request_kwargs) | |
# Check. | |
# View was called. | |
view.assert_called_once_with(request, *request_args, **request_kwargs) | |
# But response is "Hello world!". | |
self.assertEqual(response.status_code, 200) | |
self.assertEqual(response.content, u"Hello world!") | |
class HttpResponseUnauthorized(HttpResponse): | |
status_code = 401 | |
class UnauthorizedView(TemplateView): | |
response_class = HttpResponseUnauthorized | |
template_name = '401.html' | |
class ForbiddenView(TemplateView): | |
response_class = HttpResponseForbidden | |
template_name = '403.html' | |
def authenticated_user_passes_test(test_func, | |
unauthorized=UnauthorizedView.as_view(), | |
forbidden=ForbiddenView.as_view()): | |
"""Make sure user is authenticated and passes test. | |
This is an adaptation of | |
``django.contrib.auth.decorators.user_passes_test`` where: | |
* if user is anonymous, the request is routed to ``unauthorized`` view. | |
No additional tests are performed in that case. | |
* if user is authenticated and doesn't pass ``test_func ``test, the | |
request is routed to ``forbidden`` view. | |
* else, request and arguments are passed to decorated view. | |
Typical ``unauthorized`` view returns HTTP 401 status code and gives the | |
user an opportunity to log in: access may be granted after | |
authentication. | |
Typical ``forbidden`` view returns HTTP 403 status code: with active user | |
account, access is refused. As explained in rfc2616, 401 and 403 status | |
codes could be suitable. | |
.. seealso:: | |
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4 | |
* https://en.wikipedia.org/wiki/List_of_HTTP_status_codes | |
""" | |
def decorator(view_func): | |
@wraps(view_func, assigned=available_attrs(view_func)) | |
def _wrapped_view(request, *args, **kwargs): | |
if not request.user.is_authenticated(): | |
return unauthorized(request) | |
if not test_func(request.user): | |
return forbidden(request) | |
return view_func(request, *args, **kwargs) | |
return _wrapped_view | |
return decorator | |
class AuthenticatedUserPassesTestTestCase(unittest.TestCase): | |
def setUp(self): | |
"""Common setup: fake request, stub views, stub user test function.""" | |
super(AuthenticatedUserPassesTestTestCase, self).setUp() | |
# Fake request and its positional and keywords arguments. | |
self.request = mock.MagicMock() | |
self.request.user.is_authenticated = mock.MagicMock() | |
self.request_args = ['fake_arg'] | |
self.request_kwargs = {'fake': 'kwarg'} | |
# Mock user test function. | |
self.test_func = mock.MagicMock() | |
# Mock unauthorized and forbidden views. | |
self.unauthorized_view = mock.MagicMock( | |
return_value=u"401 - You may log in.") | |
self.forbidden_view = mock.MagicMock( | |
return_value=u"403 - Insufficient privileges.") | |
# Mock the view to decorate. | |
self.authorized_view = mock.MagicMock( | |
return_value=u"200 - Greetings, Professor Falken.") | |
def run_decorated_view(self, is_authenticated=True, user_passes_test=True): | |
"""Setup, decorate and call view, then return response.""" | |
# Custom setup. | |
self.request.user.is_authenticated.return_value = is_authenticated | |
self.test_func.return_value = user_passes_test | |
# Get decorator. | |
decorator = authenticated_user_passes_test( | |
self.test_func, | |
unauthorized=self.unauthorized_view, | |
forbidden=self.forbidden_view) | |
# Decorate view. | |
decorated_view = decorator(self.authorized_view) | |
# Return response. | |
return decorated_view(self.request, | |
*self.request_args, | |
**self.request_kwargs) | |
def test_unauthorized(self): | |
"""authenticated_user_passes_test first tests user authentication.""" | |
response = self.run_decorated_view(is_authenticated=False) | |
# Check: unauthorized view was called with request as unique positional | |
# argument. | |
self.unauthorized_view.assert_called_once_with(self.request) | |
self.assertEqual(response, self.unauthorized_view.return_value) | |
# Test func was not called. | |
self.assertFalse(self.test_func.called) | |
# Of course, authorized and forbidden views were not called. | |
self.assertFalse(self.authorized_view.called) | |
self.assertFalse(self.forbidden_view.called) | |
def test_test_func_args(self): | |
"""authenticated_user_passes_test passes user instance to test func.""" | |
self.run_decorated_view(is_authenticated=True) | |
# Check: test_func was called with one argument: user instance. | |
self.test_func.assert_called_once_with(self.request.user) | |
def test_forbidden(self): | |
"""authenticated_user_passes_test runs forbidden view if user fails.""" | |
response = self.run_decorated_view(is_authenticated=True, | |
user_passes_test=False) | |
# Check: forbidden view was called with request as unique positional | |
# argument. | |
self.forbidden_view.assert_called_once_with(self.request) | |
self.assertEqual(response, self.forbidden_view.return_value) | |
# Of course, authorized and unauthorized views were not triggered. | |
self.assertFalse(self.authorized_view.called) | |
self.assertFalse(self.unauthorized_view.called) | |
def test_authorized(self): | |
"""authenticated_user_passes_test runs view if user passes test.""" | |
response = self.run_decorated_view(is_authenticated=True, | |
user_passes_test=True) | |
# Check: decorated view has been called, request and other arguments | |
# were proxied as is, response was not altered. | |
self.authorized_view.assert_called_once_with(self.request, | |
*self.request_args, | |
**self.request_kwargs) | |
self.assertEqual(response, self.authorized_view.return_value) | |
# Of course, forbidden and unauthorized views were not triggered. | |
self.assertFalse(self.forbidden_view.called) | |
self.assertFalse(self.unauthorized_view.called) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment