Django forms which needs the request , makes testing harder? - django

I have many django forms in which I pass the request as kwarg.
I've just started dive into testing, and it seems that testing forms which require the request as argument makes the testing harder. As I have to somehow create a request and I cant test my forms without it.
So is it best to avoid passing the request to the form at all? Or another workaround?
The reason I do that on first place is that sometimes I need request.user, or request.session and do some cleaning/setting based on that info in the form.
UPDATE:
This is an example form:
class OrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
self.user = self.request.user
def clean(self):
# Here I have some cross session-field validation
if self.request.session['has_response'] and self.cleaned_data('status') == 'NEW':
raise ValidationError()
def save(self, commit=False):
self.instance.user = self.user
return super(OrderForm, self).save(commit=True)
class Meta:
model = Order
fields = ('address', 'city', 'status', ) # more fields
The view code is simple:
form = OrderForm(request.POST, request=request)
The Order model also have a clean() method with some validation logic.
The session is populated at most during the user login.
The point it I need the session/user there.
But most important, the question - is it a bad design to pass the request and session to the form, considering options for testing this form? I find it more logical when the form take care for saving the object, including the request.user. But maybe I should try to split that between the form and view?

Passing request to the form is okay if you need it in the clean() method. You can use a request/session/user in a test like this:
from django.test import TestCase, Client
from django.test.client import RequestFactory
from django.contrib.auth.models import AnonymousUser, User
from .views import my_view
from .forms import MyForm
from django.contrib.sessions.middleware import SessionMiddleware
# If Python >= 3.4
from unittest.mock import patch, MagicMock
# Else
from mock import patch, MagicMock
class SimpleTest(TestCase):
def setUp(self):
# Create a RequestFactory accessible by the entire class.
self.factory = RequestFactory()
# Create a new user object accessible by the entire class.
self.user = User.objects.create_user(username='username',
email='email', password='password')
def test_my_view(self):
# Create an instance of a GET request.
request = self.factory.get('/my-url/')
# Middleware is not supported so simulate a
# logged-in user by setting request.user.
request.user = self.user
# Or add anonymous user to request.
request.user = AnonymousUser()
# Test view() at '/my-url/'
response = my_view(request)
self.assertEqual(response.status_code, 200)
#patch('app.models.ModelName.save', MagicMock(name="save"))
def test_my_form_view_with_factory(self):
# Set up form data.
form_data = {'something': 'something'}
# Create an instance of a POST request.
request = self.factory.post('/my-form-url/', form_data)
# Simulate logged-in user
request.user = self.user
# Setup session.
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
# Or you should just be able to do
request.session['somekey'] = 'test'
request.session.save()
# Get response from form view, and test passing
# request/data to form.
form = MyForm(request=request, data=form_data)
response = my_form_view(request)
self.assertTrue(form.is_valid())
self.assertEqual(response.status_code, 200)
# If model form you can do
self.assertTrue(ModelName.save.called)
#patch('app.models.ModelName.save', MagicMock(name="save"))
def test_my_form_view_with_client(self):
# Use Client instead of RequestFactory.
self.client = Client()
# Login with Client.
self.client.login(username='username', password='password')
# Set up form data.
form_data = {'something': 'something'}
# Get/set session.
session = self.client.session
session['somekey'] = 'test'
session.save()
# Get response with Client.
response = self.client.post('/my-form-url/', form_data)
self.assertEqual(response.status_code, 200)
# If model form you can do
self.assertTrue(ModelName.save.called)
Should give a general idea of what you can do, not specifically tested.

Related

django-axes is not getting the request argument

I recently added django-axes to my Django project. It is suppose to work out the box with django-restframework. However, I am using django-rest-framework-simplejwt to handle authentication. But it should still work out the box since the only thing that is required for django-axes is passing Django's authentication method the request object which it does in it's source code (line 39 and 43).
When I try to authenticate, I get this error from django-axes:
axes.exceptions.AxesBackendRequestParameterRequired: AxesBackend requires a request as an argument to authenticate
You need to add requests to the authentication function. See sample code below.
serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
def _authenticate_user_email(self, email, password, request):
# This is key: Pass request to the authenticate function
self.user = authenticate(email=email, password=password, request=request)
return self.user
def validate(self, attrs):
password = attrs.get('password')
email = attrs.get('email')
request = self.context.get('request') # <<=== Grab request
self.user = self._authenticate_user_email(email, password, request)
# All error handling should be done by this code line
refresh = self.get_token(self.user)
data = {}
data['refresh'] = str(refresh)
data['access'] = str(refresh.access_token)
return data
views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from authy.serializers import MyTokenObtainPairSerializer
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
urls.py
from authy.views import MyTokenObtainPairView
url(r'^/auth/api/token/$', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
It is also worth mentioning that the simple jwt lib uses the authenticate function, however it does not pass the request parameter. Therefore you need call authenticate, get_token and return data object yourself.
In addition, if you have extended the AbstractBaseUser model of django. And set the USERNAME_FIELD. Then use the param username instead of email. E.g: authenticate(username=email, password=password, request=request)
Use this:
from axes.backends import AxesBackend
class MyBackend(AxesBackend)
def authenticate(self, request=None, *args, **kwargs):
if request:
return super().authenticate(request, *args, **kwargs)
This would skip the AxesBackend in AUTHENTICATION_BACKENDS if the request is unset and would weaken your security setup.
source: https://github.com/jazzband/django-axes/issues/478

django testing class based view

I have a Class based view defined as:
class Myview(LoginRequiredMixin, View):
def post():
#.......
to test this view i tried this
class MyViewTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='jacob', email='soos#i.com', password='vvggtt')
def view_test(self):
# Create an instance of a POST request.
request = self.factory.post('/my-url/')
request.user = self.user
response = MyView(request)
print (response,"**")
self.assertEqual(response.status_code, 200)
But this gives this error.
response = MyView(request)
TypeError: __init__() takes 1 positional argument but 2 were given
I understand why this error is coming (cinstructor of MyView has 2 ars) but how do i remove it? i couldnt get the details on searching.
we can use django test client
from django.test import Client
class MyViewTest(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='jacob', email='soos#i.com', password='vvggtt')
def view_test(self):
# Create an instance of a POST request.
self.client.login(username="jacob", password="vvggtt")
data = {'name': 'test name'}
res = self.client.post('/my-url/', data)
print(res)
self.assertEqual(res.status_code, 200)
From the docs:
# Use this syntax for class-based views.
response = MyView.as_view()(request)
Try
response = MyView(request=request)
There's a section of the Django docs called Testing Class Based Views which addresses this:
In order to test class-based views outside of the request/response cycle you must ensure that they are configured correctly, by calling setup() after instantiation.
So in your case this looks something like:
def view_test(self):
# Create an instance of a POST request.
request = self.factory.post('/my-url/')
request.user = self.user
my_view = MyView()
my_view.setup(request)
response = my_view.post(request)
self.assertEqual(response.status_code, 200)

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.
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.
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.
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

Testing custom admin actions in django

I'm new to django and I'm having trouble testing custom actions(e.g actions=['mark_as_read']) that are in the drop down on the app_model_changelist, it's the same dropdown with the standard "delete selected". The custom actions work in the admin view, but I just dont know how to call it in my mock request, I know I need to post data but how to say I want "mark_as_read" action to be done on the data I posted?
I want to reverse the changelist url and post the queryset so the "mark_as_read" action function will process the data I posted.
change_url = urlresolvers.reverse('admin:app_model_changelist')
response = client.post(change_url, <QuerySet>)
Just pass the parameter action with the action name.
response = client.post(change_url, {'action': 'mark_as_read', ...})
Checked items are passed as _selected_action parameter. So code will be like this:
fixtures = [MyModel.objects.create(read=False),
MyModel.objects.create(read=True)]
should_be_untouched = MyModel.objects.create(read=False)
#note the unicode() call below
data = {'action': 'mark_as_read',
'_selected_action': [unicode(f.pk) for f in fixtures]}
response = client.post(change_url, data)
This is what I do:
data = {'action': 'mark_as_read', '_selected_action': Node.objects.filter(...).values_list('pk', flat=True)}
response = self.client.post(reverse(change_url), data, follow=True)
self.assertContains(response, "blah blah...")
self.assertEqual(Node.objects.filter(field_to_check=..., pk__in=data['_selected_action']).count(), 0)
A few notes on that, comparing to the accepted answer:
We can use values_list instead of list comprehension to obtain the ids.
We need to specify follow=True because it is expected that a successfull post will lead to a redirect
Optionally assert for a successful message
Check that the changes indeed are reflected on the db.
Here is how you do it with login and everything, a complete test case:
from django.test import TestCase
from django.urls import reverse
from content_app.models import Content
class ContentModelAdminTests(TestCase):
def setUp(self):
# Create some object to perform the action on
self.content = Content.objects.create(titles='{"main": "test tile", "seo": "test seo"}')
# Create auth user for views using api request factory
self.username = 'content_tester'
self.password = 'goldenstandard'
self.user = User.objects.create_superuser(self.username, 'test#example.com', self.password)
def shortDescription(self):
return None
def test_actions1(self):
"""
Testing export_as_json action
App is content_app, model is content
modify as per your app/model
"""
data = {'action': 'export_as_json',
'_selected_action': [self.content._id, ]}
change_url = reverse('admin:content_app_content_changelist')
self.client.login(username=self.username, password=self.password)
response = self.client.post(change_url, data)
self.client.logout()
self.assertEqual(response.status_code, 200)
Just modify to use your model and custom action and run your test.
UPDATE: If you get a 302, you may need to use follow=True in self.client.post().
Note that even if the POST is successful, you still need to test that your action performed the operations intended successfully.
Here's another method to test the action directly from the Admin class:
from django.contrib.auth.models import User
from django.contrib.admin.sites import AdminSite
from django.shortcuts import reverse
from django.test import RequestFactory, TestCase
from django.contrib.messages.storage.fallback import FallbackStorage
from myapp.models import MyModel
from myapp.admin import MyModelAdmin
class MyAdminTestCase(TestCase):
def setUp(self) -> None:
self.site = AdminSite()
self.factory = RequestFactory()
self.superuser = User.objects.create_superuser(username="superuser", is_staff=True)
def test_admin_action(self):
ma = MyModelAdmin(MyModel, self.site)
url = reverse("admin:myapp_mymodel_changelist")
superuser_request = self.factory.get(url)
superuser_request.user = self.superuser
# if using 'messages' in your actions
setattr(superuser_request, 'session', 'session')
messages = FallbackStorage(superuser_request)
setattr(superuser_request, '_messages', messages)
qs = MyModel.objects.all()
ma.mymodeladminaction(superuser_request, queryset=qs)
# check that your action performed the operations intended
...

Accessing the request.user object when testing Django

I'm trying to access the request.user object when testing my app using django's client class.
from django.test import TestCase
from django.test.client import Client
class SomeTestCase(TestCase):
def setUp(self):
self.client = Client()
self.client.login( username="foo", password="bar")
def test_one(self):
response = self.client.get("/my_profile/")
self.fail( response.request.user )
This will obviously fail, but it fails because response.request doesn't have a user attribute.
AttributeError: 'dict' object has no attribute 'user'
Is there any way to access the user object from the client instance? I want to do this in order to check if some test user's data is properly rendered. I can try to tackle this by setting up all the necessary info during setUp, but before I do that I wanted to check if I'm not just missing something.
This may seem a roundabout way of doing it but it can be useful.
Use RequestFactory, its pretty straightforward. It imitates the request object.
from django.test import TestCase, RequestFactory
from django.test.client import Client
from django.contrib.auth.models import User
class SomeTestCase(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='foo', email='foo#bar',
password='bar')
def test_one(self):
self.client.login( username="foo", password="bar")
request = self.factory.get("/my_profile/")
request.user = self.user
#rest of your code
def tearDown(self):
self.user.delete()
I hope that was helpful.
Use response.context['user'].
User is automatically available in the template context if you use RequestContext. See auth data in templates doc.
Otherwise i believe you should just query it:
def test_one(self):
response = self.client.get("/my_profile/")
user = User.objects.get(username="foo")
self.fail( user.some_field == False )