Skip to content

Commit eab9afa

Browse files
authored
Merge pull request #501 from ror-community/dev
2 parents 2d80bb5 + 91c7d85 commit eab9afa

File tree

5 files changed

+169
-1
lines changed

5 files changed

+169
-1
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Django==2.2.28
22
elasticsearch_dsl==7.4.1
33
geonamescache==1.3.0
4-
requests==2.22.0
4+
requests==2.32.4
55
requests-aws4auth==0.9
66
mock==3.0.5
77
base32_crockford==0.3.0

rorapi/middleware/__init__.py

Whitespace-only changes.

rorapi/middleware/deprecation.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from django.http import JsonResponse
2+
from django.conf import settings
3+
4+
5+
class V1DeprecationMiddleware:
6+
"""
7+
Middleware to return 410 Gone status for deprecated v1 API endpoints.
8+
9+
This middleware checks if V1_DEPRECATED setting is enabled, and if so,
10+
returns a 410 status code with a deprecation message for any requests
11+
to /v1 endpoints.
12+
"""
13+
14+
def __init__(self, get_response):
15+
self.get_response = get_response
16+
17+
def __call__(self, request):
18+
# Check if v1 deprecation is enabled and path starts with /v1
19+
if getattr(settings, 'V1_DEPRECATED', False):
20+
if request.path.startswith('/v1/') or request.path == '/v1':
21+
return JsonResponse(
22+
{
23+
'errors': [{
24+
'status': '410',
25+
'title': 'API Version Deprecated',
26+
'detail': 'The v1 API has been deprecated. Please migrate to v2.'
27+
}]
28+
},
29+
status=410
30+
)
31+
32+
response = self.get_response(request)
33+
return response

rorapi/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
MIDDLEWARE = [
6565
'django_prometheus.middleware.PrometheusBeforeMiddleware',
6666
'corsheaders.middleware.CorsMiddleware',
67+
'rorapi.middleware.deprecation.V1DeprecationMiddleware',
6768
'django.middleware.common.CommonMiddleware',
6869
'django.middleware.security.SecurityMiddleware',
6970
'django.contrib.sessions.middleware.SessionMiddleware',
@@ -305,3 +306,6 @@
305306
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
306307
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
307308
AWS_SES_REGION_NAME = os.environ.get('AWS_REGION', 'eu-west-1')
309+
310+
# API Deprecation
311+
V1_DEPRECATED = os.environ.get("V1_DEPRECATED", "False").lower() in ("true", "1", "yes")
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from django.test import TestCase, RequestFactory, override_settings
2+
from django.http import JsonResponse
3+
from rorapi.middleware.deprecation import V1DeprecationMiddleware
4+
import json
5+
6+
7+
class V1DeprecationMiddlewareTestCase(TestCase):
8+
"""
9+
Tests for V1DeprecationMiddleware that returns 410 Gone for deprecated v1 endpoints.
10+
"""
11+
12+
def setUp(self):
13+
self.factory = RequestFactory()
14+
15+
# Mock get_response function
16+
def get_response(request):
17+
return JsonResponse({'message': 'success'}, status=200)
18+
19+
self.get_response = get_response
20+
self.middleware = V1DeprecationMiddleware(self.get_response)
21+
22+
@override_settings(V1_DEPRECATED=True)
23+
def test_v1_path_returns_410_when_deprecated(self):
24+
"""Test that /v1/ paths return 410 when V1_DEPRECATED is True"""
25+
request = self.factory.get('/v1/organizations')
26+
response = self.middleware(request)
27+
28+
self.assertEqual(response.status_code, 410)
29+
content = json.loads(response.content.decode('utf-8'))
30+
self.assertIn('errors', content)
31+
self.assertEqual(content['errors'][0]['status'], '410')
32+
self.assertEqual(content['errors'][0]['title'], 'API Version Deprecated')
33+
34+
@override_settings(V1_DEPRECATED=True)
35+
def test_v1_exact_path_returns_410_when_deprecated(self):
36+
"""Test that exact /v1 path returns 410 when V1_DEPRECATED is True"""
37+
request = self.factory.get('/v1')
38+
response = self.middleware(request)
39+
40+
self.assertEqual(response.status_code, 410)
41+
content = json.loads(response.content.decode('utf-8'))
42+
self.assertIn('errors', content)
43+
44+
@override_settings(V1_DEPRECATED=True)
45+
def test_v2_path_passes_through_when_v1_deprecated(self):
46+
"""Test that /v2/ paths work normally even when V1_DEPRECATED is True"""
47+
request = self.factory.get('/v2/organizations')
48+
response = self.middleware(request)
49+
50+
self.assertEqual(response.status_code, 200)
51+
content = json.loads(response.content.decode('utf-8'))
52+
self.assertEqual(content['message'], 'success')
53+
54+
@override_settings(V1_DEPRECATED=False)
55+
def test_v1_path_passes_through_when_not_deprecated(self):
56+
"""Test that /v1/ paths work normally when V1_DEPRECATED is False"""
57+
request = self.factory.get('/v1/organizations')
58+
response = self.middleware(request)
59+
60+
self.assertEqual(response.status_code, 200)
61+
content = json.loads(response.content.decode('utf-8'))
62+
self.assertEqual(content['message'], 'success')
63+
64+
@override_settings(V1_DEPRECATED=False)
65+
def test_v2_path_passes_through_when_v1_not_deprecated(self):
66+
"""Test that /v2/ paths work normally when V1_DEPRECATED is False"""
67+
request = self.factory.get('/v2/organizations')
68+
response = self.middleware(request)
69+
70+
self.assertEqual(response.status_code, 200)
71+
content = json.loads(response.content.decode('utf-8'))
72+
self.assertEqual(content['message'], 'success')
73+
74+
@override_settings(V1_DEPRECATED=None)
75+
def test_v1_path_passes_through_when_setting_not_set(self):
76+
"""Test that /v1/ paths work when V1_DEPRECATED setting doesn't exist"""
77+
# Don't use override_settings, rely on default behavior
78+
request = self.factory.get('/v1/organizations')
79+
response = self.middleware(request)
80+
81+
self.assertEqual(response.status_code, 200)
82+
content = json.loads(response.content.decode('utf-8'))
83+
self.assertEqual(content['message'], 'success')
84+
85+
@override_settings(V1_DEPRECATED=True)
86+
def test_root_path_passes_through(self):
87+
"""Test that root path is not affected by middleware"""
88+
request = self.factory.get('/')
89+
response = self.middleware(request)
90+
91+
self.assertEqual(response.status_code, 200)
92+
93+
@override_settings(V1_DEPRECATED=True)
94+
def test_other_paths_pass_through(self):
95+
"""Test that non-v1 paths pass through normally"""
96+
request = self.factory.get('/heartbeat')
97+
response = self.middleware(request)
98+
99+
self.assertEqual(response.status_code, 200)
100+
101+
@override_settings(V1_DEPRECATED=True)
102+
def test_v1_with_query_params_returns_410(self):
103+
"""Test that /v1/ paths with query parameters return 410"""
104+
request = self.factory.get('/v1/organizations?query=test')
105+
response = self.middleware(request)
106+
107+
self.assertEqual(response.status_code, 410)
108+
109+
@override_settings(V1_DEPRECATED=True)
110+
def test_v1_post_request_returns_410(self):
111+
"""Test that POST requests to /v1/ paths return 410"""
112+
request = self.factory.post('/v1/organizations')
113+
response = self.middleware(request)
114+
115+
self.assertEqual(response.status_code, 410)
116+
117+
@override_settings(V1_DEPRECATED=True)
118+
def test_deprecation_error_message_format(self):
119+
"""Test that the deprecation error message follows the expected format"""
120+
request = self.factory.get('/v1/organizations')
121+
response = self.middleware(request)
122+
123+
content = json.loads(response.content.decode('utf-8'))
124+
self.assertIn('errors', content)
125+
self.assertEqual(len(content['errors']), 1)
126+
127+
error = content['errors'][0]
128+
self.assertIn('status', error)
129+
self.assertIn('title', error)
130+
self.assertIn('detail', error)
131+
self.assertIn('migrate to v2', error['detail'])

0 commit comments

Comments
 (0)