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
}
}
Related
I am trying for some time to test a view of mine that I have protected from access via LoginRequiredMixin and UserPassesTestMixin.
Unfortunately I do not manage to write the appropriate test.
here is the view. The special thing is, that the user must not only be logged in, but he must also belong to the group "Administrator" or "Supervisor".
With this combination I have not yet managed to write a test.
Please who can help me. Here is my View:
class FeatureListView(LoginRequiredMixin, UserPassesTestMixin, ListView):
model = FeatureFilm
template_name = "project/feature-list.html"
def test_func(self):
if self.request.user.groups.filter(name="Administrator").exists():
return True
elif self.request.user.groups.filter(name="Supervisor").exists():
return True
elif self.request.user.groups.filter(name="Operator").exists():
return True
else:
return False
def handle_no_permission(self):
return redirect("access-denied")
and here is a snippet of the url.py:
urlpatterns = [
path("feature/list/", FeatureListView.as_view(), name="feature-list"),
path(
"feature/<int:pk>/date",
FeatureDetailViewDate.as_view(),
name="feature-detail-date",
),
how would you test this FeatureListView and the template belongs to
thank you very much!
First of all it would be better if you would give those groups a permission. Because django offers a lot nice functionalities to check for permissions rather than checking for groups (see here). You would give Administrator, Supervisor and Operator all the same permission and then instead of checking if the user is in one of these groups, you would just check whether the user has that permission. But as that was not the question there you go:
from django.test import TestCase
from django.test import Client
class TestFeatureListView(TestCase):
def _create_administrator(self):
u = Users.objects.create( <input for administrator> )
u.groups.add(administrator)
return u
def _create_supervisor(self):
u = Users.objects.create( <input for supervisor> )
u.groups.add(supervisor)
return u
def _create_operator(self):
u = Users.objects.create( <input for operator> )
u.groups.add(operator)
return u
def _create_normal_user(self):
return Users.objects.create( <input for normal_user> )
def test_users_with_access_rights(self):
users = [
self._create_administrator(),
self._create_supervisor(),
self._create_operator(),
]
for u in users:
c = Client() # initialize every iteration a new client
c.login(username=<username>, password=<password>)
response = c.get(reverse("appwithfeaturelist:feature-list"))
self.assertTemplateUsed(response, "project/feature-list.html")
def test_users_withOUT_access_rights(self):
u = self._create_normal_user()
self.client.login(username=<username>, password=<password>)
response = self.client.get(reverse("appwithfeaturelist:feature-list"), follow=True)
self.assertTemplateNotUsed(response, "project/feature-list.html")
self.assertRedirects(response, "/access-denied/")
def test_without_any_user(self):
response = self.client.get(reverse("appwithfeaturelist:feature-list"), follow=True)
self.assertTemplateNotUsed(response, "project/feature-list.html")
self.assertRedirects(response, "/access-denied/") # this is probably wrong and should test whether the redirect goes to something like a login page
We are trying to create a model to control the number of Access attempts of a user, and then make a call to a function that might throw an exception when, for example, the credentials are not valid.
If there is an exception, update the model by incrementing the attempts by one, otherwise update the model in a different way.
Everytime we are in the exception branch the model doesn't get created, nor updated.
We have tried using
s1 = transaction.savepoint()
transaction.savepoint_commit(s1)
and #transaction.non_atomic_requests
but the result gets discarded anyway.
There is no way to avoid the exception logic. Django version: Django==2.2.24
access_attempt.py
from django.db import models
from django.utils.translation import gettext_lazy as _
class AccessAttempt(models.Model):
username = models.CharField(_("Username"), max_length=255, null=True, db_index=True)
failures_since_start = models.PositiveIntegerField(_("Failed Logins"))
validate_jwt_auth.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from .models.access_attempt import AccessAttempt
from rest_framework import status
from rest_framework.exceptions import AuthenticationFailed
#transaction.non_atomic_requests
def obtain_tokens(request, credentials):
attempt = AccessAttempt.objects.filter(username=credentials['username']).first()
if attempt is None:
attempt = AccessAttempt.objects.create(
username=credentials['username'],
failures_since_start=0
)
attempt.save()
failures = attempt.failures_since_start
print(failures)
if failures > 5:
return {'message': "Failed too many times"}, status.HTTP_403_FORBIDDEN
try:
with transaction.atomic():
payload = TokenObtainPairSerializer(context={'request':request}).validate(credentials)
attempt.failures_since_start=0
attempt.save()
return payload, status.HTTP_200_OK
except AuthenticationFailed:
attempt.failures_since_start += 1
attempt.save()
print(attempt.failures_since_start)
return {'message': "Unauthorized"}, status.HTTP_401_UNAUTHORIZED
views.py
def generate_auth(request):
b = json.loads(request.body)
if b['username'] is None or b['password'] is None:
return JsonResponse({'message': "Bad Request"}, status=status.HTTP_400_BAD_REQUEST)
payload, status = obtain_tokens(request, b)
return JsonResponse(payload, status=status)
I would like to implement pagination in my custom jsonresponse function. But I have no idea on how would i implement this. This is the code of my function. Any inputs will be a great help. Thank you.
def json_response(data = {}, message = 'successful!', status = 'success', code = 200):
data_set = {}
status = 'success' if code == 200 else 'error'
if status == 'success':
data_set['code'] = code
data_set['status'] = status
data_set['message'] = message
# data_set['data'] = data.data
try:
data_set['data'] = data.data
except TypeError:
data_set['data'] = json.dumps(data)
except AttributeError:
data_set['data'] = data
else:
data_set['code'] = code
data_set['status'] = status
data_set['message'] = message
return JsonResponse(data_set, safe=False, status=code)
if you're using Django Rest Framework, checkout the Pagination provided by the framework
https://www.django-rest-framework.org/api-guide/pagination/
Example:
# pagination.py
from rest_framework.pagination import PageNumberPagination
class CustomNumberPagination(PageNumberPagination):
page_size = 10 # Put the number of items you desire
# views.py
from somewhere.pagination import CustomNumberPagination
from somewhere.model import SomeModel
from somewhere.serializer import SomeSerializer
from rest_framework.generics import ListAPIView
class SomeView(generics.ListAPIView):
queryset = SomeModel.objects.all()
serializer_class = SomeSerializer
pagination_class = CustomNumberPagination
P.S. Also it can be configured globally like below
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10, # Put the number of items you desire
}
I defined a viewset using ModelViewSet as follow
I tried to redefine the GET method to do something like getting something from celery . but this part of code just won't work , it acts just like a standard API and didn't do what I wrote in the get_job_detail function.
How should I correctly define the "detail_route" function.
views.py
class JobViewSet(viewsets.ModelViewSet):
queryset = job.objects.all()
serializer_class = JobSerializer
#detail_route(methods=['get'])
def get_job_detail(self, request, pk=None):
# print('these part wont proceed')
job_item = self.get_object()
if job_item.isReady or job_item.isSuccessful:
return Response(self.serializer_class(job_item).data)
celeryjob = sometask.AsyncResult(pk)
celeryjob.get()
if celeryjob.state == 'SUCCESS':
job_item.state = celeryjob.state
job_item.result = celeryjob.result
job_item.isReady = True
job_item.isSuccessful = True
job_item.save()
if celeryjob.state == 'FAILURE':
job_item.state = celeryjob.state
job_item.result = celeryjob.result
job_item.isReady = True
job_item.isSuccessful = False
job_item.save()
return Response(self.serializer_class(job_item).data)
urls.py
from django.conf.urls import url, include
from apply_api import views
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'job',views.JobViewSet)
urlpatterns = [
url(r'^', include(router.urls)),
]
now your correct url is: /job/<pk>/get_job_detail if you want just: /job/<pk> you don need to use #detail_route just rename your method to the def retrieve(self, request, *args, **kwargs): more details retrievemodelmixin one of the part class of the modelviewset
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