Unable to unittest Django APIviews with TestCase library? - django-views

This is a very project specific question, but I can't figure out what I'm doing wrong. I have a view class that allows users to perform GET and POST requests to the project database. I am able to successfully GET/POST using curl, but when I write my unittests for this, I can't get it to pass the test, and I don't fully understand why.
This command works:
curl http://127.0.0.1:8000/list/categories/ -d '{"id":"00001", "name":"some info"}'
view.py:
class CategoryList(APIView):
def get(self, request, format=None):
categories = Category.objects.all()
serializer = CategorySerializer(categories, many=True)
context = {
'categories': categories
}
return render(request, 'myapp/categories.html', context)
def post(self, request, format=None):
print('=======inside post=========')
data = JSONParser().parse(request)
serializer = CategorySerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
urls.py:
urlpatterns = [
# eg: /list/categories/
url(r'^categories/$', CategoryList.as_view()),
]
Here's where my test fails.
tests.py:
class CategoryViewTests(TestCase):
def setUp(self):
""" Mock model data """
cat1 = Category.objects.create(id=55555, name="Shoes")
cat2 = Category.objects.create(id=12345, name="Wallets")
def test_new_unique_category(self):
"""
Create a new category obj
"""
c = Client()
print(str(Category.objects.all()))
new_cat_data = {'id':'54211', 'name':'Soccer Balls'}
response = c.post('/list/categories', new_cat_data)
print(str(Category.objects.all()))
self.assertEqual(response.status_code, 201)
Terminal output:
test_new_unique_category (inferbrand.tests.CategoryViewTests) ...
<QuerySet [<Category: Test Men's Shoes>, <Category: Test Wallets>]>
<QuerySet [<Category: Test Men's Shoes>, <Category: Test Wallets>]>
FAIL
======================================================================
FAIL: test_new_unique_category (inferbrand.tests.CategoryViewTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "path/to/proj/tests.py", line 104, in test_new_unique_category
self.assertEqual(response.status_code, 201)
AssertionError: 301 != 201
What am I doing wrong?? Any help would be greatly appreciated.

The curl command works because you have a trailing slash in the URL, but in your unit test the URL does not have a trailing slash, so Django is redirecting it to a URL with a trailing slash with status code 301, which is the default behavior unless you set APPEND_SLASH to False in settings.py.
Please refer to https://docs.djangoproject.com/en/2.0/ref/settings/#append-slash for more details.

Okay. I think the root cause of this is that serializers aren't able to process the incoming request from django unittests correctly. I ended up removing all the serializers, and instead replaced them like so:
class CategoryList(APIView):
def get(self, request, format=None):
categories = Category.objects.all()
context = {
'categories': categories
}
return render(
request,
'myapp/categories.html',
context
)
def post(self, request, format=None):
data = request.POST
new_cat = Category(id=data['id'],
name=data['name'])
new_cat.save()
return HttpResponseRedirect(reverse('myapp:category_detail', args=[ new_cat.id ]))
new curl format:
curl http://127.0.0.1:8000/list/categories/ -d 'id=1299988&name=Tennis123'
The post HttpResponseRedirect still isn't working correctly, (returns status 301), but POST/GET now works using curl and unittesting which was the main issue I was having.

Related

Django Restapi - How to test if POST saves data in database?

So I found this question:
Django Rest Framework testing save POST request data
And I understood that the data created with POST request should be accessible as long as the function is running. So I made the whole test in one function:
class PostMovieAPITest(APITestCase):
def test_correct_request(self):
title = 'Snatch'
response = self.client.post('/movies/', data={'title': title}, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
movies = Movie.objects.all()
self.assertTrue(Movie.objects.get(title=title))
The problem is, Movie.objects.all() is empty, even though I send a CREATE/POST request in the same function. The API works fine when I do `manage.py runserver' and test it in browser. But how can write a proper test to check if data is actually saved in database?
urls.py:
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from api import views
router = routers.DefaultRouter()
router.register(r'movies', views.MovieViewSet)
urlpatterns = [
path('', include(router.urls)),
path('admin/', admin.site.urls),
]
views.py:
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def create(self, request, *args, **kwargs):
title = request.data.get('title')
if not title:
return Response({'Error': "Body should be {'title':'The title of the Movie'}"}, status=status.HTTP_400_BAD_REQUEST)
data = get_data_from_omdb(title)
if len(data) == 0:
return Response({"Error": "Title does not exist in OMDB database"}, status=status.HTTP_400_BAD_REQUEST)
serializer = MovieSerializer(data=data, context={'request': request})
if serializer.is_valid(raise_exception=False):
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Found the problem thanks to Ozgur Akcali's comment. I couldn't get the movie because Movie object was created based on data from external API, including the title.
"Hire me!" -> "Hire Me!"
Sometimes scripting makes me want to kill. Thanks a lot and sorry for wasting your time.
def test_correct_request(self):
title = 'Hire Me!' # was: 'Hire me!'
response = self.client.post('/movies/', data={'title': title}, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(Movie.objects.get(title=title))

allow post requests in django REST framework

I am creating a simple rest api using django REST framework. I have successfully got the response by sending GET request to the api but since I want to send POST request, the django rest framework doesn't allow POST request by default.
As in image(below) only GET,HEAD, OPTIONS are allowed but not the POST request
The GET and POST methods inside of views.py
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from profiles_api import serializers
from rest_framework import status
# Create your views here.
class HelloApiView(APIView):
"""Test APIView"""
#Here we are telling django that the serializer class for this apiViewClass is serializer.HelloSerializer class
serializer_class = serializers.HelloSerializer
def get(self, request, format=None):
"""Retruns a list of APIViews features."""
an_apiview = [
'Uses HTTP methods as fucntion (get, post, patch, put, delete)',
'It is similar to a traditional Django view',
'Gives you the most of the control over your logic',
'Is mapped manually to URLs'
]
#The response must be as dictionary which will be shown in json as response
return Response({'message': 'Hello!', 'an_apiview': an_apiview})
def post(self,request):
"""Create a hello message with our name"""
serializer = serializer.HelloSerializer(data=request.data)
if serializer.is_valid():
name = serializer.data.get('name')
message = 'Hello! {0}'.format(name)
return Response({'message':message})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
How to allow POST requests in django REST framework?
The problem with the code was, you have added the def post() after the return statement.
To solve, just correct your indentation level as below,
class HelloApiView(APIView):
def get(self, request, format=None):
return Response()
def post(self, request):
return Response()

django-rest testing views with custom authentication

I try to test view that has custom authentication, mainly because the main auth is based on external login-logout system, utilizing Redis as db for storing sessions.
Auth class is checking session id from the request, whether it is the same as in Redis - if yes, succeed.
My custom authentication.py looks like:
from django.utils.six import BytesIO
from rest_framework import authentication
from rest_framework import exceptions
from rest_framework.parsers import JSONParser
import redis
class RedisAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
print(request.META)
token = request.META['HTTP_X_AUTH_TOKEN']
redis_host = "REDIS_IP_ADRESS"
redis_db = redis.StrictRedis(host=redis_host)
user_data = redis_db.get("user_feature:{}".format(token))
if user_data is None:
raise exceptions.AuthenticationFailed('No such user or session expired')
try:
stream = BytesIO(user_data) # Decode byte type
data = JSONParser(stream) # Parse bytes class and return dict
current_user_id = data['currentUserId']
request.session['user_id'] = current_user_id
except Exception as e:
print(e)
return (user_data, None)
and my views.py looks like:
#api_view(['GET', 'POST'])
#authentication_classes((RedisAuthentication, ))
def task_list(request):
if request.method == 'GET':
paginator = PageNumberPagination()
task_list = Task.objects.all()
result_page = paginator.paginate_queryset(task_list, request)
serializer = TaskSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
elif request.method == 'POST':
serializer = PostTaskSerializer(data=request.data)
if serializer.is_valid():
user_id = request.session.get('user_id')
serializer.save(owner_id=user_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Manual tests pass, but my current pytests failed after adding authentication.py, and have no clue how I can fix it properly - tried with forcing auth, but no succeed.
I'm thinking that one of solution will be use fakeredis for simulate real redis. Question is, how that kind of test should looks like?
Example of test you could find here:
#pytest.mark.webtest
class TestListView(TestCase):
def setUp(self):
self.client = APIClient()
def test_view_url_accessible_by_name(self):
response = self.client.get(
reverse('task_list')
)
assert response.status_code == status.HTTP_200_OK
#pytest.mark.webtest
class TestCreateTask(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='admin', email='xx', password='xx')
def test_create(self):
data = {some_data}
self.client.login(username='xx', password='xx')
response = self.client.post(
reverse('task_list'),
data,
format='json')
assert response.status_code == status.HTTP_201_CREATED
self.client.logout()
Thanks in advance for any help!
I managed to mock whole redis auth using mock.patch decorator - https://docs.python.org/3.5/library/unittest.mock-examples.html#patch-decorators.
When you put import patch to mock.patch decorator, do not insert absolute module path where redis code is stored, but insert the path where redis code was imported as a module and used.
My test looks like that now:
#mock.patch('api.views.RedisAuthentication.authenticate')
def test_view_url_accessible_by_name(self, mock_redis_auth):
data = {"foo": 1, "currentUserId": 2, "bar": 3}
mock_redis_auth.return_value = (data, None)
response = self.client.get(
reverse('task_list'),
HTTP_X_AUTH_TOKEN='foo'
)
assert response.status_code == status.HTTP_200_OK

Test redirect using #user_passes_test decorator

My view file has:
def is_authorised(user):
return user.groups.filter(name='bookkeepers').exists()
#login_required
def unauthorised(request):
context = {'user': request.user}
return render(request, 'order_book/unauthorised.html', context)
#login_required
#user_passes_test(is_authorised,
login_url='/order_book/unauthorised/',
redirect_field_name=None)
def book(request):
return render(request, 'order_book/book.html', {})
I want to write a test asserting that a logged in user who is not authorised does get redirected correctly, so far I have this:
class RestrictedViewsTest(TestCase):
#classmethod
def setUpTestData(cls): # noqa
"""Set up data for the whole TestCase."""
User.objects.create_user(username='JaneDoe',
email='jane.doe#example.com',
password='s3kr3t')
def setUp(self):
auth = self.client.login(username='JaneDoe', password='s3kr3t')
self.assertTrue(auth)
def test_book(self):
response = self.client.get('/order_book/book')
self.assertEqual(response.status_code, 301, response)
self.assertTrue(isinstance(response, HttpResponsePermanentRedirect))
def tearDown(self):
self.client.logout()
This works fine as it is but I cannot fathom where to get the redirected to url. Trying to get response['Location'] gives me '/order_book/book' which is not right.
What am I doing wrong?
You can use the assertRedirects method.
def test_book(self):
response = self.client.get('/order_book/book/')
self.assertRedirects(response, '/order_book/unauthorised/', status_code=302, target_status_code=200)

django rest framework PUT returns 404 instead of creating an object

I want to be able to create or update an object using the same request. The operation should be idempotent.
Sending a PUT request to DRF work as expected if the object exists but if the object doesn't exists I get a 404 instead of creating it.
models.py:
class Btilog(models.Model):
md5hash = models.CharField(primary_key=True, max_length=32)
vteip = models.ForeignKey('vte.VTE')
timestamp = models.DateTimeField(blank=False)
source = models.TextField()
code = models.CharField(max_length=10, blank=False)
msg = models.TextField(blank=False)
api.py:
class BtilogSerializer(serializers.ModelSerializer):
class Meta:
model = models.Btilog
class BtilogVSet(viewsets.ModelViewSet):
queryset = models.Btilog.objects.all()
serializer_class = BtilogSerializer
permission_classes = (permissions.AllowAny,)
urls.py:
...
router = routers.DefaultRouter()
router.register(r'btilog', api.BtilogVSet)
urlpatterns = patterns('',
url(r'^api/', include(router.urls)),
...
)
Failing request
http --form PUT http://192.168.10.121:8888/logger/api/btilog/60c6b9e99c43c0bf4d8bc22d671169b1/ vteip='172.25.128.85' 'code'='Test' 'md5hash'='60c6b9e99c43c0bf4d8bc22d671169b1' 'timestamp'='2015-05-31T13:34:01' msg='Test' source='Test'
HTTP/1.0 404 NOT FOUND
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Type: application/json
Date: Mon, 09 Feb 2015 15:16:47 GMT
Server: WSGIServer/0.1 Python/2.7.6
Vary: Accept, Cookie
{
"detail": "Not found"
}
As described here: http://restcookbook.com/HTTP%20Methods/put-vs-post/ the correct behaviour of put should be to create the object if it doesn't exists.
The same error occurs using The Browsable API Tool from DRF to make the request. Is the behaviour of DRF also alike? What I'm doing wrong?
Well, maybe you should try to overwrite update method inside your modelviewset, which handle the PUT http method:
class BtilogVSet(viewsets.ModelViewSet):
queryset = models.Btilog.objects.all()
serializer_class = BtilogSerializer
permission_classes = (permissions.AllowAny,)
def update(self, request, *args, **kwargs):
try:
instance = Btilog.objects.get(pk=kwargs['pk'])
serializer = serializers.BtilogSerializer(instance=instance,data=request.data)
if serializer.is_valid():
btilog=serializer.save()
return Response(serializer.data,status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Btilog.DoesNotExist:
serializer = serializers.BtilogSerializer(data=request.data)
if serializer.is_valid():
btilog=serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Yes, in general with DRF you will create an object using a POST and update an object using PUT. Http PUTs should be idempotent, whereas POSTs are not necessarily so -and POSTs will never be idemponent if you have an automatically created field like a timestamp in the created object.
To get the effect the OP wishes above you need to place the create functionality of the POST http method into the PUT method.
The issue is that PUTs are mapped to the "update" action only (when using DefaultRouter in urls.py) and the update action does expect the object to exist. So you have to slightly amend the update function (from rest_framework.mixins.UpdateModelMixin) to handle creating objects that do not currently exist.
I am arriving somewhat late to this question so perhaps this may assist someone working on later versions of Django Rest Framework, my version is v3.9.4 .
if you are using a ModelViewSet, then I would suggest inserting the following update function within your views.py file, within your class viewset :
It is simply a blend of the DRF´s existing update and create mixins -and you get some extra checking thrown in with those mixins (permission checking, get_serializer_class etc.) Plus it is a bit more portable as it does not contain references to models, - well done to DRF developers (yet again). You will need to import Http404 and ValidationError as shown below.
from django.http import Http404
from rest_framework import status
from rest_framework.exceptions import ValidationError
class BtilogVSet(viewsets.ModelViewSet):
queryset = models.Btilog.objects.all()
serializer_class = BtilogSerializer
permission_classes = (permissions.AllowAny,)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
try:
instance = self.get_object() #throws a Http404 if instance not found
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
except Http404:
#create the object if it has not been found
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) # will throw ValidationError
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
except ValidationError: # typically serializer is not valid
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except:
raise
Note, PATCH is also mapped to the update() function indirectly via the function
partial_update().You don't need to include the partial_update code below, it is supplied by default from the file rest_framework.mixins.UpdateModelMixin, which is a mixin to the ModelViewSet. I show it here for purely illustrative purposes, you do not need to do anything to it.
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)