Test Django with dictionary in dictionary data - python

Description
I'm using Django for my website and trying to write some unit tests. However, when using dict in dict data I met some errors. I found out that view received slightly different data.
From
{'class_id': '1', 'filter_options': {'full_name': 'user 1'}} in data of test file
to <QueryDict: {'class_id': ['1'], 'filter_options': ['full_name']}>. I means that value has changed to array, and value of nested dict wasn't detected.
Code
# test.py
def test_search_with_full_name(self):
data = {'class_id': '1', 'filter_options': {'full_name': 'user 1'}}
response = self.client.post('/api/students', data, format='json')
self.assertEqual(response.status_code, 200)
# view.py
class SearchStudentView(generics.GenericAPIView):
def post(self, request):
print(request.data)
if not 'class_id' in request.data:
return HttpResponse('class_id is required', status=400)
class_id = request.data['class_id']
students = Users.objects.filter(
details_student_attend_class__course__pk=class_id)
if 'filter_options' in request.data:
filter_options = request.data['filter_options']
if 'full_name' in filter_options:
students = students.filter(
full_name=filter_options['full_name'])
Thanks for any help!

Django's TestCase uses django.test.Client as the client and it does not set the content type etc. according to the format kwarg, instead it uses the content_type kwarg so you should either set that or use APITestCase from DRF which will use the APIClient class.
While using TestCase:
def test_search_with_full_name(self):
data = {'class_id': '1', 'filter_options': {'full_name': 'user 1'}}
response = self.client.post('/api/students', data, content_type='application/json')
self.assertEqual(response.status_code, 200)
While using APITestCase from DRF:
from rest_framework.test import APITestCase
class YourTest(APITestCase):
def test_search_with_full_name(self):
data = {'class_id': '1', 'filter_options': {'full_name': 'user 1'}}
response = self.client.post('/api/students', data, format='json')
self.assertEqual(response.status_code, 200)

Could you change format to content_type='application/json'
Django docs say this
If you provide content_type as application/json, the data is serialized using json.dumps() if it’s a dict, list, or tuple. Serialization is performed with DjangoJSONEncoder by default, and can be overridden by providing a json_encoder argument to Client. This serialization also happens for put(), patch(), and delete() requests.
There might be an issue with serializing the dictionaries as values for POST fields. So in case if above solution doesn't work another way could be a serialize filter options by your self using json.dumps(filter_options) before passing to the request. And then read them via json.loads(request.POST.get(filter_options)) in your view.

Related

AttributeError: This QueryDict instance is immutable for test cases

I am trying to change my request.data dict to remove some additional field.
It is working completely fine in views.
But when I run test cases for the same, I get this error:
AttributeError: This QueryDict instance is immutable
Here is my viewset:
def create(self, request, *args, **kwargs):
context = {'view': self, 'request': request}
addresses = request.data.pop("addresses", None)
serializer = self.get_serializer(data=request.data, context=context)
serializer.is_valid(raise_exception=True)
response = super(WarehouseViewSet, self).create(request, *args, **kwargs)
if addresses is None:
pass
else:
serializer = self.get_serializer(data=request.data, context=context)
serializer.is_valid(raise_exception=True)
addresses = serializer.update_warehouse_address(request, addresses, response.data["id"])
response.data["addresses"] = addresses
return Response(data=response.data, status=status.HTTP_201_CREATED)
and here is my test case for the same view:
def test_create_warehouse_authenticated(self):
response = client.post(
reverse('warehouse_list_create'),
data={
'name': self.test_warehouse['test_warehouse']['name'],
'branch': self.test_warehouse['test_warehouse']['branch'],
},
**{'HTTP_AUTHORIZATION': 'Bearer {}'.format(
self.test_users['test_user']['access_token']
)},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
How to fix this error?
Try setting format='json' when calling client.post, rather than relying on the default. You don't mention which test client you are using, but you should be using the APIClient
client = APIClient()
client.login(...)
client.post(..., format='json')
Newer Django has a immutable QueryDict, so this error will always happen if you are getting your data from querystring or a multipart form body. The test client uses multipart by default, which results in this issue.
Last Resort: If you need to post multipart, and also modify the query dict (very rare, think posting image + form fields) you can manually set the _mutable flag on the QueryDict to allow changing it. This is
setattr(request.data, '_mutable', True)

Use existing ModelSerializer with JSONResponse

I have a Twitter authentication view that doesn't use a viewset so the auth can be handled on the backend. The view takes in the oauth_token & uses Twython to get the profile & create a Twitter model.
Currently I just return status 201 on success, but to alleviate the need for another request after creation, I'd like to return the created model. I have a TwitterSerializer already which defines the fields that I want to include, so I'd like to be able to reuse this if possible.
TwitterSerializer
class TwitterSerializer(serializers.ModelSerializer):
class Meta:
model = Twitter
fields = (
"id",
"twitter_user_id",
"screen_name",
"display_name",
"profile_image_url",
)
When I try to use this, I get the error that Instance of TwitterSerializer is not JSON serializable.
serialized = TwitterSerializer(instance=twitter)
return JsonResponse({ "created": serialized })
I could return a serialized instance of the model using serializers.serialize()
serialized = serializers.serialize('json', [twitter, ])
serialized = serialized[0]
return JsonResponse({ "created": serialized })
I could pass the fields kwarg to serialize() but I don't want to have to repeat myself if I don't have to. So would it be possible to re-use my TwitterSerializer in this case? I'm having trouble finding a direct answer since most docs assume you'll be using a ViewSet when using serializerss understandably, and this feels like an edge case. I'm open to suggestions for refactoring this approach as well!
After serialization, you can get your data using data attribute of serializer like this.
serialized = TwitterSerializer(instance=twitter)
return JsonResponse({ "created": serialized.data })
You should use Django rest Response instead of JsonResponse like this
from rest_framework response
serialized = TwitterSerializer(instance=twitter)
return response.Response({ "created": serialized.data })

POST document with Django RequestFactory instead of form data

I'd like to build a request for testing middleware, but I don't want POST requests to always assume I'm sending form data. Is there a way to set request.body on a request generated from django.test.RequestFactory?
I.e., I'd like to do something like:
from django.test import RequestFactory
import json
factory = RequestFactory(content_type='application/json')
data = {'message':'A test message'}
body = json.dumps(data)
request = factory.post('/a/test/path/', body)
# And have request.body be the encoded version of `body`
The code above will fail the test because my middleware needs the data to be passed as the document in request.body not as form data in request.POST. However, RequestFactory always sends the data as form data.
I can do this with django.test.Client:
from django.test import Client
import json
client = Client()
data = {'message':'A test message'}
body = json.dumps(data)
response = client.post('/a/test/path/', body, content_type='application/json')
I'd like to do the same thing with django.test.RequestFactory.
RequestFactory has built-in support for JSON payloads. You don't need to dump your data first. But you should be passing the content-type to post, not to the instantiation.
factory = RequestFactory()
data = {'message':'A test message'}
request = factory.post('/a/test/path/', data, content_type='application/json')
I've tried Jay's solution and didn't work, but after some reseach, this did (Django 2.1.2)
factory = RequestFactory()
request = factory.post('/post/url/')
request.data = {'id': 1}
Here's what worked for me in Django 4.1:
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from customauth import views
class RegistrationViewTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
def test_post_request_creates_new_user(self):
data = {
'email': 'new_user#email.com',
'screen_name': 'new_user',
'password1': 'new_user_password',
'password2': 'new_user_password',
}
request = self.factory.post('/any/path/will/do/', data )
middleware = SessionMiddleware(request)
middleware.process_request(request)
request.session.save()
response = views.registration_view(request)
self.assertEqual(response.status_code, 302)
# ok
This test passes. The form was successfully processed in views.registration_view.
Note:
When I included content_type='application/json' in the call to self.factory.post (as the accepted answer suggests), request.POST had no content in the view. Without that, it worked. I don't know why but would be happy to learn.
I needed to manually added SessionMiddleware to request.
In later version of Django (tested on 4.0) this is no longer an issue. On the other hand, to pass data to request.POST might be.
In default, when passing content-type to a RequestFactory, data goes into request.body and when you don't, data goes into request.POST.
request_factory = RequestFactory()
# provide content-type
request = request_factory.post(f'url', data={'foo': 'bar'}, content_type="application/json")
print(request.body) # b'{"foo": "bar"}'
# don't provide content type
request = request_factory.post(f'url', data={'foo': 'bar'})
print(request.POST) # <QueryDict: {'foo': ['bar']}>

Django / DjangoRestFramework - unittest not authenticating user created using ORM

This is my test:
class PageTests(APITestCase):
def setUp(self):
Location.objects.create(locationName = 'Location of Mine', LocationCode = 'LOM')
User.objects.create(username='b', password='b', email='b#hotmail.com')
def test_create_page(self):
"""
Ensure only authenticated users can create a new page object.
"""
url = reverse('page-list')
# See if unauthenticated unadmin users can create a page (they shouldn't.)
data = {'location': 1, 'pageName': 'Test Page 1', 'pageDescription': 'This is the first test page', 'pageCode': 'TP1'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
# See if authenticated users can create a page (they should).
print(User.objects.get().username)
self.client.login(username='b', password='b')
response = self.client.post(url, data, format='json')
print(response.data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
This is my views.py / viewset:
class IsAuthenticated(permissions.BasePermission):
def has_permission(self, request, view):
print('here!!!!!!!!!!!!')
print(request.user)
return request.user.is_authenticated()
class pageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all()
serializer_class = PageSerializer
permission_classes = (IsAuthenticated,)
The problem is, even after I log the user in by doing self.client.login(username='b', password='b') it still raises a 403 error when posting. This is what gets printed:
here!!!!!!!!!!!!
AnonymousUser
b
here!!!!!!!!!!!!
AnonymousUser
{'detail': 'Authentication credentials were not provided.'}
As you can see, Django does see the user object (because it prints 'b') but the user does not get signed in for some reason and is still an AnonymousUser. Now, when I change my setup to this:
def setUp(self)
url = reverse('user-list')
# Create the user using the API.
data = {'username': 'b', 'password': 'b', 'email': 'a#hotmail.com', 'location': '1'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
and then log the user in, it work perfectly fine and the test doesn't raise any errors. Any idea why it raises errors when creating the user using User.objects.create()?
I've used similar code before in other unittest classes (creating the user using the ORM and then signing him in) and it works. I'm not sure why it's not working here.
Edit: Also, if I create the user and make him a super user and log him in, like so:
User.objects.create_superuser(username='a', password='a', email='a#hotmail.com')
it works as well.
Found the answer. I had to create the user by doing this:
User.objects.create_user()
and not this:
User.objects.create()

How to get POST data from request?

I just set up an apache server with django, and to test it, made a very simple function in views.py
channel = rabbit_connection()
#csrf_protect
#csrf_exempt
def index(request):
data={'text': 'Food truck is awesome! ', 'email': 'bob#yahoo.com', 'name': 'Bob'}
callback(json.dumps(data))
context = RequestContext(request)
return render_to_response('index.html', context_instance=context)
This function works fine if I send a GET or POST request to the server. However I would like to get this data from POST request. Assuming I send request like this:
import pycurl
import simplejson as json
data = json.dumps({'name':'Bob', 'email':'bob#yahoo.com', 'text': u"Food truck is awesome!"})
c = pycurl.Curl()
c.setopt(c.URL, 'http://ec2-54-......compute-1.amazonaws.com/index.html')
c.setopt(c.POSTFIELDS, data)
c.setopt(c.VERBOSE, True)
for i in range(100):
c.perform()
What I would like to have in the view is something like this:
if request.method == 'POST':
data = ?????? # Something that will return me my dictionary
Just in case:
It is always will be in JSON format and the fields are unknown.
data= request.POST.get('data','')
Will return you a single value (key=data) from your dictionary. If you want the entire dictionary, you simply use request.POST. You are using the QueryDict class here:
In an HttpRequest object, the GET and POST attributes are instances of django.http.QueryDict. QueryDict is a dictionary-like class customized to deal with multiple values for the same key. This is necessary because some HTML form elements, notably , pass multiple values for the same key.
QueryDict instances are immutable, unless you create a copy() of them. That means you can’t change attributes of request.POST and request.GET directly.
-Django Docs
If the data posted is in JSON format, you need to deserialize it:
import simplejson
myDict = simplejson.loads(request.POST.get('data'))

Categories