Skip to content

Commit ab5d0ae

Browse files
authored
Prepare for multiple API versions
* Add namespace versioning to api * Update tests to care about versions * Ensure version is available when reversing urls
1 parent f89a937 commit ab5d0ae

File tree

11 files changed

+100
-24
lines changed

11 files changed

+100
-24
lines changed

src/argus/incident/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import secrets
22

33
from django.db import IntegrityError
4-
from django.urls import reverse
54

65
from django_filters import rest_framework as filters
76
from drf_spectacular.types import OpenApiTypes
@@ -12,6 +11,7 @@
1211
from rest_framework.pagination import CursorPagination
1312
from rest_framework.permissions import IsAuthenticated
1413
from rest_framework.response import Response
14+
from rest_framework.reverse import reverse
1515

1616
from argus.auth.models import User
1717
from argus.drf.permissions import IsSuperuserOrReadOnly
@@ -323,7 +323,7 @@ def validate_incident_has_no_relation_to_event_type():
323323
self._raise_type_validation_error("Cannot change the state of a stateless incident.")
324324

325325
if event_type == Event.Type.ACKNOWLEDGE:
326-
acks_endpoint = reverse("incident:incident-acks", args=[incident.pk])
326+
acks_endpoint = reverse("incident:incident-acks", args=[incident.pk], request=self.request)
327327
self._raise_type_validation_error(
328328
f"Acknowledgements of this incidents should be posted through {acks_endpoint}."
329329
)

src/argus/site/api_v1_urls.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.urls import include, path
2+
3+
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
4+
5+
from argus.auth.views import ObtainNewAuthToken
6+
7+
openapi_urls = [
8+
path("", SpectacularAPIView.as_view(api_version="v1"), name="schema-v1"),
9+
path("swagger-ui/", SpectacularSwaggerView.as_view(url_name="v1:openapi:schema-v1"), name="swagger-ui-v1"),
10+
]
11+
12+
tokenauth_urls = [
13+
path("", ObtainNewAuthToken.as_view(), name="api-token-auth"),
14+
]
15+
16+
urlpatterns = [
17+
path("schema/", include((openapi_urls, "openapi"))),
18+
path("auth/", include("argus.auth.urls")),
19+
path("incidents/", include("argus.incident.urls")),
20+
path("notificationprofiles/", include("argus.notificationprofile.urls")),
21+
path("token-auth/", include(tokenauth_urls)),
22+
]

src/argus/site/api_v2_urls.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.urls import include, path
2+
3+
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
4+
5+
from argus.auth.views import ObtainNewAuthToken
6+
7+
openapi_urls = [
8+
path("", SpectacularAPIView.as_view(api_version="v2"), name="schema"),
9+
path("swagger-ui/", SpectacularSwaggerView.as_view(url_name="v2:openapi:schema"), name="swagger-ui"),
10+
]
11+
12+
tokenauth_urls = [
13+
path("", ObtainNewAuthToken.as_view(), name="api-token-auth"),
14+
]
15+
16+
urlpatterns = [
17+
path("schema/", include((openapi_urls, "openapi"))),
18+
path("auth/", include("argus.auth.urls")),
19+
path("incidents/", include("argus.incident.urls")),
20+
path("notificationprofiles/", include("argus.notificationprofile.urls")),
21+
path("token-auth/", include((tokenauth_urls, "auth"))),
22+
]

src/argus/site/settings/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@
199199
"rest_framework.parsers.MultiPartParser",
200200
),
201201
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
202+
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
203+
"DEFAULT_VERSION": "v1",
202204
"TEST_REQUEST_DEFAULT_FORMAT": "json",
203205
"PAGE_SIZE": 100,
204206
}

src/argus/site/urls.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,6 @@
2323
from argus.dataporten import views as dataporten_views
2424

2525

26-
api_urls = [
27-
path("auth/", include("argus.auth.urls")),
28-
path("incidents/", include("argus.incident.urls")),
29-
path("notificationprofiles/", include("argus.notificationprofile.urls")),
30-
path("token-auth/", ObtainNewAuthToken.as_view(), name="api-token-auth"),
31-
]
32-
3326
psa_urls = [
3427
# Overrides social_django's `complete` view
3528
re_path(fr"^complete/(?P<backend>[^/]+){extra}$", dataporten_views.login_wrapper, name="complete"),
@@ -39,7 +32,8 @@
3932
urlpatterns = [
4033
path("admin/", admin.site.urls),
4134
path("oidc/", include(psa_urls)),
42-
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
43-
path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
44-
path("api/v1/", include(api_urls)),
35+
path("api/schema/", SpectacularAPIView.as_view(api_version="v1"), name="schema-v1-old"),
36+
path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema-v1-old"), name="swagger-ui-v1-old"),
37+
path("api/v1/", include(("argus.site.api_v1_urls", "api"), namespace="v1")),
38+
path("api/v2/", include(("argus.site.api_v2_urls", "api"), namespace="v2")),
4539
]

tests/auth/test_auth.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def expire_token(token: Token):
3737
token.save()
3838

3939
def test_logout_deletes_token(self):
40-
logout_path = reverse("auth:logout")
40+
logout_path = reverse("v1:auth:logout")
4141

4242
def assert_token_is_deleted(token: Token, user: User, client: APIClient):
4343
self.assertTrue(hasattr(user, "auth_token"))
@@ -52,7 +52,7 @@ def assert_token_is_deleted(token: Token, user: User, client: APIClient):
5252
assert_token_is_deleted(self.superuser1_token, self.superuser1, self.superuser1_client)
5353

5454
def _successfully_get_auth_token(self, user: User, user_password: str, client: APIClient):
55-
auth_token_path = reverse("api-token-auth")
55+
auth_token_path = reverse("v1:api-token-auth")
5656
response = client.post(auth_token_path, {"username": user.username, "password": user_password})
5757
self.assertEqual(response.status_code, status.HTTP_200_OK)
5858
return response
@@ -78,7 +78,7 @@ def assert_token_is_replaced(user: User, user_password: str, old_token: Token, c
7878
)
7979

8080
def test_auth_token_expires_and_is_deleted(self):
81-
some_auth_required_path = reverse("auth:current-user")
81+
some_auth_required_path = reverse("v1:auth:current-user")
8282

8383
def assert_token_expires_and_is_deleted(user: User, token: Token, client: APIClient):
8484
self.assertEqual(client.get(some_auth_required_path).status_code, status.HTTP_200_OK)
@@ -95,8 +95,8 @@ def assert_token_expires_and_is_deleted(user: User, token: Token, client: APICli
9595
assert_token_expires_and_is_deleted(self.superuser1, self.superuser1_token, self.superuser1_client)
9696

9797
def test_can_get_auth_token_after_deletion_or_expiration(self):
98-
logout_path = reverse("auth:logout")
99-
some_auth_required_path = reverse("auth:current-user")
98+
logout_path = reverse("v1:auth:logout")
99+
some_auth_required_path = reverse("v1:auth:current-user")
100100

101101
def assert_unauthorized_until_getting_auth_token(user: User, user_password: str, client: APIClient):
102102
self.assertEqual(client.get(some_auth_required_path).status_code, status.HTTP_401_UNAUTHORIZED)
@@ -122,7 +122,7 @@ def assert_can_get_auth_token_after_deletion_and_expiration(user: User, user_pas
122122
)
123123

124124
def test_get_current_user_returns_correct_user(self):
125-
current_user_path = reverse("auth:current-user")
125+
current_user_path = reverse("v1:auth:current-user")
126126

127127
response = self.superuser1_client.get(current_user_path)
128128
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -133,7 +133,7 @@ def test_get_current_user_returns_correct_user(self):
133133
self.assertEqual(response.data["username"], self.normal_user1.username)
134134

135135
def test_get_user_returns_the_correct_fields(self):
136-
user_path = lambda user: reverse("auth:user", args=[user.pk])
136+
user_path = lambda user: reverse("v1:auth:user", args=[user.pk])
137137

138138
def assert_correct_fields_for_user(user: User):
139139
response = self.normal_user1_client.get(user_path(user))

tests/incident/test_event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def setUp(self):
3131
)
3232
self.stateless_incident1 = duplicate(self.stateful_incident1, end_time=None, source_incident_id="2")
3333

34-
self.events_url = lambda incident: reverse("incident:incident-events", args=[incident.pk])
34+
self.events_url = lambda incident: reverse("v1:incident:incident-events", args=[incident.pk])
3535

3636
def tearDown(self):
3737
connect_signals()

tests/incident/test_source_system.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def setUp(self):
2727
self.base_admin_url = f"admin:{SourceSystem._meta.app_label}_{SourceSystem._meta.model_name}"
2828
self.add_url = reverse(f"{self.base_admin_url}_add")
2929
self.change_url = lambda source_system: reverse(f"{self.base_admin_url}_change", args=[source_system.pk])
30-
self.sources_url = reverse("incident:sourcesystem-list")
30+
self.sources_url = reverse("v1:incident:sourcesystem-list")
3131

3232
def _post_source1_dict(self, url: str, client: Client):
3333
self.source1_dict = {

tests/incident/test_views.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from datetime import datetime, timedelta
2+
3+
from django.core.exceptions import ValidationError
4+
from django.test import TestCase, RequestFactory
5+
from django.utils import timezone
6+
from django.utils.timezone import is_aware, make_aware
7+
8+
from rest_framework import serializers, versioning
9+
10+
from argus.auth.factories import SourceUserFactory
11+
from argus.incident.factories import *
12+
from argus.incident.models import Event
13+
from argus.incident.views import EventViewSet
14+
from argus.util.testing import disconnect_signals, connect_signals
15+
16+
17+
class EventViewSetTestCase(TestCase):
18+
def setUp(self):
19+
disconnect_signals()
20+
source_type = SourceSystemTypeFactory()
21+
source_user = SourceUserFactory()
22+
self.source = SourceSystemFactory(type=source_type, user=source_user)
23+
24+
def tearDown(self):
25+
connect_signals()
26+
27+
def test_validate_event_type_for_incident_acknowledge_raises_validation_error(self):
28+
incident = IncidentFactory(source=self.source)
29+
viewfactory = RequestFactory()
30+
request = viewfactory.get(f"/api/v1/incidents/{incident.pk}/events/")
31+
request.versioning_scheme = versioning.NamespaceVersioning()
32+
request.version = "v1"
33+
view = EventViewSet()
34+
view.request = request
35+
with self.assertRaises(serializers.ValidationError):
36+
view.validate_event_type_for_incident(Event.Type.ACKNOWLEDGE, incident)

tests/notificationprofile/test_views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ def teardown(self):
4040

4141
def test_incidents_filtered_by_notification_profile_view(self):
4242
response = self.user1_rest_client.get(
43-
reverse("notification-profile:notificationprofile-incidents", args=[self.notification_profile1.pk])
43+
reverse("v1:notification-profile:notificationprofile-incidents", args=[self.notification_profile1.pk])
4444
)
4545
response.render()
4646
self.assertEqual(response.content, self.incident1_json)
4747

4848
def test_notification_profile_can_properly_change_timeslot(self):
4949
profile1_pk = self.notification_profile1.pk
50-
profile1_path = reverse("notification-profile:notificationprofile-detail", args=[profile1_pk])
50+
profile1_path = reverse("v1:notification-profile:notificationprofile-detail", args=[profile1_pk])
5151

5252
self.assertEqual(self.user1.notification_profiles.get(pk=profile1_pk).timeslot, self.timeslot1)
5353
self.assertEqual(self.user1_rest_client.get(profile1_path).status_code, status.HTTP_200_OK)
@@ -69,7 +69,7 @@ def test_notification_profile_can_properly_change_timeslot(self):
6969
self.notification_profile1.refresh_from_db()
7070
self.assertTrue(self.user1.notification_profiles.filter(pk=new_profile1_pk).exists())
7171
self.assertEqual(self.user1.notification_profiles.get(pk=new_profile1_pk).timeslot, self.timeslot2)
72-
new_profile1_path = reverse("notification-profile:notificationprofile-detail", args=[new_profile1_pk])
72+
new_profile1_path = reverse("v1:notification-profile:notificationprofile-detail", args=[new_profile1_pk])
7373
self.assertEqual(self.user1_rest_client.get(new_profile1_path).status_code, status.HTTP_200_OK)
7474

7575
# TODO: test more endpoints

0 commit comments

Comments
 (0)