using APITestCase with django-rest-framework - python

I followed this code:
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APITestCase
class AccountTests(APITestCase):
def test_create_account(self):
"""
Ensure we can create a new account object.
"""
url = reverse('account-list')
data = {'name': 'DabApps'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data, data)
Found in the django-rest-framewok docs here:
DRF API-guide: Testing example
I created a single Model with a single field name, and I am still getting a "bad request 400 error". The view and reverse name is also set up correctly, and I have manually tested viewing the URL with success. I don't have Authentication enabled
And can't figure out if I am missing a step?
Does anyone have a working example of a django-rest-framework APITestCase create model object test code snippet?

This GIT repo has several working examples, which I was able to follow and get APITestCase working:
django-rest-framework-oauth2-provider-example/apps/users/tests.py

It could be a JSON decode error.
In the line self.client.post(url, data, format='json') use json.dumps(data) and try.

Related

AttributeError: 'UserViewSet' object has no attribute 'user'

I've currently got an API endpoint that works as I would expect it when I send a request to it manually via Postman. The way I do so is running:
GET /user/?fromid=1&toid=100
I am now trying to setup unit tests using factory-boy but I'm not able to get it to work due to an error.
So far, the important files with regards to the unit tests that I have are:
urls.py
from django.urls import include, path
from rest_framework import routers
from rest_framework.schemas import get_schema_view
import views
router = routers.DefaultRouter()
router.register(r"user", views.UserViewSet, basename='user')
urlpatterns = [
path('user', views.UserViewSet.as_view({'get': 'user'}), name='user')
]
test_user.py
import factory
from django.test import Client, TestCase
from django.urls import reverse
from factory import DjangoModelFactory, Faker
from models.user import User
class UserFactory(DjangoModelFactory):
class Meta:
model = User
class UserViewSetTest(TestCase):
def setUp(self):
client = Client()
def test_user_list(self):
response = self.client.get(reverse('user'), format='json')
self.assertEqual(response.status_code, 200)
When running pytestusing the above, I get an error:
AttributeError: 'UserViewSet' object has no attribute 'user'
I have a feeling that the parameters in the URL aren't allowing me to call it as is from my unit test, but I'm not sure.
As I mentioned, I've been able to send a request via Postman to the API so I'm not sure it's useful to show my complete code (i.e. the ViewSet, models and serializers). For what it's worth, the viewset currently just returns a list of users in Json format.
EDIT:
I've been able to recreate the error manually on Postman. If I create a request without specifying the URL parameters, like the following:
GET /user
I get the same error manually
AttributeError: 'UserViewSet' object has no attribute 'user'
As a result, I have tried to update my reverse command within the unit test to include the URL parameters, as such:
response = self.client.get(reverse('user'), kwargs={'fromid':1, 'toid': 100}, format='json')
But I'm still getting the same original error.
EDIT2:
I think part of the problem is with the way I'm calling reverse. The kwargs parameter wasn't in the actual reverse function. I've modified it to be as such:
response = self.client.get(reverse('user', kwargs={'fromid':1, 'toid': 100}), format='json')
Now my error is to do with the url path (I believe) as now I'm getting a new error which is:
Reverse for 'user' with keyword arguments '{'fromid':1, 'toid': 100}'
not found. 1 pattern(s) tried: ['/user$']
My (hopefully final) question is how do I specify multiple parameters in my path to accept the kwargs? My endpoint is currently accepted to parameters (fromid and toid) in the URL, so I need to specify that in the path elements below. How can I do so?
router.register(r"user", views.UserViewSet, basename='user')
urlpatterns = [
path('user', views.UserViewSet.as_view({'get': 'user'}), name='user')
]
drf routers will use a name like
basename+'-list'
for generated url with list actions. so your code should be:
response = self.client.get(reverse('user-list', kwargs={'fromid':1, 'toid': 100}), format='json')

What's the most elegant way to convert requests' response to DRF response in Django?

Consider the following flow:
public client ----> DRF API on Service A ------> DRF API on Service B
Some of the DRF API on Service A merely proxying to Service B, so in the particular API on Service A looks like this:
class SomeServiceAPI(APIView):
def get(request):
resp = requests.get('http://service-b.com/api/...')
return Response(resp.json())
While this works on normal status, but it has a few issues:
It doesn't proxy the actual status code from service b.
Unnecessary round-trip of json serialization within Response()
If service b returns a non-json error, service does not return actual error from service b.
The question is, is there a better way to do it? I had a look at Django Rest Framework Proxy project, but I am not entirely sure if it actually suits my use case here.
You can solve the status code part by modifying your Response:
return Response(resp.json(), status=resp.status_code)
For the second part though, this is the essence of Proxying... (True, sometimes you want to manipulate the request and/or the response in the middleman of the proxy, but what you do is the essence).
Notes:
The DRF Proxy that you are suggesting seems to do the job just
fine, without the need for you to write a specific view just for the
roundtrip.
There exist another tool, DRF Reverse Proxy which is a DRF port of Django Revproxy and you may want to consider.
The general idea of both of the above is that you create a URL path specifically to Proxy the path to another API:
DRF Proxy:
Add your proxy to settings.py:
REST_PROXY = {
'HOST': 'http://service-b.com/api/'
}
In urls.py:
url(
r'^somewere_in_a/$',
ProxyView.as_view(source='somewere_in_b/'),
name='a_name'
)
DRF Reverse Proxy:
Pretty much similar with the above, without the settings part:
url(
r'^(?P<path>.*)$',
ProxyView.as_view(upstream='http://service-b.com/api/somewere_in_b/'),
name='a_name'
)
Opinion: the DRF Proxy seems more solid...
I had a look at both existing packages mentioned in John's answer but they don't seem to perfectly suit in my use case, so I have created a simple wrapper to proxy the requests' response to DRF response.
# encoding: utf-8
from __future__ import unicode_literals
from __future__ import absolute_import
from rest_framework.response import Response
from requests.models import Response as RResponse
class InCompatibleError(Exception):
pass
class DRFResponseWrapper(Response):
"""
Wraps the requests' response
"""
def __init__(self, data, *args, **kwargs):
if not isinstance(data, RResponse):
raise InCompatibleError
status = data.status_code
content_type = data.headers.get('content_type')
try:
content = data.json()
except:
content = data.content
super(DRFResponseWrapper, self).__init__(content, status=status, content_type=content_type)
And use as below:
resp = requests.get(
'{}://{}/api/v5/business/'.format(settings.SEARCH_HOST_SCHEMA, settings.SEARCH_HOST),
params=request.query_params
)
return DRFResponseWrapper(resp)

How do test in Django

I'm trying to do my first tests on Django and I don't know do it or after reading the docs (where it explains a very easy test) I still don't know how do it.
I'm trying to do a test that goes to "login" url and makes the login, and after a succesfull login redirects to the authorized page.
from unittest import TestCase
from django.test.client import Client
class Test(TestCase):
def testLogin(self):
client = Client()
headers = {'X-OpenAM-Username': 'user', 'X-OpenAM-Password': 'password', 'Content-Type': 'application/json'}
data = {}
response = client.post('/login/', headers=headers, data=data, secure=False)
assert(response.status_code == 200)
And the test success, but I don't know if it's beacuse the 200 of loading "/login/" or because the test do the login and after redirect get the 200 code.
How can I check on the test that after the login the url redirected it's the correct? There is a plugin or something that helps with the test? Or where I can find a good tutorial to test my views and the model?
Thanks and regards.
To properly test redirects, use the follow parameter
If you set follow to True the client will follow any redirects and a
redirect_chain attribute will be set in the response object containing
tuples of the intermediate urls and status codes.
Then your code is as simple as
from django.test import TestCase
class Test(TestCase):
def test_login(self):
client = Client()
headers = {'X-OpenAM-Username': 'user', 'X-OpenAM-Password': 'password', 'Content-Type': 'application/json'}
data = {}
response = client.post('/login/', headers=headers, data=data, secure=False)
self.assertRedirects(response,'/destination/',302,200)
Note that it's self.assertRedirects rather than assert or assertRedirects
Also note that the above test will most likely fail because you are posting an empty dictionary as the form data. Django form views do not redirect when the form is invalid and an empty form will probably be invalid here.
Django have plenty of tools for testing. For this task, you should use test case class from Django, for example django.test.TestCase.
Then you can use method assertRedirects() and it will check where you've been redirected and with which code. You can find any info you need here.
I've tried to write the code for your task:
from django.test import TestCase
class Test(TestCase):
def test_login(self):
data = {'X-OpenAM-Username': 'user', 'X-OpenAM-Password': 'password'}
response = client.post('/login/', data=data, content_type='application/json', secure=false)
assertRedirects(response, '/expected_url/', 200)
Then you can use python3 manage.py test to run all tests.

How to get api format response from Django

I followed "Writing regular Django views..." from the official documentation of Django Rest framework and got this kind of code:
#views.py file
#imports go here
class JSONResponse(HttpResponse):
"""
An HttpResponse that renders its content into JSON.
"""
def __init__(self, data, **kwargs):
content = JSONRenderer().render(data)
kwargs['content_type'] = 'application/json'
super(JSONResponse, self).__init__(content, **kwargs)
def items(request):
output = [{"a":"1","b":"2"},{"c":"3","d":"4"}]
return JSONResponse(output)
And it works well. When a user goes to /items/ page, he or she sees a nicely looking json-formated data [{"a":"1","b":"2"},{"c":"3","d":"4"}]. But, how can I get (code?) api-formated data, or check if a user requested ?format=api then render in api format manner and if not, then in json format. By api-formated data I mean this kind of view
Try using the #api_view() decorator as described here. And make sure you use the built in Response instead of JSONResponse.
Your view should then look something like this:
from rest_framework.decorators import api_view
...
#api_view()
def items(request):
output = [{"a":"1","b":"2"},{"c":"3","d":"4"}]
return Response(output)
In the case of getting the error
Cannot apply DjangoModelPermissions on a view that does not have model or queryset property
Remove DjangoModelPermissions from your rest framework permissions settings in you settings.py

Django, Testing Redirects to External Pages

I have a routing rule in my Django app for downloads that redirect to a external CDN. I am now writing tests for my app, and I want to test that the route does successfully redirect to the configured url in my Django settings. Here is a simplified example that should help explain what I'm trying to do:
from django.test import SimpleTestCase
from django.test.client import Client
from django.conf import settings
class MyTestCase(SimpleTestCase):
def setUp(self):
self.client = Client()
def test_download(self):
response = self.client.get('/download/mac/')
self.assertRedirects(response, settings.URLS.get('mac'))
Now this doesn't work, the redirect gets a 404 even though when I print settings.DOWNLOAD_URL in this method it is correct, and a copy/paste into the browser proves it works. I started to look into why it wasn't working, and I noticed this in the Django source code:
Note that assertRedirects won't work for external links since it uses
TestClient to do a request.
So then, how does one test these redirects? I'm not looking for anything super fancy, what I expect to check is the response's status_code and location. I saw that response has a follow parameter, and tried something like this, but it still didn't work:
def test_download(self):
response = self.client.get('/download/mac/', follow=True)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], settings.URLS.get('mac')
It was requested that I include the relevant parts from my urls.py and views.py, here they are:
# urls.py
from django.conf.urls import patterns, url
urlpatterns = patterns('myapp.views',
url(r'^download/(?P<platform>\w+)/$', 'download_app', name='download'),
)
#views.py
from django.conf import settings
from django.shortcuts import redirect
from django.http import Http404
def download_app(request, platform):
if platform in settings.URLS:
return redirect( settings.URLS.get(platform) )
else:
raise Http404
Any help in solving this would be much appreciated. Thanks in advance.
Probably the original poster has solved his problems and moved on long ago, but the solution here is to not specify follow=True as he did in the alternative proposal that didn't work either.
The following will simply check that the view being tested redirects as expected, without relying on the external resources.
def test_download(self):
response = self.client.get('/download/mac')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], 'http://external/resource')
In Django 1.7 assertRedirect got a new parameter fetch_redirect_response which can be set to False to get the same effect, but I haven't tried it myself.

Categories