Files
offline-db/users/tests/test_api_rate_limits.py
Barış Keserandgoogle-labs-jules[bot] 15aced2180 🛡️ Sentinel: [HIGH] Fix lack of rate limiting on creation endpoints (#276)
- Added endpoint-specific `UserRateThrottle` subclasses (`WatchLogCreateThrottle`, `ReviewCreateThrottle`) to `users/views.py` and `content/api/views.py` that only limit `POST` requests.
- Subclassed `TokenObtainPairView` to add a dedicated `AnonRateThrottle` for the login process.
- Configured throttling rates in `aniscrap_core/settings.py` (e.g., `login: 5/minute`, `watchlog: 10/minute`, `review: 5/hour`).
- Recorded learnings in `.jules/sentinel.md` to establish safe throttling patterns for future mutations.
- Updated `.jules/development-plan.md` to reflect the completion of section 1.2 tasks.
- Created `users/tests/test_api_rate_limits.py` to ensure HTTP 429 and `Retry-After` headers are correctly returned under excessive loads.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-03-10 21:44:12 +00:00

72 lines
2.9 KiB
Python

from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from users.models import User
from content.models import Anime, Season, Episode
from django.core.cache import cache
import time
class APIRateLimitTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='password123')
cache.clear() # Clear cache to reset rate limits
def test_login_rate_limit(self):
url = reverse('token_obtain_pair')
data = {'username': 'testuser', 'password': 'password123'}
# login throttle is 5/minute
for i in range(5):
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# 6th should fail
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
self.assertIn('Retry-After', response.headers)
def test_watchlog_create_rate_limit(self):
self.client.force_authenticate(user=self.user)
url = reverse('watch-history-list')
# watchlog throttle is 10/minute
for i in range(10):
# Create a new episode for each log to avoid potential unique constraints or 400s
anime = Anime.objects.create(title=f"Test Anime {i}")
season = Season.objects.create(anime=anime, number=1)
episode = Episode.objects.create(season=season, number=1)
data = {'episode': episode.id, 'watch_time': 100, 'duration': 1200}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# 11th should fail
anime = Anime.objects.create(title=f"Test Anime 11")
season = Season.objects.create(anime=anime, number=1)
episode = Episode.objects.create(season=season, number=1)
data = {'episode': episode.id, 'watch_time': 100, 'duration': 1200}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
self.assertIn('Retry-After', response.headers)
def test_review_create_rate_limit(self):
self.client.force_authenticate(user=self.user)
url = '/api/content/reviews/'
# review throttle is 5/hour
for i in range(5):
anime = Anime.objects.create(title=f"Test Review Anime {i}")
data = {'anime': anime.id, 'rating': 8, 'text': 'Good'}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# 6th should be throttled
anime = Anime.objects.create(title=f"Test Review Anime 6")
data = {'anime': anime.id, 'rating': 8, 'text': 'Good'}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
self.assertIn('Retry-After', response.headers)