What is the best way to test drf serializer validate - django

I think two way how can i test drf serializer validate
following is my serializer validate code
def validate_md5(self, md5):
if len(md5) != 40:
raise serializers.ValidationError("Wrong md5")
return md5
and it is test code
1)
def test_wrong_validate_md5_2(self):
url = reverse('apk-list')
response = self.client.post(url, {'md5':'test'}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
2)
def test_wrong_validate_md5(self):
serializer = ApkSerializer(data=self.apk)
if len(self.apk.get('md5')) != 40:
self.assertEqual(serializer.is_valid(), False)
else:
self.assertEqual(serializer.is_valid(), True)
what is better than another?
or is there best solution?
and ... I practice test-driven coding. is it necessary to write test code as above

The first method actually doesn't test serializer class. It's testing entire 'apk-list' endpoint. Since error may be raised not only in serializer's validate_md5 method, but in any other places even if self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) will be passed you cannot be sure that serializer worked as expected.
So second method is preferabe. But instead of if/else in one test caseyou'd better create two different test case: one for correct data another for incorrect and also you can check if error indead related with md5 field:
def test_wrong_validate_md5(self):
serializer = ApkSerializer(data=self.apk_wrong)
self.assertEqual(serializer.is_valid(), False)
self.assertEqual(set(serializer.errors.keys()), set(['md5']))
def test_correct_validate_md5(self):
serializer = ApkSerializer(data=self.apk_correct)
self.assertEqual(serializer.is_valid(), True)
UPD
It's also possible to use first method, but in this case you need to parse response data. And check if this data contains error with 'md5' key, something like:
def test_wrong_validate_md5_2(self):
url = reverse('apk-list')
response = self.client.post(url, {'md5':'test'}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data,{'md5': Wrong md5')

Related

How to test django non-REST POST endpoint?

I'm having trouble testing one of my endpoints:
#require_http_methods(["POST"])
def store(request):
try:
body_unicode = request.body.decode('utf-8')
body = ast.literal_eval(body_unicode)
new_short_url = body['short_url']
original_url = body['original_url']
check_parameters(new_short_url, original_url)
Url.objects.create(short_url=new_short_url, original_url=original_url)
return HttpResponse('Created', status=201)
except KeyError as error:
return HttpResponse('Missing {}'.format(error.args), status=400)
except (AttributeError, IntegrityError, ValidationError) as error:
return HttpResponse(error.args, status=400)
As you can see, this endpoint only accepts POST requests and when trying to pass data from my tests, it arrives in the request.body, so I implemented my logic to get the data from there:
def test_create_url_ok(self):
creation_data = {
"short_url": "ab",
"original_url": "https://stackoverflow.com/"
}
response = self.client.post(reverse('store'), data=creation_data, content_type="application/json")
self.assertEqual(response.status_code, 201)
This works, but the problem is that when sending requests from my templates, data is not in the request.body, but in the request.POST. How to send data in the request.POST from my tests?
So, the thing is that vanilla django accepts data from request.POST when the content_type='multipart/form-data'. Since I was using 'json' it wasn't working (though it works when using DjangoRestFramework views):
When using default django tests, this content_type is the default, so you don't need to specify any:

DRF Viewset test method

I have added a method to my viewset as follows:
class CustomImageViewSet(viewsets.ModelViewSet):
queryset = CustomImage.objects.all()
serializer_class = CustomImageSerializer
lookup_field = 'id'
#action(detail=True, methods=['get'], url_path='sepia/')
def sepia(self, request, id):
# do something
data = image_to_string(image)
return HttpResponse(data, content_type="image/png", status=status.HTTP_200_OK)
Since it is not a default or overridden request method, I am not sure how can I proceed writing a test for it. Any suggestions?
You're not clear on what the test should test but you can test the response status_code for example like this:
def test_sepia_api():
api_client = APIClient()
response = api_client.get(path="{path_to_your_api}/sepia/")
assert response.status_code == 200
I noticed you were using pytest. I'll assume you've got pytest-django too then (it really does make everything easier). I like using request factory since it's generally faster if you've got authentication needs.
def test_me(self, user, rf):
view = CustomImageViewSet()
request = rf.get("")
request.user = user # If you need authentication
view.request = request
response = view.sepia(request, 123)
assert response.data == BLAH

Custom function for API Django Framework

Working on custom API function that works on creating and updating Ratings
when I try to test run the function on Postman I get the following error:
IntegrityError at /api/movies/1/rate_movie/
UNIQUE constraint failed: API_rating.user_id, API_rating.movie_id
so I do not know if the flaw could be on code or what
here is the code below
#action(detail=True, methods=['POST'])
def rate_movie(self, request, pk=None):
if 'stars' in request.data:
movie = Movie.objects.get(id=pk)
#user = request.user #Can not use now due to lack of auth, login
user = User.objects.get(id=1)
stars = request.data['stars']
try:
rating = Rating.objects.get(user=user.id, movie=movie.id)
rating.stars = stars
rating.save()
serializer = RatingSerializer
response = {'message': 'Rating Updated', 'results': serializer.data}
return Response(response, status=HTTP_200_OK)
except:
rating = Rating.objects.create(user=user, movie=movie, stars=stars)
serializer = RatingSerializer
response = {'message': 'Rating Created', 'results': serializer.data}
return Response(response, status=HTTP_200_OK)
else:
response = {'message':'Stars not selected for rating'}
return Response(response, status=HTTP_400_bad_request)
Lastly here is a picture of the sample test I made when I got the error
Something is failing silently and caught by your generic except
Looks like your Rating model has a unique constraint that only allows a user to rate a movie once, which makes sense. Therefore you can't do Rating.objects.create(user=user, movie=movie, stars=stars) if another rating exists from user to movie.
Instead, you need to update the existing one if it exists, otherwise create a new one. Something like:
Rating.objects.update_or_create(user=user, movie=movie, defaults={stars:stars})
It looks like this is what you're trying to do, but
Don't use generic except unless really necessary! It can hide your errors and lead to bugs. In this case it hides the fact that:
You need to do serializer = RatingSerializer() (with the parens()), or better yet serializer = self.get_serializer() and define a serializer_class class attribute

Django Rest Framework APIClient does not parse query parameters

I am using djangorestframework==3.3.3 and Django==1.9.4
I have a test where I want to check that query parameters processed correctly.
class TestResourceView(APITestCase):
def test_view_process_query_params_correctly(self):
client = APIClient()
client.login(username='<username>', password='password')
response = client.get('/api/v2/resource/1/users/?fields=first_name;last_name')
self.assertEqual(response.status_code, 200)
# .... rest of the test ....
In my view I put print statement just to see if query parameters are parsed properly, but I get empty query dictionary:
class Resource(APIView):
def get(self, request):
query_params = request.query_params
print('Printing query params')
print(query_params)
# .... rest of the code ....
def post(self, request):
query_params = request.query_params
print('Printing query params')
print(query_params)
# .... rest of the code ....
Result in terminal when running tests:
Printing query params
<QueryDict: {}>
In the same time if I test post request like this:
response = client.post('/api/v2/resource/1/users/?fields=first_name;last_name')
i get params incorrectly parsed:
Printing query params
<QueryDict: {'last_name': [''], 'fields': ['first_name']}>
What is the correct way of using APIClient? Or is this still a bug? Because there was already similar issue
For me, the issue was that DRF wants the params in data not args like the Django test client.
This answer helped me:
https://stackoverflow.com/a/45183972/9512437
class AccountTests(APITestCase):
def setUp(self):
self.user = CustomUser.objects.create_user(email="user1#test.com", password="password1", is_staff=True)
self.client = APIClient()
def test_add_name(self):
self.client.force_authenticate(self.user)
url = reverse('customuser-detail', args=(self.user.id,))
data = {'first_name': 'test', 'last_name': 'user'}
response = self.client.put(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
The format of your query parameters is incorrect.
If you want the result to be {'fields': ['last_name', 'first_name']}, then your POST url should be .../users/?fields=first_name&fields=last_name'. You'll probably want to access the params using getlist().
1) Regarding empty <QueryDict: {}> in client.get('/api/v2/resource/1/users/?fields=first_name;last_name') - there was my mistake in my code. I had two tests with the same name, one of which indeed has empty <QueryDict: {}>. So, when running tests, django ran the test with <QueryDict: {}>
2) Regarding incorrect parsing of query params in client.post('/api/v2/resource/1/users/?fields=first_name;last_name'), I found the following discussion. So, django basically follows HTTP standards, where it is said that ; semicolon is reserved character and if using, than the correct way to comprehend it same as &. Here more details

how to access POST data inside tastypie custom Authentication

I'm trying to write custom Authentication in tastypie. Basically, I want to do the authentication using the post parameters and I don't want to use the django auth at all, so my code looks something like:
class MyAuthentication(Authentication):
def is_authenticated(self, request, **kwargs):
if request.method == 'POST':
token = request.POST['token']
key = request.POST['key']
return is_key_valid(token,key)
This is more or less the idea. The problem is that I keep getting the following error:
"error_message": "You cannot access body after reading from request's data stream"
I understand that this is related to the fact that I'm accessing the POST, but I could not figure if there is a way to solve it. Any ideas?
Thanks.
EDIT: Maybe I forgot the mention the most important thing. I'm handling form data using a trick I found in github. My resource derives from multipart resource
class MultipartResource(object):
def deserialize(self, request, data, format=None):
if not format:
format = request.META.get('CONTENT_TYPE', 'application/json')
if format == 'application/x-www-form-urlencoded':
return request.POST
if format.startswith('multipart'):
data = request.POST.copy()
data.update(request.FILES)
return data
return super(MultipartResource, self).deserialize(request, data, format)
The problem is the Content-Type in your request' headers isn't correctly set. [Reference]
Tastypie only recognizes xml, json, yaml and bplist. So when sending the POST request, you need to set Content-Type in the request headers to either one of them (eg., application/json).
EDIT:
It seems like you are trying to send a multipart form with files through
Tastypie.
A little background on Tastypie's file upload support by Issac Kelly for
roadmap 1.0 final (hasn't released yet):
Implement a Base64FileField which accepts base64 encoded files (like the one in issue #42) for PUT/POST, and provides the URL for GET requests. This will be part of the main tastypie repo.
We'd like to encourage other implementations to implement as independent projects. There's several ways to do this, and most of them are slightly finicky, and they all have different drawbacks, We'd like to have other options, and document the pros and cons of each
That means for now at least, Tastypie does not officially support multipart
file upload. However, there are forks in the wild that are supposedly working
well, this is one of
them. I haven't tested it though.
Now let me try to explain why you are encountering that error.
In Tastypie resource.py, line 452:
def dispatch(self, request_type, request, **kwargs):
"""
Handles the common operations (allowed HTTP method, authentication,
throttling, method lookup) surrounding most CRUD interactions.
"""
allowed_methods = getattr(self._meta, "%s_allowed_methods" % request_type, None)
if 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
request.method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
request_method = self.method_check(request, allowed=allowed_methods)
method = getattr(self, "%s_%s" % (request_method, request_type), None)
if method is None:
raise ImmediateHttpResponse(response=http.HttpNotImplemented())
self.is_authenticated(request)
self.is_authorized(request)
self.throttle_check(request)
# All clear. Process the request.
request = convert_post_to_put(request)
response = method(request, **kwargs)
# Add the throttled request.
self.log_throttled_access(request)
# If what comes back isn't a ``HttpResponse``, assume that the
# request was accepted and that some action occurred. This also
# prevents Django from freaking out.
if not isinstance(response, HttpResponse):
return http.HttpNoContent()
return response
convert_post_to_put(request) is called from here. And here is the code for
convert_post_to_put:
# Based off of ``piston.utils.coerce_put_post``. Similarly BSD-licensed.
# And no, the irony is not lost on me.
def convert_post_to_VERB(request, verb):
"""
Force Django to process the VERB.
"""
if request.method == verb:
if hasattr(request, '_post'):
del(request._post)
del(request._files)
try:
request.method = "POST"
request._load_post_and_files()
request.method = verb
except AttributeError:
request.META['REQUEST_METHOD'] = 'POST'
request._load_post_and_files()
request.META['REQUEST_METHOD'] = verb
setattr(request, verb, request.POST)
return request
def convert_post_to_put(request):
return convert_post_to_VERB(request, verb='PUT')
And this method isn't really intended to handled multipart as it has
side-effect of preventing any further accesses to request.body because
_load_post_and_files() method will set _read_started flag to True:
Django request.body and _load_post_and_files():
#property
def body(self):
if not hasattr(self, '_body'):
if self._read_started:
raise Exception("You cannot access body after reading from request's data stream")
try:
self._body = self.read()
except IOError as e:
six.reraise(UnreadablePostError, UnreadablePostError(*e.args), sys.exc_info()[2])
self._stream = BytesIO(self._body)
return self._body
def read(self, *args, **kwargs):
self._read_started = True
return self._stream.read(*args, **kwargs)
def _load_post_and_files(self):
# Populates self._post and self._files
if self.method != 'POST':
self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict()
return
if self._read_started and not hasattr(self, '_body'):
self._mark_post_parse_error()
return
if self.META.get('CONTENT_TYPE', '').startswith('multipart'):
if hasattr(self, '_body'):
# Use already read data
data = BytesIO(self._body)
else:
data = self
try:
self._post, self._files = self.parse_file_upload(self.META, data)
except:
# An error occured while parsing POST data. Since when
# formatting the error the request handler might access
# self.POST, set self._post and self._file to prevent
# attempts to parse POST data again.
# Mark that an error occured. This allows self.__repr__ to
# be explicit about it instead of simply representing an
# empty POST
self._mark_post_parse_error()
raise
else:
self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict()
So, you can (though probably shouldn't) monkey-patch Tastypie's
convert_post_to_VERB() method by setting request._body by calling
request.body and then immediately set _read_started=False so that
_load_post_and_files() will read from _body and won't set
_read_started=True:
def convert_post_to_VERB(request, verb):
"""
Force Django to process the VERB.
"""
if request.method == verb:
if hasattr(request, '_post'):
del(request._post)
del(request._files)
request.body # now request._body is set
request._read_started = False # so it won't cause side effects
try:
request.method = "POST"
request._load_post_and_files()
request.method = verb
except AttributeError:
request.META['REQUEST_METHOD'] = 'POST'
request._load_post_and_files()
request.META['REQUEST_METHOD'] = verb
setattr(request, verb, request.POST)
return request
You say you need custom auth which is fine but please consider using the Authorization header instead. By using POST you force Django to parse the entire payload assuming the data is either urlencoded or multipart form encoded. This effectively makes it impossible to use non-form payloads such as JSON or YAML.
class MyAuthentication(Authentication):
def is_authenticated(self, request, **kwargs):
auth_info = request.META.get('HTTP_AUTHORIZATION')
# ...
This error occurs when you access request.body (or request.raw_post_data if you're still on Django 1.3) a second time or, I believe, if you access it after having accessed the POST, GET, META or COOKIES attributes.
Tastypie will access the request.body (raw_post_data) attribute when processing PUT or PATCH requests.
With this in mind and without knowing more detail, I would:
Check if this only happens for POST/PUTs. If so, then you would have to do some overriding of some tastypie methods or abandon your approach for authentication.
Look for places in your code where you access request.body (raw_post_data)
Look for calls on 3rd party modules (perhaps a middleware) that might try to access body/raw_post_data
Hope this helps!
I've created a utility method that works well for me. Though I am not sure how this affects the underlying parts of Django, it works:
import io
def copy_body(request):
data = getattr(request, '_body', request.body)
request._body = data
request._stream = io.BytesIO(data)
request._files = None
return data
I use it in a middleware to add a JSON attribute to request: https://gist.github.com/antonagestam/9add2d69783287025907