Testing custom admin actions in django - 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
...

Related

Django testing how to assert Redirect

With the folliwing code I get this wrong result : nose.proxy.AssertionError: 302 != 200 : Couldn't retrieve redirection page '/mes_dossiers/': response code was 302 (expected 200)
what is wrong with my code ?
#test.py
from django.test import TestCase, RequestFactory, Client
from ..models import *
from ..views import *
from django.core.management import call_command
class Cas(TestCase):
def setUp(self):
call_command('loaddata', 'fixture_users.json', verbosity=1)
call_command('loaddata', 'xxxxx_tests_xxxx.yaml',
verbosity=1)
def test_dossier_duplicate(self) :
request = self.factory.get('/dossier/3/copier/', follow = True)
request.user = User.objects.get(id=3)
pk = 3
response = dossier_duplicate(request, pk)
response.client = Client()
self.assertRedirects(response,'/mes_dossiers/',status_code=302,
target_status_code=200)
#urls.py
urlpatterns = [
url(r'^dossier/(?P<pk>[0-9]+)/copier/$',views.dossier_duplicate),
]
#views.py
#login_required(login_url="/accounts/login/")
def dossier_duplicate(request, pk):
dossier = get_object_or_404(Dossier, pk=pk)
groupe = dossier.createdBy.groups.all()[0].name
if not in_group(request.user, groupe) :
return HttpResponseForbidden('Vous ne pouvez pas accéder à ce
dossier')
else :
#code to duplicate the "dossier" instance and child contents
#
#
return redirect('/mes_dossiers/')
I've found more examples there:
Django : Testing if the page has redirected to the desired url
https://docs.djangoproject.com/en/3.0/topics/testing/tools/#django.test.SimpleTestCase.assertRedirects
and this worked:
class Cas(TestCase):
def setUp(self):
call_command('loaddata', 'fixture_users.json', verbosity=1)
call_command('loaddata', 'xxx_tests_xxxx.yaml',
verbosity=1)
def test_dossier_duplicate(self) :
request = self.client.get('/dossier/3/copier/', follow = True)
request.user = User.objects.get(id=3)
pk = 3
response = dossier_duplicate(request, pk)
response.client = Client()
response.client.login(username='xxxx', password='xxxxx')
self.assertRedirects(response, '/mes_dossiers/', status_code=302,
target_status_code=200, fetch_redirect_response=True)
I reach this question via google and I have a similar issue with testing redirect.
But my Django is v2.2 and the accepted answer cites a v1.7 which is no longer supported
I then google some more and found this code block at https://docs.djangoproject.com/en/2.2/topics/testing/tools/#django.test.SimpleTestCase.settings
from django.test import TestCase
class LoginTestCase(TestCase):
def test_login(self):
# First check for the default behavior
response = self.client.get('/sekrit/')
self.assertRedirects(response, '/accounts/login/?next=/sekrit/')
I modified for my own use which works.
For the OP case, this is how I believe should work if the OP uses 2.2
def test_dossier_duplicate(self) :
response = self.client.get('/dossier/3/copier/')
self.assertRedirects(response, '/mes_dossiers/')
I am leaving this here. In case future readers have a similar question but have it for Django v2.2
To test redirects you should use the test client instead of RequestFactory.
The API for the RequestFactory is a slightly restricted subset of the
test client API:
It only has access to the HTTP methods get(), post(), put(), delete(),
head(), options(), and trace().
These methods accept all the same
arguments except for follows. Since this is just a factory for
producing requests, it’s up to you to handle the response.
https://docs.djangoproject.com/en/1.11/topics/testing/advanced/#the-request-factory
Try changing self.factory.get to self.client.get
response = self.client.get('/dossier/3/copier/', follow = True)

Django forms which needs the request , makes testing harder?

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.

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 Django REST Framework

I'm working on my first project which uses Django REST Framework, and I'm having issues testing the API. I'm getting 403 Forbidden errors instead of the expected 200 or 201. However, the API works as expected.
I have been going through DRF Testing docs, and it all seems straightforward, but I believe my client is not being logged in. Normally in my Django projects I use a mixture of factory boy and django webtest which I've had a lot of happy success with. I'm not finding that same happiness testing the DRF API after a couple days of fiddling around.
I'm not sure if this is a problem relating to something I'm doing wrong with DRF APITestCase/APIClient or a problem with the django test in general.
I'm just pasting the following code and not posting the serializers/viewsets because the API works in the browser, it seems I'm just having issues with the APIClient authentication in the APITestCase.
# settings.py
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}
# tests.py
from django.test import TestCase
from rest_framework.test import APITestCase, APIClient
from accounts.models import User
from .factories import StaffUserFactory
class MainSetUp(TestCase):
def setUp(self):
self.user = StaffUserFactory
self.api_root = '/api/v0/'
self.client = APIClient()
class APITests(MainSetUp, APITestCase):
def test_create_feedback(self):
"""
Ensure we can create a new account object.
"""
self.client.login(username='staffuser', password='staffpassword')
url = '%sfeedback/' % self.api_root
data = {
'feedback': 'this is test feedback'
}
response = self.client.post(url, data, user=self.user)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data, data)
# factories.py
from factory.django import DjangoModelFactory
from django.contrib.auth import get_user_model
User = get_user_model()
class UserFactory(DjangoModelFactory):
class Meta:
model = User
class StaffUserFactory(UserFactory):
username = 'staffuser'
password = 'staffpassword'
email = 'staff#email.com'
first_name = 'Staff'
last_name = 'User'
is_staff = True
I've never used DjangoModelFactory before, but it appears that you have to call create after setting your user to the StaffUserFactory. http://factoryboy.readthedocs.org/en/latest/_modules/factory/django.html#DjangoModelFactory
class MainSetUp(TestCase):
def setUp(self):
self.user = StaffUserFactory
self.user.create()
self.api_root = '/api/v0/'
self.client = APIClient()
I bet your User's password is not being set properly. You should use set_password. As a start, trying changing your setUp to this:
def setUp(self):
self.user = StaffUserFactory
self.user.set_password('staffpassword')
self.user.save() # You could probably omit this, but set_password does't call it
self.api_root = '/api/v0/'
self.client = APIClient()
If that works, you probably want to override _generate() in your factory to add that step.
Another thing to check would be that SessionAuthentication is in your DEFAULT_AUTHENTICATION_CLASSES setting.
I think you must instantiate the Factory to get real object, like this:
self.user = StaffUserFactory()
Hope that help.
Moreover, you don't need to create a seperate class for staff, just set is_staff=True is enough. Like this:
self.user = UseFactory(is_staff=True)

Ordering items on the root api view for DefaultRouter

I'm using the DefaultRouter provided by DRF because I need a root api view. However, the items on that view aren't in any logical order. I looked into the source and discovered that each entry is just put into a dictionary (which inherently isn't ordered).
class DefaultRouter(SimpleRouter):
"""
The default router extends the SimpleRouter, but also adds in a default
API root view, and adds format suffix patterns to the URLs.
"""
include_root_view = True
include_format_suffixes = True
root_view_name = 'api-root'
def get_api_root_view(self):
"""
Return a view to use as the API root.
"""
api_root_dict = {}
list_name = self.routes[0].name
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
class APIRoot(views.APIView):
_ignore_model_permissions = True
def get(self, request, format=None):
ret = {}
for key, url_name in api_root_dict.items():
ret[key] = reverse(url_name, request=request, format=format)
return Response(ret)
return APIRoot.as_view()
I'd like to order the items on the root api view alphabetically and could easily do that by modifying the source. But I was wondering, have any of you come up with solutions to order the root api items without modifying the source code?
Along the lines of what you suggest and of the first point in Denis Cornehi's answer, here is an extension of DefaultRouter that orders the urls by their base_names:
# myapp/routers.py
from rest_framework import routers
from rest_framework import views
from rest_framework.response import Response
from rest_framework.reverse import reverse
import operator
import collections
class OrderedDefaultRouter(routers.DefaultRouter):
def get_api_root_view(self):
"""
Return a view to use as the API root but do it with ordered links.
"""
api_root_dict = {}
list_name = self.routes[0].name
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
class APIRoot(views.APIView):
_ignore_model_permissions = True
def get(self, request, format=None):
ret = {}
for key, url_name in api_root_dict.items():
ret[key] = reverse(url_name, request=request, format=format)
sorted_ret = collections.OrderedDict(sorted(ret.items(), key=operator.itemgetter(0)))
return Response(sorted_ret)
return APIRoot.as_view()
I see two ways here:
as you suggested, override the router, change the APIRoot view (returning an OrderedDict should be enough). In this case I addiotinally would raise an issue with DRF to (perhaps) change it for everyone.
Extend/Override the JSONRenderer to allow sorting the keys when dumping the JSON. Then extend the BrowsableAPIRenderer to set this property. Or just sort all responses. (and, again, perhaps this is interesting for every user of DRF).
With DRF 3.11.2 [not sure from which older version the below will work] you can also do
router = DefaultRouter()
# Register routes ...
router.registry.sort(key=lambda x: x[0])