diff --git a/docs/developers-info.rst b/docs/developers-info.rst
index 39520e58..9d797c3a 100644
--- a/docs/developers-info.rst
+++ b/docs/developers-info.rst
@@ -1,127 +1,127 @@
Developers Information
======================
Sample configuration
--------------------
The configuration will be taken from the default configuration file: ``~/.config/swh/web/web.yml``.
The following introduces a default configuration file:
.. sourcecode:: yaml
storage:
cls: remote
args:
url: http://localhost:5002
debug: false
throttling:
cache_uri: None
scopes:
swh_api:
limiter_rate:
default: 120/h
exempted_networks:
- 127.0.0.0/8
Run server
----------
Either use the django manage script directly (useful in development mode as it offers various commands):
.. sourcecode:: shell
$ python3 -m swh.web.manage runserver
or use the following shortcut:
.. sourcecode:: shell
$ make run
Modules description
-------------------
Common to all web applications
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Configuration and settings
""""""""""""""""""""""""""
* :mod:`swh.web.config`: holds the configuration for the web applications.
* :mod:`swh.web.doc_config`: utility module used to extend the sphinx configuration
when building the documentation.
* :mod:`swh.web.manage`: Django management module for developers.
* :mod:`swh.web.urls`: module that holds the whole URI scheme of all
the web applications.
* :mod:`swh.web.settings.common`: Common Django settings
* :mod:`swh.web.settings.development`: Django settings for development
* :mod:`swh.web.settings.production`: Django settings for production
* :mod:`swh.web.settings.tests`: Django settings for tests
Common utilities
""""""""""""""""
* :mod:`swh.web.common.converters`: conversion module used to transform raw data
to serializable ones. It is used by :mod:`swh.web.common.service`: to convert data
before transmitting then to Django views.
* :mod:`swh.web.common.exc`: module defining exceptions used in the web applications.
* :mod:`swh.web.common.highlightjs`: utility module to ease the use of the highlightjs_
library in produced Django views.
* :mod:`swh.web.common.query`: Utilities to parse data from HTTP endpoints. It is used
by :mod:`swh.web.common.service`.
* :mod:`swh.web.common.service`: Orchestration layer used by views module
in charge of communication with :mod:`swh.storage` to retrieve information and
perform conversion for the upper layer.
* :mod:`swh.web.common.swh_templatetags`: Custom Django template tags library for swh.
- * :mod:`swh.web.common.throttling`: Custom request rate limiter to use with the `Django REST Framework
- `_
* :mod:`swh.web.common.urlsindex`: Utilities to help the registering of endpoints
for the web applications
* :mod:`swh.web.common.utils`: Utility functions used in the web applications implementation
swh-web API application
^^^^^^^^^^^^^^^^^^^^^^^
* :mod:`swh.web.api.apidoc`: Utilities to document the web api for its html
browsable rendering.
* :mod:`swh.web.api.apiresponse`: Utility module to ease the generation of
web api responses.
* :mod:`swh.web.api.apiurls`: Utilities to facilitate the registration of web api endpoints.
+ * :mod:`swh.web.api.throttling`: Custom request rate limiter to use with the `Django REST Framework
+ `_
* :mod:`swh.web.api.urls`: Module that defines the whole URI scheme for the api endpoints
* :mod:`swh.web.api.utils`: Utility functions used in the web api implementation.
* :mod:`swh.web.api.views.content`: Implementation of API endpoints for getting information
about contents.
* :mod:`swh.web.api.views.directory`: Implementation of API endpoints for getting information
about directories.
* :mod:`swh.web.api.views.origin`: Implementation of API endpoints for getting information
about origins.
* :mod:`swh.web.api.views.person`: Implementation of API endpoints for getting information
about persons.
* :mod:`swh.web.api.views.release`: Implementation of API endpoints for getting information
about releases.
* :mod:`swh.web.api.views.revision`: Implementation of API endpoints for getting information
about revisions.
* :mod:`swh.web.api.views.snapshot`: Implementation of API endpoints for getting information
about snapshots.
* :mod:`swh.web.api.views.stat`: Implementation of API endpoints for getting information
about archive statistics.
* :mod:`swh.web.api.views.utils`: Utilities used in the web api endpoints implementation.
swh-web browse application
^^^^^^^^^^^^^^^^^^^^^^^^^^
* :mod:`swh.web.browse.browseurls`: Utilities to facilitate the registration of browse endpoints.
* :mod:`swh.web.browse.urls`: Module that defines the whole URI scheme for the browse endpoints.
* :mod:`swh.web.browse.utils`: Utilities functions used throughout the browse endpoints implementation.
* :mod:`swh.web.browse.views.content`: Implementation of endpoints for browsing contents.
* :mod:`swh.web.browse.views.directory`: Implementation of endpoints for browsing directories.
* :mod:`swh.web.browse.views.identifiers`: Implementation of endpoints for browsing objects
through persistent identifiers.
* :mod:`swh.web.browse.views.origin`: Implementation of endpoints for browsing origins.
* :mod:`swh.web.browse.views.person`: Implementation of endpoints for browsing persons.
* :mod:`swh.web.browse.views.release`: Implementation of endpoints for browsing releases.
* :mod:`swh.web.browse.views.revision`: Implementation of endpoints for browsing revisions.
* :mod:`swh.web.browse.views.snapshot`: Implementation of endpoints for browsing snapshots.
.. _highlightjs: https://highlightjs.org/
diff --git a/swh/web/api/apiurls.py b/swh/web/api/apiurls.py
index 6f8031f0..9b037ead 100644
--- a/swh/web/api/apiurls.py
+++ b/swh/web/api/apiurls.py
@@ -1,91 +1,91 @@
# Copyright (C) 2017-2019 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import functools
from typing import Dict
from rest_framework.decorators import api_view
from swh.web.common.urlsindex import UrlsIndex
-from swh.web.common import throttling
+from swh.web.api import throttling
class APIUrls(UrlsIndex):
"""
Class to manage API documentation URLs.
- Indexes all routes documented using apidoc's decorators.
- Tracks endpoint/request processing method relationships for use in
generating related urls in API documentation
"""
_apidoc_routes = {} # type: Dict[str, Dict[str, str]]
scope = 'api'
@classmethod
def get_app_endpoints(cls):
return cls._apidoc_routes
@classmethod
def add_doc_route(cls, route, docstring, noargs=False,
api_version='1', **kwargs):
"""
Add a route to the self-documenting API reference
"""
route_name = route[1:-1].replace('/', '-')
if not noargs:
route_name = '%s-doc' % route_name
route_view_name = 'api-%s-%s' % (api_version, route_name)
if route not in cls._apidoc_routes:
d = {'docstring': docstring,
'route': '/api/%s%s' % (api_version, route),
'route_view_name': route_view_name}
for k, v in kwargs.items():
d[k] = v
cls._apidoc_routes[route] = d
def api_route(url_pattern=None, view_name=None,
methods=['GET', 'HEAD', 'OPTIONS'],
throttle_scope='swh_api',
api_version='1',
checksum_args=None):
"""
Decorator to ease the registration of an API endpoint
using the Django REST Framework.
Args:
url_pattern: the url pattern used by DRF to identify the API route
view_name: the name of the API view associated to the route used to
reverse the url
methods: array of HTTP methods supported by the API route
"""
url_pattern = '^' + api_version + url_pattern + '$'
def decorator(f):
# create a DRF view from the wrapped function
@api_view(methods)
@throttling.throttle_scope(throttle_scope)
@functools.wraps(f)
def api_view_f(*args, **kwargs):
return f(*args, **kwargs)
# small hacks for correctly generating API endpoints index doc
api_view_f.__name__ = f.__name__
api_view_f.http_method_names = methods
# register the route and its view in the endpoints index
APIUrls.add_url_pattern(url_pattern, api_view_f,
view_name)
if checksum_args:
APIUrls.add_redirect_for_checksum_args(view_name,
[url_pattern],
checksum_args)
return f
return decorator
diff --git a/swh/web/common/throttling.py b/swh/web/api/throttling.py
similarity index 100%
rename from swh/web/common/throttling.py
rename to swh/web/api/throttling.py
diff --git a/swh/web/misc/origin_save.py b/swh/web/misc/origin_save.py
index 50844a66..2ec25bf5 100644
--- a/swh/web/misc/origin_save.py
+++ b/swh/web/misc/origin_save.py
@@ -1,108 +1,108 @@
# Copyright (C) 2018-2019 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import json
from django.conf.urls import url
from django.core.paginator import Paginator
from django.http import (
HttpResponse, HttpResponseForbidden, HttpResponseServerError
)
from django.shortcuts import render
from rest_framework.decorators import api_view, authentication_classes
+from swh.web.api.throttling import throttle_scope
from swh.web.common.exc import ForbiddenExc
from swh.web.common.models import SaveOriginRequest
from swh.web.common.origin_save import (
create_save_origin_request, get_savable_visit_types,
get_save_origin_requests_from_queryset
)
-from swh.web.common.throttling import throttle_scope
from swh.web.common.utils import EnforceCSRFAuthentication
def _origin_save_view(request):
return render(request, 'misc/origin-save.html',
{'heading': ('Request the saving of a software origin into '
'the archive')})
@api_view(['POST'])
@authentication_classes((EnforceCSRFAuthentication, ))
@throttle_scope('swh_save_origin')
def _origin_save_request(request, visit_type, origin_url):
"""
This view is called through AJAX from the save code now form of swh-web.
We use DRF here as we want to rate limit the number of submitted requests
per user to avoid being possibly flooded by bots.
"""
try:
response = json.dumps(create_save_origin_request(visit_type,
origin_url),
separators=(',', ': '))
return HttpResponse(response, content_type='application/json')
except ForbiddenExc as exc:
return HttpResponseForbidden(json.dumps({'detail': str(exc)}),
content_type='application/json')
except Exception as exc:
return HttpResponseServerError(json.dumps({'detail': str(exc)}),
content_type='application/json')
def _visit_save_types_list(request):
visit_types = json.dumps(get_savable_visit_types(),
separators=(',', ': '))
return HttpResponse(visit_types, content_type='application/json')
def _origin_save_requests_list(request, status):
if status != 'all':
save_requests = SaveOriginRequest.objects.filter(status=status)
else:
save_requests = SaveOriginRequest.objects.all()
table_data = {}
table_data['recordsTotal'] = save_requests.count()
table_data['draw'] = int(request.GET['draw'])
search_value = request.GET['search[value]']
column_order = request.GET['order[0][column]']
field_order = request.GET['columns[%s][name]' % column_order]
order_dir = request.GET['order[0][dir]']
if order_dir == 'desc':
field_order = '-' + field_order
save_requests = save_requests.order_by(field_order)
length = int(request.GET['length'])
page = int(request.GET['start']) / length + 1
save_requests = get_save_origin_requests_from_queryset(save_requests)
if search_value:
save_requests = \
[sr for sr in save_requests
if search_value.lower() in sr['save_request_status'].lower()
or search_value.lower() in sr['save_task_status'].lower()
or search_value.lower() in sr['visit_type'].lower()
or search_value.lower() in sr['origin_url'].lower()]
table_data['recordsFiltered'] = len(save_requests)
paginator = Paginator(save_requests, length)
table_data['data'] = paginator.page(page).object_list
table_data_json = json.dumps(table_data, separators=(',', ': '))
return HttpResponse(table_data_json, content_type='application/json')
urlpatterns = [
url(r'^save/$', _origin_save_view, name='origin-save'),
url(r'^save/(?P.+)/url/(?P.+)/$',
_origin_save_request, name='origin-save-request'),
url(r'^save/types/list/$', _visit_save_types_list,
name='origin-save-types-list'),
url(r'^save/requests/list/(?P.+)/$', _origin_save_requests_list,
name='origin-save-requests-list'),
]
diff --git a/swh/web/settings/common.py b/swh/web/settings/common.py
index 94fa4519..0fc3f08e 100644
--- a/swh/web/settings/common.py
+++ b/swh/web/settings/common.py
@@ -1,304 +1,304 @@
# Copyright (C) 2017-2020 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
"""
Django common settings for swh-web.
"""
import os
import sys
from typing import Any, Dict
from swh.web.config import get_config
swh_web_config = get_config()
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = swh_web_config['secret_key']
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = swh_web_config['debug']
DEBUG_PROPAGATE_EXCEPTIONS = swh_web_config['debug']
ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] + swh_web_config['allowed_hosts']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'swh.web.common',
'swh.web.api',
'swh.web.browse',
'webpack_loader',
'django_js_reverse',
'corsheaders',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'swh.web.common.middlewares.ThrottlingHeadersMiddleware',
]
# Compress all assets (static ones and dynamically generated html)
# served by django in a local development environment context.
# In a production environment, assets compression will be directly
# handled by web servers like apache or nginx.
if swh_web_config['serve_assets']:
MIDDLEWARE.insert(0, 'django.middleware.gzip.GZipMiddleware')
ROOT_URLCONF = 'swh.web.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(PROJECT_DIR, "../templates")],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'swh.web.common.utils.context_processor'
],
'libraries': {
'swh_templatetags': 'swh.web.common.swh_templatetags',
},
},
},
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': swh_web_config['development_db'],
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
# static folder location when swh-web has been installed with pip
STATIC_DIR = os.path.join(sys.prefix, 'share/swh/web/static')
if not os.path.exists(STATIC_DIR):
# static folder location when developping swh-web
STATIC_DIR = os.path.join(PROJECT_DIR, '../../../static')
STATICFILES_DIRS = [STATIC_DIR]
INTERNAL_IPS = ['127.0.0.1']
throttle_rates = {}
http_requests = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH']
throttling = swh_web_config['throttling']
for limiter_scope, limiter_conf in throttling['scopes'].items():
if 'default' in limiter_conf['limiter_rate']:
throttle_rates[limiter_scope] = limiter_conf['limiter_rate']['default']
# for backward compatibility
else:
throttle_rates[limiter_scope] = limiter_conf['limiter_rate']
# register sub scopes specific for HTTP request types
for http_request in http_requests:
if http_request in limiter_conf['limiter_rate']:
throttle_rates[limiter_scope + '_' + http_request.lower()] = \
limiter_conf['limiter_rate'][http_request]
REST_FRAMEWORK: Dict[str, Any] = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'swh.web.api.renderers.YAMLRenderer',
'rest_framework.renderers.TemplateHTMLRenderer'
),
'DEFAULT_THROTTLE_CLASSES': (
- 'swh.web.common.throttling.SwhWebRateThrottle',
+ 'swh.web.api.throttling.SwhWebRateThrottle',
),
'DEFAULT_THROTTLE_RATES': throttle_rates,
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'swh.web.auth.backends.OIDCBearerTokenAuthentication',
],
}
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'formatters': {
'request': {
'format': '[%(asctime)s] [%(levelname)s] %(request)s %(status_code)s', # noqa
'datefmt': "%d/%b/%Y %H:%M:%S"
},
'simple': {
'format': '[%(asctime)s] [%(levelname)s] %(message)s',
'datefmt': "%d/%b/%Y %H:%M:%S"
},
'verbose': {
'format': '[%(asctime)s] [%(levelname)s] %(name)s.%(funcName)s:%(lineno)s - %(message)s', # noqa
'datefmt': "%d/%b/%Y %H:%M:%S"
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'file': {
'level': 'WARNING',
'filters': ['require_debug_false'],
'class': 'logging.FileHandler',
'filename': os.path.join(swh_web_config['log_dir'], 'swh-web.log'),
'formatter': 'simple'
},
'file_request': {
'level': 'WARNING',
'filters': ['require_debug_false'],
'class': 'logging.FileHandler',
'filename': os.path.join(swh_web_config['log_dir'], 'swh-web.log'),
'formatter': 'request'
},
'console_verbose': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'file_verbose': {
'level': 'WARNING',
'filters': ['require_debug_false'],
'class': 'logging.FileHandler',
'filename': os.path.join(swh_web_config['log_dir'], 'swh-web.log'),
'formatter': 'verbose'
},
'null': {
'class': 'logging.NullHandler',
},
},
'loggers': {
'': {
'handlers': ['console_verbose', 'file_verbose'],
'level': 'DEBUG' if DEBUG else 'WARNING',
},
'django': {
'handlers': ['console'],
'level': 'DEBUG' if DEBUG else 'WARNING',
'propagate': False,
},
'django.request': {
'handlers': ['file_request'],
'level': 'DEBUG' if DEBUG else 'WARNING',
'propagate': False,
},
'django.db.backends': {
'handlers': ['null'],
'propagate': False
},
'django.utils.autoreload': {
'level': 'INFO',
},
},
}
WEBPACK_LOADER = {
'DEFAULT': {
'CACHE': False,
'BUNDLE_DIR_NAME': './',
'STATS_FILE': os.path.join(STATIC_DIR, 'webpack-stats.json'),
'POLL_INTERVAL': 0.1,
'TIMEOUT': None,
'IGNORE': ['.+\\.hot-update.js', '.+\\.map']
}
}
LOGIN_URL = '/admin/login/'
LOGIN_REDIRECT_URL = 'admin'
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
},
'db_cache': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'swh_web_cache',
}
}
JS_REVERSE_JS_MINIFY = False
CORS_ORIGIN_ALLOW_ALL = True
CORS_URLS_REGEX = r'^/badge/.*$'
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'swh.web.auth.backends.OIDCAuthorizationCodePKCEBackend',
]
OIDC_SWH_WEB_CLIENT_ID = 'swh-web'
diff --git a/swh/web/tests/common/test_throttling.py b/swh/web/tests/api/test_throttling.py
similarity index 98%
rename from swh/web/tests/common/test_throttling.py
rename to swh/web/tests/api/test_throttling.py
index e82c82d6..f363289d 100644
--- a/swh/web/tests/common/test_throttling.py
+++ b/swh/web/tests/api/test_throttling.py
@@ -1,148 +1,148 @@
# Copyright (C) 2017-2019 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from swh.web.settings.tests import (
scope1_limiter_rate, scope1_limiter_rate_post,
scope2_limiter_rate, scope2_limiter_rate_post,
scope3_limiter_rate, scope3_limiter_rate_post
)
from django.conf.urls import url
from django.test.utils import override_settings
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.decorators import api_view
-from swh.web.common.throttling import SwhWebRateThrottle, throttle_scope
+from swh.web.api.throttling import SwhWebRateThrottle, throttle_scope
class MockViewScope1(APIView):
throttle_classes = (SwhWebRateThrottle,)
throttle_scope = 'scope1'
def get(self, request):
return Response('foo_get')
def post(self, request):
return Response('foo_post')
@api_view(['GET', 'POST'])
@throttle_scope('scope2')
def mock_view_scope2(request):
if request.method == 'GET':
return Response('bar_get')
elif request.method == 'POST':
return Response('bar_post')
class MockViewScope3(APIView):
throttle_classes = (SwhWebRateThrottle,)
throttle_scope = 'scope3'
def get(self, request):
return Response('foo_get')
def post(self, request):
return Response('foo_post')
@api_view(['GET', 'POST'])
@throttle_scope('scope3')
def mock_view_scope3(request):
if request.method == 'GET':
return Response('bar_get')
elif request.method == 'POST':
return Response('bar_post')
urlpatterns = [
url(r'^scope1_class$', MockViewScope1.as_view()),
url(r'^scope2_func$', mock_view_scope2),
url(r'^scope3_class$', MockViewScope3.as_view()),
url(r'^scope3_func$', mock_view_scope3)
]
def check_response(response, status_code,
limit=None, remaining=None):
assert response.status_code == status_code
if limit is not None:
assert response['X-RateLimit-Limit'] == str(limit)
else:
assert 'X-RateLimit-Limit' not in response
if remaining is not None:
assert response['X-RateLimit-Remaining'] == str(remaining)
else:
assert 'X-RateLimit-Remaining' not in response
@override_settings(ROOT_URLCONF=__name__)
def test_scope1_requests_are_throttled(api_client):
"""
Ensure request rate is limited in scope1
"""
for i in range(scope1_limiter_rate):
response = api_client.get('/scope1_class')
check_response(response, 200, scope1_limiter_rate,
scope1_limiter_rate - i - 1)
response = api_client.get('/scope1_class')
check_response(response, 429, scope1_limiter_rate, 0)
for i in range(scope1_limiter_rate_post):
response = api_client.post('/scope1_class')
check_response(response, 200, scope1_limiter_rate_post,
scope1_limiter_rate_post - i - 1)
response = api_client.post('/scope1_class')
check_response(response, 429, scope1_limiter_rate_post, 0)
@override_settings(ROOT_URLCONF=__name__)
def test_scope2_requests_are_throttled(api_client):
"""
Ensure request rate is limited in scope2
"""
for i in range(scope2_limiter_rate):
response = api_client.get('/scope2_func')
check_response(response, 200, scope2_limiter_rate,
scope2_limiter_rate - i - 1)
response = api_client.get('/scope2_func')
check_response(response, 429, scope2_limiter_rate, 0)
for i in range(scope2_limiter_rate_post):
response = api_client.post('/scope2_func')
check_response(response, 200, scope2_limiter_rate_post,
scope2_limiter_rate_post - i - 1)
response = api_client.post('/scope2_func')
check_response(response, 429, scope2_limiter_rate_post, 0)
@override_settings(ROOT_URLCONF=__name__)
def test_scope3_requests_are_throttled_exempted(api_client):
"""
Ensure request rate is not limited in scope3 as
requests coming from localhost are exempted from rate limit.
"""
for _ in range(scope3_limiter_rate+1):
response = api_client.get('/scope3_class')
check_response(response, 200)
for _ in range(scope3_limiter_rate_post+1):
response = api_client.post('/scope3_class')
check_response(response, 200)
for _ in range(scope3_limiter_rate+1):
response = api_client.get('/scope3_func')
check_response(response, 200)
for _ in range(scope3_limiter_rate_post+1):
response = api_client.post('/scope3_func')
check_response(response, 200)