How to write Django Unit Test for Authentication Protected REST APIs? - python

I have looked at two answers on SO that may already have my answer but frankly I just don't understand them. These SO post are: Django Rest Framework API Authentication Test and Django authenticate unit test.
Both of the SO questions use different approaches.
The approach I am attempting to use is this. I have created this class:
from rest_framework.test import APIClient
from django.test import testcases
from django.contrib.auth.models import User
class RemoteAuthenticatedTest(testcases.TestCase):
client_class = APIClient
def setUp(self):
self.username = 'mister_neutron'
self.user = User.objects.create_user(username='mister_neutron',
email='mister_neutron#example.com',
password='F4kePaSs0d')
super(RemoteAuthenticatedTest, self).setUp()
My unit test looks like this:
class InfoViewTestCase(RemoteAuthenticatedTest):
def create_info_record(self):
from random import randint
blade = 'test-blade-host-name-%s' % (randint(0, 100))
breachs = randint(0,100)
dimm = 'test dimm slot %s' % (randint(0,100))
url = reverse('info:info_creat')
data = {
'blade_hostname': blade,
'breach_count': breachs,
'dimm_slot': dimm,
}
response = self.client.post(url,
data,
format='json',
REMOTE_USER=self.username)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Info.objects.count(), 1)
self.assertEqual(Info.objects.get().blade_hostname, blade)
I have this in my settings.py file:
#Authentications
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
When I run my test I get this result:
Failure
Traceback (most recent call last):
File "/myproject/info/tests.py", line 686, in create_info_record
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
AssertionError: 401 != 201
What am I doing wrong?
UPDATE: After following #henriquesalvaro sage advice I have updated my code to this:
from rest_framework.test import APIClient,APITestCase
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
class RemoteAuthenticatedTest(APITestCase):
client_class = APIClient
def setUp(self):
self.username = 'mister_neutron'
self.user = User.objects.create_user(username='mister_neutron',
email='mister_neutron#example.com',
password='F4kePaSs0d')
Token.objects.create(user=self.user)
super(RemoteAuthenticatedTest, self).setUp()
And I updated my test case to this:
class InfoViewTestCase(RemoteAuthenticatedTest):
def create_info_record(self):
from random import randint
blade = 'test-blade-host-name-%s' % (randint(0, 100))
breachs = randint(0,100)
dimm = 'test dimm slot %s' % (randint(0,100))
url = reverse('info:info_creat')
data = {
'blade_hostname': blade,
'breach_count': breachs,
'dimm_slot': dimm,
}
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.user.auth_token.key)
response = self.client.post(url,
data,
format='json',
REMOTE_USER=self.username)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Info.objects.count(), 1)
self.assertEqual(Info.objects.get().blade_hostname, blade)
And now my unit test passes.

After following #henriquesalvaro (see comments) sage advice I have updated my code to this:
from rest_framework.test import APIClient,APITestCase
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
class RemoteAuthenticatedTest(APITestCase):
client_class = APIClient
def setUp(self):
self.username = 'mister_neutron'
self.user = User.objects.create_user(username='mister_neutron',
email='mister_neutron#example.com',
password='F4kePaSs0d')
Token.objects.create(user=self.user)
super(RemoteAuthenticatedTest, self).setUp()
And I updated my test case to this:
class InfoViewTestCase(RemoteAuthenticatedTest):
def create_info_record(self):
from random import randint
blade = 'test-blade-host-name-%s' % (randint(0, 100))
breachs = randint(0,100)
dimm = 'test dimm slot %s' % (randint(0,100))
url = reverse('info:info_creat')
data = {
'blade_hostname': blade,
'breach_count': breachs,
'dimm_slot': dimm,
}
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.user.auth_token.key)
response = self.client.post(url,
data,
format='json',
REMOTE_USER=self.username)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Info.objects.count(), 1)
self.assertEqual(Info.objects.get().blade_hostname, blade)
And now my unit test passes.

Related

How do i access request.user in self.client.post Testing

I have a model called Quote which has a ForeignKey pointing to User.
I want to make a post request to create a Quote but i don't know what data i should give to self.client.post, this is what i got so far i am talking about the test_can_save_a_POST_request:
from django.contrib.auth import get_user_model
from django.test import TestCase
from quotes.models import Quote
from quotes.views import home_page
User = get_user_model()
class HomePageTest(TestCase):
def test_uses_home_template(self):
response = self.client.get('/')
self.assertTemplateUsed(response, 'home.html')
def test_display_all_quotes(self):
user1 = User.objects.create(email="rafik#test.com", password="foo")
Quote.objects.create(text='quote 1', user=user1)
Quote.objects.create(text='quote 2', user=user1)
response = self.client.get('/')
self.assertIn('quote 1', response.content.decode())
self.assertIn('quote 2', response.content.decode())
def test_can_save_a_POST_request(self):
user1 = User.objects.create(email="rafik#test.com", password="foo")
self.client.post('/', data={
'quote_text': 'The first ever quote'
}
)

TypeError: 'Token' object is not callable

When I test the test_delivery.py with pytest, I get the error: "TypeError: 'Token' object is not callable".
I am using pytest with Django REST. From the examples that I saw, it seems that the code is correct. What am I missing?
basic.py
import pytest
#pytest.fixture
def api_client():
from rest_framework.test import APIClient
return APIClient()
#pytest.fixture
def test_password():
return 'Str0ng-test-pa$$'
import uuid
#pytest.fixture
def create_user(db, django_user_model, test_password):
def make_user(**kwargs):
kwargs['password'] = test_password
if 'username' not in kwargs:
kwargs['username'] = str(uuid.uuid4())
return django_user_model.objects.create_user(**kwargs)
return make_user
from rest_framework.authtoken.models import Token
#pytest.fixture
def get_or_create_token(db, create_user):
user = create_user()
token, _ = Token.objects.get_or_create(user=user)
return token
test_delivery.py
import pytest
from django.urls import reverse
from tests.utils.basic import api_client, get_or_create_token, create_user, test_password
from rest_framework.authtoken.models import Token
#pytest.mark.django_db
def test_unauthorized_request(api_client):
url = reverse('deliveries-list')
response = api_client.get(url)
assert response.status_code == 401
#pytest.mark.django_db
def test_authorized_request(api_client, get_or_create_token):
url = reverse('deliveries-list')
token = get_or_create_token()
api_client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
response = api_client.get(url)
assert response.status_code == 200
[Update] Error details
======================================================= FAILURES =======================================================
_______________________________________________ test_authorized_request ________________________________________________
api_client = <rest_framework.test.APIClient object at 0x7f1ee5e0fa00>
get_or_create_token = <Token: 1771cc20278aed83af8e09646286edd1ef8cb7b7>
#pytest.mark.django_db
def test_authorized_request(api_client, get_or_create_token):
url = reverse('deliveries-list')
> token = get_or_create_token()
E TypeError: 'Token' object is not callable
tests/test_delivery/test_delivery.py:17: TypeError
This is the new test_authorized_request that worked for me. I changed the way that I created the token.
test_delivery.py
#pytest.mark.django_db
def test_authorized_request(api_client, get_or_create_token):
user = User.objects.create_user('test', 'user#example.com', 'Strong-test-pass')
token = Token.objects.create(user=user)
url = reverse('deliveries-list')
api_client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
response = api_client.get(url)
assert response.status_code == 200

how to capture logged in user's name in django?

class logEngine:
def logger(request):
t1 = time.time()
params = {'user': request.user.username,
'ip_address': socket.gethostbyname(socket.gethostname()),
'process_time': time.time() - t1,
}
return params
when I uses request.user.username (which is given in most of the examples in the internet), I get the below error,
AttributeError: 'logEngine' object has no attribute 'user'
Please suggest any other method
I got the solution. It's working fine now
customdblogger_views.py
import socket
import time
import logging
from urllib import request
class logEngine:
def logger(self, request):
t1 = time.time()
params = {'user': request.user.username,
'ip_address': socket.gethostbyname(socket.gethostname()),
'process_time': time.time() - t1,
}
return params
def django_logger(self):
django_logger = logging.getLogger('db')
return django_logger
views.py
# Create your views here.
import logging
import sys
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
from CustomDBLogger.customdblogger_views import logEngine
from rest_framework import viewsets, status
from rest_framework.decorators import action
db_logger = logging.getLogger('db')
class Request(viewsets.GenericViewSet):
attributes = logEngine()
#action(detail=True)
def test_api_method(self, request):
params2 = {
'class_name': __class__.__name__,
'method_name': sys._getframe().f_code.co_name,
'module_name': __name__,
}
v1 = self.attributes.logger(request)
params2.update(dict(v1))
db_logger.info("Successfully entered into function", extra=params2)
try:
1 / 0
except Exception as e:
db_logger.exception(e, extra=params2)
return Response({'data': True}, status=status.HTTP_200_OK)
finally:
db_logger.info("exited from the function", extra=params2)

How is throttling disabled for testing in Django Rest Framework?

Upon implementing a throttle for a REST API, I'm encountering an issue when running my tests all at once.
Upon isolating the subject TestCase and running the test runner, the TestCase passes its assertions. However when all the tests are ran I get the following error: AssertionError: 429 != 400. Which that type of error of course is due to the requests exceeding a rate limit.
How can I disable throttling for the tests so the assertion error is not raised. I decorated the TestCase with #override_settings but that doesn't have any effect.
from copy import deepcopy
from django.conf import settings
from django.test import TestCase, override_settings
from django.contrib.auth.models import User
from rest_framework.test import APITestCase, APIClient
from django.urls import reverse
from ..models import QuestionVote, Question
from users.models import UserAccount
from tags.models import Tag
from .model_test_data import mock_questions_submitted
REST_FRAMEWORK = deepcopy(settings.REST_FRAMEWORK)
del REST_FRAMEWORK['DEFAULT_THROTTLE_RATES']
#override_settings(REST_FRAMEWORK=REST_FRAMEWORK)
class TestUserVoteOnOwnQuestion(APITestCase):
'''Verify that a User cannot vote on their own Question'''
#classmethod
def setUpTestData(cls):
cls.user1 = User.objects.create_user("Me", password="topsecretcode")
cls.user1_account = UserAccount.objects.create(user=cls.user1)
cls.tag = Tag.objects.create(name="Tag")
cls.q = mock_questions_submitted[2]
cls.q.update({'user_account': cls.user1_account})
cls.question = Question(**cls.q)
cls.question.save()
cls.question.tags.add(cls.tag)
def test_vote_on_own_posted_question(self):
self.client.login(username="Me", password="topsecretcode")
response = self.client.put(
reverse("questions_api:vote", kwargs={'id': 1}),
data={"vote": "upvote"}
)
self.assertEqual(response.status_code, 400)
self.assertEquals(
response.data['vote'],
"Cannot vote on your own question"
)
REST_FRAMEWORK = {
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
'DEFAULT_THROTTLE_RATES': {
'voting': '5/minute'
}
}
class UserQuestionVoteView(APIView):
renderer_classes = [JSONRenderer, ]
parser_classes = [JSONParser, ]
permission_classes = [IsAuthenticated, ]
authentication_classes = [SessionAuthentication, ]
throttle_classes = [ScopedRateThrottle, ]
throttle_scope = "voting"
def put(self, request, id):
# import pdb; pdb.set_trace()
account = UserAccount.objects.get(user=request.user)
question = Question.objects.get(id=id)
if account == question.user_account:
return Response(data={
'vote': "Cannot vote on your own question"
}, status=400)
try:
stored_vote = QuestionVote.objects.get(
account=account, question=question
)
serializer = QuestionVoteSerializer(stored_vote, request.data)
except QuestionVote.DoesNotExist:
serializer = QuestionVoteSerializer(data=request.data)
finally:
if serializer.is_valid(raise_exception=True):
question_vote = serializer.save(
account=account,
question=question
)
vote = serializer.validated_data['vote']
if vote == "downvote":
question.vote_tally = F('vote_tally') - 1
else:
question.vote_tally = F('vote_tally') + 1
question.save()
question.refresh_from_db()
return Response(data={
'id': question.id,
'tally': question.vote_tally
})
return Response(serializer.errors)
One way to do this is by setting your config files up to support testing versions:
# config.py
REST_FRAMEWORK = {
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
'DEFAULT_THROTTLE_RATES': {
'voting': '5/minute'
}
}
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
if TESTING:
del REST_FRAMEWORK['DEFAULT_THROTTLE_RATES']
The pro of this approach is you're not hacking away at your application in tests and hiding modifications to the config file - all of your testing based changes are in the same config file as their true values.
The con of this approach is unless your developers know this setting and values are there, they may be scratching their heads as to why the throttling doesn't work in tests, but does at runtime.
My solution was to apply some monkey patching.
I have a throttles.py file where I have custom throttles, such as
class UserBurstRateThrottle(UserRateThrottle):
rate = '120/minute'
What I've done is create a stub allow_request function to always return true, so something like
def apply_monkey_patching_for_test():
def _allow_request(self, request, view):
return True
UserBurstRateThrottle.allow_request = _allow_request
Then, in the test_whatever.py file, I add the following at the top.
from my_proj import throttles
throttles.apply_monkey_patching_for_test()
Another easy way is to disable the cache that's responsible for storing the clients' meta data (https://www.django-rest-framework.org/api-guide/throttling/#setting-up-the-cache). So you need this in your test settings:
CACHES = {
'<throttling-cache-name|default>': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache', # to prevent API throttling
}
}

Mocking patched class methods is not working

I'm using Django with DRF and python mock. What I am trying to do is to test my views and mock a serializer and some methods.
This is what I have:
views.py
from gmm_mobile.v1.serializers import RegisterParticipationSerializer
from gmm_mobile.v1.exceptions import GameOrCampaignDoesNotExist
from gmm_util.header import Header
from gmm_util.response import ResponseUtils
from gmm_util.permissions import MobileTokenPermission
from gmm_util.permissions import MobileTokenAuthentication
class ParticipantViewMobile(APIView):
permission_classes = (MobileTokenPermission, )
authentication_classes = (MobileTokenAuthentication, )
def post(self, request, service_id, campaign_id):
try:
environment_info = Header.get_environment_info(request)
request.data[Field.GAME_SERVICE_ID] = service_id
request.data[Field.CAMPAIGN] = campaign_id
request.data[Field.LOCATION] = environment_info
participation_serializer = RegisterParticipationSerializer(data=request.data)
participation_serializer.is_valid(raise_exception=True)
participation_serializer.save()
return ResponseUtils.created()
except Http404:
raise GameOrCampaignDoesNotExist()
serializers.py
class RegisterParticipationSerializer(serializers.ModelSerializer):
location = LocationSerializer(many=False)
campaign = serializers.IntegerField()
game_service_id = serializers.CharField(max_length=254)
class Meta:
model = Participation
fields = (Field.PK, Field.EMAIL, Field.GPG_ID, Field.DATE, Field.LOCATION, Field.INFO, Field.CAMPAIGN,
Field.GAME_SERVICE_ID, Field.GCM_ID)
read_only_fields = (Field.GCM_ID,)
test_views.py
from mock import patch
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase, APIRequestFactory
from rest_framework import status
from rest_framework.exceptions import ValidationError
from gmm_mobile.v1.views import ParticipantViewMobile
from gmm_mobile.v1.urls import CONSULT_PARTICIPANT
from gmm_push.environment_configuration import EnvironmentConfiguration
from gmm_util.util_test import JsonObjects
class ParticipantViewMobileTests(APITestCase):
factory = APIRequestFactory()
url = reverse(PARTICIPANT_MOBILE, kwargs={'service_id': 1, 'campaign_id': 1})
def setUp(self):
self.view = ParticipantViewMobile.as_view()
#patch('gmm_mobile.v1.views.RegisterParticipationSerializer')
def test__post__invalid_data__status_400(self, mock_serializer):
# Arrange
mock_serializer.is_valid.side_effect = ValidationError({})
request = self.factory.post(
self.url,
{},
HTTP_X_ENV=JsonObjects.environment_info_1(),
HTTP_X_TOKEN=EnvironmentConfiguration.get_token(False),
format='json')
# Act
response = self.view(request, 1, 1)
# Assert
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
But when I run the test, the side_effect is not working. When I change the .is_valid in the test it has no effect on ParticipantViewMobile.post.
If I do #patch('gmm_mobile.v1.views.RegisterParticipationSerializer.is_valid') and mock_serializer.side_effect = ValidationError({}) it works, but I don't want that because there will be cases that I will need to mock more than one method, like .save and .is_valid.
I also tried to change the import styles views.py and test_views.py but it still didn't work.
Am I doind something wrong or missing anything?
EDIT 1:
I've put prints on the view to try to understand what was happening:
def post(self, request, service_id, campaign_id):
try:
environment_info = Header.get_environment_info(request)
request.data[Field.GAME_SERVICE_ID] = service_id
request.data[Field.CAMPAIGN] = campaign_id
request.data[Field.LOCATION] = environment_info
participation_serializer = RegisterParticipationSerializer(data=request.data)
print RegisterParticipationSerializer.is_valid.side_effect
print participation_serializer.is_valid.side_effect
participation_serializer.is_valid(raise_exception=True)
participation_serializer.save()
return ResponseUtils.created()
except Http404:
raise GameOrCampaignDoesNotExist()
And the output:
{}
None
So, when I create an instance of RegisterParticipationSerializer, I lost the mocked methods. How to avoid this?
Your mock doesn't work because you're mocking the class and not the instance being generated from the class. Try this:
mock_instance = Mock()
mock_instance.is_valid.side_effect = ValidationError({})
mock_serializer.return_value = mock_instance

Categories

Resources