web-dev-qa-db-fra.com

Comment puis-je tester le téléchargement de fichiers binaires avec le client de test Django-rest-framework?

J'ai une application Django avec une vue qui accepte un fichier à télécharger. Utilisation de Django REST I ') m sous-classer APIView et implémenter la méthode post () comme ceci:

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.'})

Maintenant, j'essaie d'écrire quelques tests pour s'assurer que l'authentification est requise et qu'un fichier téléchargé est réellement traité.

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)

Mais cela échoue lorsque le framework REST tente de coder la demande

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'>)

Comment puis-je faire en sorte que le client de test envoie les données sans essayer de les décoder en UTF-8?

48
Tore Olsen

Lors du test de téléchargement de fichiers, , vous devez passer l'objet de flux dans la demande, pas les données .

Cela a été souligné dans les commentaires par @ arocks

Passez {'image': fichier} à la place

Mais cela n'expliquait pas complètement pourquoi c'était nécessaire (et ne correspondait pas non plus à la question). Pour cette question spécifique, vous devriez faire

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)

Cela correspondra à une requête Django standard, où le fichier est passé en tant qu'objet de flux, et Django REST Lorsque vous passez simplement les données du fichier, Django et Django REST Framework l'interprète comme une chaîne, ce qui cause des problèmes car il attend un flux.

Et pour ceux qui viennent ici à la recherche d'une autre erreur courante, pourquoi les téléchargements de fichiers ne fonctionneront tout simplement pas, mais les données de formulaire normales: s'assureront de définir format="multipart" lors de la création de la demande .

Cela donne également un problème similaire, et a été souligné par @ RobinElvin dans les commentaires

C'est parce que je manquais format = 'multipart'

30
Kevin Brown

Utilisateurs de Python 3: assurez-vous que open le fichier dans mode='rb' (lecture, binaire). Sinon, lorsque Django appelle read sur le fichier, le utf-8 le codec commencera immédiatement à s'étouffer. Le fichier doit être décodé en binaire et non en utf-8, ascii ou tout autre encodage.

# 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')
15
Meistro

Ce n'est pas si simple de comprendre comment le faire si vous voulez utiliser la méthode PATCH, mais j'ai trouvé la solution dans cette question .

from Django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart

with open(tmp_file.name, 'rb') as fp:
    response = self.client.patch(
        'my_url', 
        encode_multipart(BOUNDARY, {'image': fp}), 
        content_type=MULTIPART_CONTENT
    )
2
Anton Shurashov

Pour ceux sous Windows, la réponse est un peu différente. Je devais faire ce qui suit:

resp = None
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp_file:
    image = Image.new('RGB', (100, 100), "#ddd")
    image.save(tmp_file, format="JPEG")
    tmp_file.close()

# create status update
with open(tmp_file.name, 'rb') as photo:
    resp = self.client.post('/api/articles/', {'title': 'title',
                                               'content': 'content',
                                               'photo': photo,
                                               }, format='multipart')
os.remove(tmp_file.name)

La différence, comme indiqué dans cette réponse ( https://stackoverflow.com/a/23212515/7235 ), le fichier ne peut pas être utilisé après sa fermeture dans Windows. Sous Linux, la réponse de @ Meistro devrait fonctionner.

1
Diego Jancic