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?
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.
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.
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.
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'
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')
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With