Django Rest Mocking a Get request - django

I have a viewset for services and an action method defined as:
#action(
methods=['patch'], detail=True, url_name='deploy', url_path='deploy')
def deploy(self, request, pk=None):
"""
An extra action for processing requests
"""
# Gets current instance
instance = self.get_object()
[...]
# Sends GET request
get = APIServiceRequestRouter()
item_list = get.get_data_request()
My get_data_request() method will send a get request to a url, which will return a json response. But of course, this goes out to a third-party api, which I want to mock in my unit test.
I have this unit test for my deploy action endpoint on my services viewset, which currently only test the send of a patch payload to the endpoint. So I need to add a mocked get response as well:
def test_valid_deploy(self) -> None:
"""Test an valid deploy"""
mock_request
response = self.client.patch(
reverse('services-deploy', kwargs={'pk': self.service2.pk}),
data=json.dumps(self.deploy_payload),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_206_PARTIAL_CONTENT)
What I'm not sure is how to add a mocked get response into this unit test. The error I'm getting when I run my unit tests is that in my test_valid_deploy is that the get_data_request() method I have this:
response = http_get_request.delay(api_url)
response_obj = response.get()
item_ids = response_obj['item_id']
But of course response_obj is empty or of NoneType and therefore the ['item_id'] key does not exist, I get a TypeError: 'NoneType' object is not subscriptable error.
My thoughts then turned to mocking the get to populate response_obj with relevant data so that my test passes.
Any thoughts or help would be very much appreciated.

Try mocking the return value of get_data_request instead?
#mock.patch('path.to.APIServiceRequestRouter.get_data_request')
def test_valid_deploy(self, mocked_get_data_request):
mocked_get_data_request.return_value = {"key": "value"} # your mocked object here
# rest of the test...

Related

How to allow throttle only if the response was successful?

I'm trying to make throttling on OTP authentication so user can only send one message every minute
class BurstRateThrottle(AnonRateThrottle, UserRateThrottle):
scope = 'burst'
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES':
('rest_framework_simplejwt.authentication.JWTAuthentication',),
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'burst': '1/min',
'anon': '200/min',
'user': '250/min',
},
#api_view(['POST'])
#permission_classes([AllowAny])
#throttle_classes([BurstRateThrottle])
def login_send_token(request):
...
The problem with this is that the api gets throttled even when the phone number is wrong so I'm trying to only throttle when the OTP message is send or when the response is 200 or 201
Is there any way to access the response status code in allow_request method?
or to manually execute the throttle from the function that call twilio api?
The allow_request method is run before the method, so you only have the result of the API request after the framework decides whether the request can be run or not.
One way to do this might be to make the request to the API within the allow_request method and only add the request to the history of requests if the API request succeeded.
That does mix up where you would expect to find that code though, so an alternative would be to build the custom throttling directly into your login_send_token method.
I solved this by converting login_send_token to a class based view
class LoginSendToken(APIView):
permission_classes = [AllowAny]
throttle_classes = [BurstRateThrottle]
after that I overrode initial method and commented out the self.check_throttles(request) calling
def initial(self, request, *args, **kwargs):
"""
Runs anything that needs to occur prior to calling the method handler.
"""
self.format_kwarg = self.get_format_suffix(**kwargs)
# Perform content negotiation and store the accepted info on the request
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
# Determine the API version, if versioning is in use.
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
# Ensure that the incoming request is permitted
self.perform_authentication(request)
self.check_permissions(request)
# self.check_throttles(request)
and called check_throttles from my post method after serializer validation
now it only checks throttling if the serializer was valid

Django REST Framework - Set request in serializer test for ApiClient

I already read: Django REST Framework - Set request in serializer test?. And it doesn't work for me! Because I'm using APIClient and not RequestFactory like him.
I built a web app where the back-end is implemented using the Django REST Framework. Now I'm writing unit tests and I have come across a problem in testing my serializer methods. Here is one example of a serializer method I'm struggling with:
def get_can_edit(self, obj):
request = self.context['request']
user = User.objects.get(username=request.user)
return user == obj.admin
When trying to call this from the test, first I declare an instance of the serializer:
But now I need self.serializer to have the correct request when get_can_edit does self.context.get('request'). I've created a fake request with the correct information using APIClient:
self.client = APIClient()
self.client.force_authenticate(user)
conference = a_fake_conference
res = self.client.get('conference:conference-detail'. args=[conference.id])
serializer = ConferenceSerializer(conference, context={WHAT_IS_REQUEST?})
# I'm using a dict as context but the request gave me an error: context={'request': { 'user': user }}
sert.assertEqual(res.data, serializer.data)
Now I am stuck because I am unsure how to add request1 to serializer such that request = self.context['request'] will return
Thanks.
Use wsgi_request (source code-Django) of APIClient to get the WSGI request object.
self.client = APIClient()
self.client.force_authenticate(user)
res = self.client.get('conference:conference-detail'. args=[conference.id]
# make sure to call the `get(...)` method before accessing `wsgi_request` attribute
request_object = res.wsgi_request
Disclaimer: Not sure whether this is a DRF way to get the request object.
You're testing two things with each other here, so that's sort of part of the problem. Typically, you'd use self.client.get(...) as an integration test, that is, a test that ensures that everything from request to response is working as expected. For tests like these, you wouldn't use a serialiser, because then you're using your application code (your serialiser) to test itself.
If you're writing an integration test, you should be testing the raw response with something like:
from django.test import TestCase, RequestFactory
class MyUnitTestCase(TestCase):
def test_the_whole_stack(self):
response = self.client.get(
"conference:conference-detail", args=[conference.id]
)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json()["some-key-you-expect"],
"Some value you expect"
)
Note that you also don't have to invoke APIClient() directly. It's there by default.
For a unit test, like the kind of test you'd write to make sure your serialiser is working properly, you don't need or want to be poking around with a WSGIRequest object. Instead, Django supplies a RequestFactory for just this case:
from django.test import TestCase, RequestFactory
class MyUnitTestCase(TestCase):
def test_my_serialiser(self):
serializer = ConferenceSerializer(
conference,
context={"request": RequestFactory().get("/")}
)
self.assertEqual(
serialiser.data["some-key-you-expect"],
"Some value you expect"
)

Django Rest Framework test fails: Expected view to be called with a URL keyword argument

I am running tests on UserDetail view written using Django Rest framework.
urls.py
url(r'^api/users/', include('calorie_counter.users.urls', namespace='users')),
users/urls.py
url(r'^(?P<pk>[0-9]+)$', views.UserDetail.as_view(), name='user-detail'),
test_api.py
BaseAPITestCase(APITestCase):
def setUp(self):
self.superuser = User.objects.create_superuser('admin', 'admin#test.com', 'johnpassword')
self.client.login(username='john', password='johnpassword')
self.user1 = User.objects.create(username="user1", password="pass", email="user1#test.com")
class ReadUserTest(BaseAPITestCase):
# check read permissions
def test_user_can_read_self_detail(self):
url = '/api/users/'+str(self.user1.id)
factory = APIRequestFactory()
request = factory.get(url)
force_authenticate(request, self.user1)
response = (UserDetail.as_view())(request)
self.assertEqual(response.status_code, status.HTTP_200_OK)
However, running this test, gives me following error. The 'pk' agrument is not getting passed to the UserDetail view.
AssertionError: Expected view UserDetail to be called with a URL keyword argument named "pk". Fix your URL conf, or set the .lookup_field attribute on the view correctly.
How do I test views with URL arguments?
UPDATE:
Now using APIClient instead of factory..
def test_user_can_read_self_detail(self):
client = APIClient()
client.login(username='user1', password='pass')
# response = self.client.get('/api/users/', {'pk': self.user.id})
response = client.get('/api/users/' + str(self.user1.id))
self.assertEqual(response.status_code, status.HTTP_200_OK)
Now I am getting following error:
AttributeError: 'AnonymousUser' object has no attribute 'is_manager'
where is manager is an attribute of my custom user model. I guess there is some problem with client authentication. I have session authentication enabled. Still getting this error.
UPDATE: My login wasn't working for APICLient because I was creating user using User.objects.create instead of User.objects.create_user. Changing that fixed the problem. :)
I don't think you need all of this set up you're doing, it's unusual to need to instantiate view classes yourself – perhaps you'll have more success leveraging the test client with something like:
def test_user_can_read_self_detail(self):
url = reverse('api:user-detail', kwargs={'pk': self.user.id})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
If you're having issues with authentication, which I suspect may have been what lead you here, you may want to try:
self.client.login(username='example', password='changeme')
or
import base64
self.client.defaults['HTTP_AUTHORIZATION'] = 'Basic ' + base64.b64encode('example:changeme')
I've used both in the past to test authenticated API endpoints.

Django Rest Framework testing save POST request data

I'm writing some tests for my Django Rest Framework and trying to keep them as simple as possible. Before, I was creating objects using factory boy in order to have saved objects available for GET requests.
Why are my POST requests in the tests not creating an actual object in my test database? Everything works fine using the actual API, but I can't get the POST in the tests to save the object to make it available for GET requests. Is there something I'm missing?
from rest_framework import status
from rest_framework.test import APITestCase
# from .factories import InterestFactory
class APITestMixin(object):
"""
mixin to perform the default API Test functionality
"""
api_root = '/v1/'
model_url = ''
data = {}
def get_endpoint(self):
"""
return the API endpoint
"""
url = self.api_root + self.model_url
return url
def test_create_object(self):
"""
create a new object
"""
response = self.client.post(self.get_endpoint(), self.data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data, self.data)
# this passes the test and the response says the object was created
def test_get_objects(self):
"""
get a list of objects
"""
response = self.client.get(self.get_endpoint())
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, self.data)
# this test fails and says the response is empty [] with no objects
class InterestTests(APITestCase, APITestMixin):
def setUp(self):
self.model_url = 'interests/'
self.data = {
'id': 1,
'name': 'hiking',
}
# self.interest = InterestFactory.create(name='travel')
"""
if I create the object with factory boy, the object is
there. But I don't want to have to do this - I want to use
the data that was created in the POST request
"""
You can see the couple lines of commented out code which are the object that I need to create through factory boy because the object does not get created and saved (although the create test does pass and say the object is created).
I didn't post any of the model, serializer or viewsets code because the actual API works, this is a question specific to the test.
First of all, Django TestCase (APITestCase's base class) encloses the test code in a database transaction that is rolled back at the end of the test (refer). That's why test_get_objects cannot see objects which created in test_create_object
Then, from (Django Testing Docs)
Having tests altering each others data, or having tests that depend on another test altering data are inherently fragile.
The first reason came into my mind is that you cannot rely on the execution order of tests. For now, the order within a TestCase seems to be alphabetical. test_create_object just happened to be executed before test_get_objects. If you change the method name to test_z_create_object, test_get_objects will go first. So better to make each test independent
Solution for your case, if you anyway don't want database reset after each test, use APISimpleTestCase
More recommended, group tests. E.g., rename test_create_object, test_get_objects to subtest_create_object, subtest_get_objects. Then create another test method to invoke the two tests as needed

Django how to test if message is sent

I want to test if message is sent to user after submit. I'm using django.contrib.messages. Everything seems to be working during manual testing (runserver), but in unit test I don't get messages.
Code that stores message:
messages.success(request, _('Internationalized evil message.'))
Code that should test message:
from django.contrib.messages.api import get_messages
...
def test_message_should_sent_to_user(self):
"""After successful phone number submit, message should be displayed."""
response = self.client.post(
reverse('evil.views.evil_data_submit'), self.valid_data)
messages = get_messages(response.request)
self.assertNotEqual(len(messages), 0)
It looks like that no middleware is called during test client post method call.
Update after #Tisho answer
Messages should be found in response.context, even my guts say that it should work, but it doesn't. I've placed import pdb; pdb.set_trace() in django/contrib/messages/context_processors.py to see if its called during test client.post, its not.
I've double checked TEMPLATE_CONTEXT_PROCESSORS, MIDDLEWARE_CLASSES and INSTALLED_APPS - probably tomorrow I'll discover that I missed something.
Important detail
Forgot to mention that in case of successful submit view returns HttpResponseRedirect therefore response.context is empty.
Solution
View returns redirect (which has no context data), to solve that we can pass follow=True to client.post method (method suggested by #Tisho).
During unit tests, the message could be found in
response = self.client.post(
reverse('evil.views.evil_data_submit'), self.valid_data)
messages = response.context['messages']
If your view returns a redirect, response.context will be empty unless you pass follow=True, like so:
response = self.client.post(
reverse('evil.views.evil_data_submit'),
self.valid_data,
follow=True)
The best way I found is by using mock
http://www.voidspace.org.uk/python/mock/patch.html#patch-methods-start-and-stop
class SimpleCommentTestDirect(TestCase):
def setUp(self):
self._patcher1 = patch('django.contrib.messages.error')
self.mock_error = self._patcher1.start()
def tearDown(self):
self._patcher1.stop()
def test_error_message(self):
self.client.get('/vote/direct/unknownapp/comment/1/up/')
self.assertEqual(self.mock_error.call_args[0][1], 'Wrong request. Model.')
BTW: There are also should be a way to get request by using mock. And by using request object get message from django.contrib.messages.get_messages