0

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)

binny
  • 649
  • 1
  • 8
  • 22
  • Try to put `@override_settings` decorator on the tested function, and not the whole class, [like here](https://www.django-rest-framework.org/api-guide/throttling/#setting-the-throttling-policy). [This question](https://stackoverflow.com/questions/52037157/how-can-i-rewrite-one-value-in-the-settings-for-the-test) might have some other ideas (even if `if setting.TEST` is a bad practice). – Romain May 10 '21 at 03:42
  • I placed the decorate of the test methods as well but still got the same result in terms of the errors being raised. – binny May 10 '21 at 05:19

3 Answers3

1

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
    }
}
tuky
  • 61
  • 1
  • 3
0

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.

Trent
  • 2,909
  • 1
  • 31
  • 46
  • The thing I'm noticing when I implement this is that errors are still being raised but the test methods that raise the errors change. – binny May 10 '21 at 05:20
  • Instead of doing "del" on the object, try just adding the full object without the throttle part. – Trent May 10 '21 at 05:28
0

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()
gmcc051
  • 390
  • 3
  • 17