jamesc
jamesc

Reputation: 6328

Test Requests for Django Rest Framework aren't parsable by its own Request class

I'm writing an endpoint to receive and parse GitHub Webhook payloads using Django Rest Framework 3. In order to match the payload specification, I'm writing a payload request factory and testing that it's generating valid requests.

However, the problem comes when trying to test the request generated with DRF's Request class. Here's the smallest failing test I could come up with - the problem is that a request generated with DRF's APIRequestFactory seems to not be parsable by DRF's Request class. Is that expected behaviour?

from rest_framework.request import Request
from rest_framework.parsers import JSONParser
from rest_framework.test import APIRequestFactory, APITestCase


class TestRoundtrip(APITestCase):

    def test_round_trip(self):
        """
        A DRF Request can be loaded into a DRF Request object
        """
        request_factory = APIRequestFactory()
        request = request_factory.post(
            '/',
            data={'hello': 'world'},
            format='json',
        )

        result = Request(request, parsers=(JSONParser,))

        self.assertEqual(result.data['hello'], 'world')

And the stack trace is:

E
======================================================================
ERROR: A DRF Request can be loaded into a DRF Request object
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/james/active/prlint/venv/lib/python3.4/site-packages/rest_framework/request.py", line 380, in __getattribute__
    return getattr(self._request, attr)
AttributeError: 'WSGIRequest' object has no attribute 'data'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/james/active/prlint/prlint/github/tests/test_payload_factories/test_roundtrip.py", line 22, in test_round_trip
    self.assertEqual(result.data['hello'], 'world')
  File "/home/james/active/prlint/venv/lib/python3.4/site-packages/rest_framework/request.py", line 382, in __getattribute__
    six.reraise(info[0], info[1], info[2].tb_next)
  File "/home/james/active/prlint/venv/lib/python3.4/site-packages/django/utils/six.py", line 685, in reraise
    raise value.with_traceback(tb)
  File "/home/james/active/prlint/venv/lib/python3.4/site-packages/rest_framework/request.py", line 186, in data
    self._load_data_and_files()
  File "/home/james/active/prlint/venv/lib/python3.4/site-packages/rest_framework/request.py", line 246, in _load_data_and_files
    self._data, self._files = self._parse()
  File "/home/james/active/prlint/venv/lib/python3.4/site-packages/rest_framework/request.py", line 312, in _parse
    parsed = parser.parse(stream, media_type, self.parser_context)
  File "/home/james/active/prlint/venv/lib/python3.4/site-packages/rest_framework/parsers.py", line 64, in parse
    data = stream.read().decode(encoding)
AttributeError: 'str' object has no attribute 'read'

----------------------------------------------------------------------

I'm obviously doing something stupid - I've messed around with encodings... realised that I needed to pass the parsers list to the Request to avoid the UnsupportedMediaType error, and now I'm stuck here.

Should I do something different? Maybe avoid using APIRequestFactory? Or test my built GitHub requests a different way?


More info

GitHub sends a request out to registered webhooks that has a X-GitHub-Event header and therefore in order to test my webhook DRF code I need to be able to emulate this header at test time.

My path to succeeding with this has been to build a custom Request and load a payload using a factory into it. This is my factory code:

def PayloadRequestFactory():
    """
    Build a Request, configure it to look like a webhook payload from GitHub.
    """
    request_factory = APIRequestFactory()
    request = request_factory.post(url, data=PingPayloadFactory())
    request.META['HTTP_X_GITHUB_EVENT'] = 'ping'
    return request

The issue has arisen because I want to assert that PayloadRequestFactory is generating valid requests for various passed arguments - so I'm trying to parse them and assert their validity but DRF's Request class doesn't seem to be able to achieve this - hence my question with a failing test.

So really my question is - how should I test this PayloadRequestFactory is generating the kind of request that I need?

Upvotes: 1

Views: 831

Answers (2)

jamesc
jamesc

Reputation: 6328

Looking at the tests for APIRequestFactory in DRF, stub views are created and then run through that view - the output is inspected for expected results. Therefore a reasonable, but slightly long solution, is to copy this strategy to assert that the PayloadRequestFactory is building valid requests, before then pointing that at a full view.

The test above becomes:

from django.conf.urls import url
from django.test import TestCase, override_settings
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory


@api_view(['POST'])
def view(request):
    """
    Testing stub view to return Request's data and GitHub event header.
    """
    return Response({
        'header_github_event': request.META.get('HTTP_X_GITHUB_EVENT', ''),
        'request_data': request.data,
    })


urlpatterns = [
    url(r'^view/$', view),
]


@override_settings(ROOT_URLCONF='github.tests.test_payload_factories.test_roundtrip')
class TestRoundtrip(TestCase):

    def test_round_trip(self):
        """
        A DRF Request can be loaded via stub view
        """
        request_factory = APIRequestFactory()
        request = request_factory.post(
            '/view/',
            data={'hello': 'world'},
            format='json',
        )

        result = view(request)

        self.assertEqual(result.data['request_data'], {'hello': 'world'})
        self.assertEqual(result.data['header_github_event'], '')

Which passes :D

Upvotes: 1

pleasedontbelong
pleasedontbelong

Reputation: 20102

"Yo dawg, I heard you like Request, cos' you put a Request inside a Request" XD

I'd do it like this:

from rest_framework.test import APIClient

client = APIClient()
response = client.post('/', {'github': 'payload'}, format='json')
self.assertEqual(response.data, {'github': 'payload'})
# ...or assert something was called, etc.

Hope this helps

Upvotes: 1

Related Questions