Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I test binary file uploading with django-rest-framework's test client?

I have a Django application with a view that accepts a file to be uploaded. Using the Django REST framework I'm subclassing APIView and implementing the post() method like this:

class FileUpload(APIView):     permission_classes = (IsAuthenticated,)      def post(self, request, *args, **kwargs):         try:             image = request.FILES['image']             # Image processing here.             return Response(status=status.HTTP_201_CREATED)         except KeyError:             return Response(status=status.HTTP_400_BAD_REQUEST, data={'detail' : 'Expected image.'}) 

Now I'm trying to write a couple of unittests to ensure authentication is required and that an uploaded file is actually processed.

class TestFileUpload(APITestCase):     def test_that_authentication_is_required(self):         self.assertEqual(self.client.post('my_url').status_code, status.HTTP_401_UNAUTHORIZED)      def test_file_is_accepted(self):         self.client.force_authenticate(self.user)         image = Image.new('RGB', (100, 100))         tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg')         image.save(tmp_file)         with open(tmp_file.name, 'rb') as data:             response = self.client.post('my_url', {'image': data}, format='multipart')             self.assertEqual(status.HTTP_201_CREATED, response.status_code) 

But this fails when the REST framework attempts to encode the request

Traceback (most recent call last):   File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/django/utils/encoding.py", line 104, in force_text     s = six.text_type(s, encoding, errors) UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 118: invalid start byte  During handling of the above exception, another exception occurred:  Traceback (most recent call last):   File "/home/vagrant/webapp/myproject/myapp/tests.py", line 31, in test_that_jpeg_image_is_accepted     response = self.client.post('my_url', { 'image': data}, format='multipart')   File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-    packages/rest_framework/test.py", line 76, in post     return self.generic('POST', path, data, content_type, **extra)   File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/rest_framework/compat.py", line 470, in generic     data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)   File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/django/utils/encoding.py", line 73, in smart_text     return force_text(s, encoding, strings_only, errors)   File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/django/utils/encoding.py", line 116, in force_text     raise DjangoUnicodeDecodeError(s, *e.args) django.utils.encoding.DjangoUnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 118: invalid start byte. You passed in b'--BoUnDaRyStRiNg\r\nContent-Disposition: form-data; name="image"; filename="tmpyz2wac.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\xff\xd8\xff[binary data omitted]' (<class 'bytes'>) 

How can I make the test client send the data without attempting to decode it as UTF-8?

like image 715
Tore Olsen Avatar asked Jun 13 '14 09:06

Tore Olsen


People also ask

How do I check Django REST framework?

For Django REST Framework to work on top of Django, you need to add rest_framework in INSTALLED_APPS, in settings.py. Bingo..!! Django REST Framework is successfully installed, one case use it in any app of Django.

What is APIRequestFactory?

APIRequestFactory : This is similar to Django's RequestFactory . It allows you to create requests with any http method, which you can then pass on to any view method and compare responses. APIClient : similar to Django's Client . You can GET or POST a URL, and test responses.

What is self client in Django?

self. client , is the built-in Django test client. This isn't a real browser, and doesn't even make real requests. It just constructs a Django HttpRequest object and passes it through the request/response process - middleware, URL resolver, view, template - and returns whatever Django produces.


2 Answers

When testing file uploads, you should pass the stream object into the request, not the data.

This was pointed out in the comments by @arocks

Pass { 'image': file} instead

But that didn't full explain why it was needed (and also didn't match the question). For this specific question, you should be doing

from PIL import Image  class TestFileUpload(APITestCase):      def test_file_is_accepted(self):         self.client.force_authenticate(self.user)          image = Image.new('RGB', (100, 100))          tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg')         image.save(tmp_file)         tmp_file.seek(0)          response = self.client.post('my_url', {'image': tmp_file}, format='multipart')         self.assertEqual(status.HTTP_201_CREATED, response.status_code) 

This will match a standard Django request, where the file is passed in as a stream object, and Django REST Framework handles it. When you just pass in the file data, Django and Django REST Framework interpret it as a string, which causes issues because it is expecting a stream.

And for those coming here looking to another common error, why file uploads just won't work but normal form data will: make sure to set format="multipart" when creating the request.

This also gives a similar issue, and was pointed out by @RobinElvin in the comments

It was because I was missing format='multipart'

like image 189
Kevin Brown-Silva Avatar answered Oct 19 '22 23:10

Kevin Brown-Silva


Python 3 users: make sure you open the file in mode='rb' (read,binary). Otherwise, when Django calls read on the file the utf-8 codec will immediately start choking. The file should be decoded as binary not utf-8, ascii or any other encoding.

# This won't work in Python 3 with open(tmp_file.name) as fp:         response = self.client.post('my_url',                                     {'image': fp},                                     format='multipart')  # Set the mode to binary and read so it can be decoded as binary with open(tmp_file.name, 'rb') as fp:         response = self.client.post('my_url',                                     {'image': fp},                                     format='multipart') 
like image 36
Meistro Avatar answered Oct 20 '22 01:10

Meistro