Skip to content

Commit

Permalink
Prepare for multiple API versions
Browse files Browse the repository at this point in the history
* Add namespace versioning to api
* Update tests to care about versions
* Ensure version is available when reversing urls
  • Loading branch information
hmpf authored Mar 23, 2021
1 parent f89a937 commit ab5d0ae
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 24 deletions.
4 changes: 2 additions & 2 deletions src/argus/incident/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import secrets

from django.db import IntegrityError
from django.urls import reverse

from django_filters import rest_framework as filters
from drf_spectacular.types import OpenApiTypes
Expand All @@ -12,6 +11,7 @@
from rest_framework.pagination import CursorPagination
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse

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

if event_type == Event.Type.ACKNOWLEDGE:
acks_endpoint = reverse("incident:incident-acks", args=[incident.pk])
acks_endpoint = reverse("incident:incident-acks", args=[incident.pk], request=self.request)
self._raise_type_validation_error(
f"Acknowledgements of this incidents should be posted through {acks_endpoint}."
)
Expand Down
22 changes: 22 additions & 0 deletions src/argus/site/api_v1_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.urls import include, path

from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

from argus.auth.views import ObtainNewAuthToken

openapi_urls = [
path("", SpectacularAPIView.as_view(api_version="v1"), name="schema-v1"),
path("swagger-ui/", SpectacularSwaggerView.as_view(url_name="v1:openapi:schema-v1"), name="swagger-ui-v1"),
]

tokenauth_urls = [
path("", ObtainNewAuthToken.as_view(), name="api-token-auth"),
]

urlpatterns = [
path("schema/", include((openapi_urls, "openapi"))),
path("auth/", include("argus.auth.urls")),
path("incidents/", include("argus.incident.urls")),
path("notificationprofiles/", include("argus.notificationprofile.urls")),
path("token-auth/", include(tokenauth_urls)),
]
22 changes: 22 additions & 0 deletions src/argus/site/api_v2_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.urls import include, path

from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

from argus.auth.views import ObtainNewAuthToken

openapi_urls = [
path("", SpectacularAPIView.as_view(api_version="v2"), name="schema"),
path("swagger-ui/", SpectacularSwaggerView.as_view(url_name="v2:openapi:schema"), name="swagger-ui"),
]

tokenauth_urls = [
path("", ObtainNewAuthToken.as_view(), name="api-token-auth"),
]

urlpatterns = [
path("schema/", include((openapi_urls, "openapi"))),
path("auth/", include("argus.auth.urls")),
path("incidents/", include("argus.incident.urls")),
path("notificationprofiles/", include("argus.notificationprofile.urls")),
path("token-auth/", include((tokenauth_urls, "auth"))),
]
2 changes: 2 additions & 0 deletions src/argus/site/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@
"rest_framework.parsers.MultiPartParser",
),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
"DEFAULT_VERSION": "v1",
"TEST_REQUEST_DEFAULT_FORMAT": "json",
"PAGE_SIZE": 100,
}
Expand Down
14 changes: 4 additions & 10 deletions src/argus/site/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@
from argus.dataporten import views as dataporten_views


api_urls = [
path("auth/", include("argus.auth.urls")),
path("incidents/", include("argus.incident.urls")),
path("notificationprofiles/", include("argus.notificationprofile.urls")),
path("token-auth/", ObtainNewAuthToken.as_view(), name="api-token-auth"),
]

psa_urls = [
# Overrides social_django's `complete` view
re_path(fr"^complete/(?P<backend>[^/]+){extra}$", dataporten_views.login_wrapper, name="complete"),
Expand All @@ -39,7 +32,8 @@
urlpatterns = [
path("admin/", admin.site.urls),
path("oidc/", include(psa_urls)),
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("api/v1/", include(api_urls)),
path("api/schema/", SpectacularAPIView.as_view(api_version="v1"), name="schema-v1-old"),
path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema-v1-old"), name="swagger-ui-v1-old"),
path("api/v1/", include(("argus.site.api_v1_urls", "api"), namespace="v1")),
path("api/v2/", include(("argus.site.api_v2_urls", "api"), namespace="v2")),
]
14 changes: 7 additions & 7 deletions tests/auth/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def expire_token(token: Token):
token.save()

def test_logout_deletes_token(self):
logout_path = reverse("auth:logout")
logout_path = reverse("v1:auth:logout")

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

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

def test_auth_token_expires_and_is_deleted(self):
some_auth_required_path = reverse("auth:current-user")
some_auth_required_path = reverse("v1:auth:current-user")

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

def test_can_get_auth_token_after_deletion_or_expiration(self):
logout_path = reverse("auth:logout")
some_auth_required_path = reverse("auth:current-user")
logout_path = reverse("v1:auth:logout")
some_auth_required_path = reverse("v1:auth:current-user")

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

def test_get_current_user_returns_correct_user(self):
current_user_path = reverse("auth:current-user")
current_user_path = reverse("v1:auth:current-user")

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

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

def assert_correct_fields_for_user(user: User):
response = self.normal_user1_client.get(user_path(user))
Expand Down
2 changes: 1 addition & 1 deletion tests/incident/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def setUp(self):
)
self.stateless_incident1 = duplicate(self.stateful_incident1, end_time=None, source_incident_id="2")

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

def tearDown(self):
connect_signals()
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion tests/incident/test_source_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def setUp(self):
self.base_admin_url = f"admin:{SourceSystem._meta.app_label}_{SourceSystem._meta.model_name}"
self.add_url = reverse(f"{self.base_admin_url}_add")
self.change_url = lambda source_system: reverse(f"{self.base_admin_url}_change", args=[source_system.pk])
self.sources_url = reverse("incident:sourcesystem-list")
self.sources_url = reverse("v1:incident:sourcesystem-list")

def _post_source1_dict(self, url: str, client: Client):
self.source1_dict = {
Expand Down
36 changes: 36 additions & 0 deletions tests/incident/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from datetime import datetime, timedelta

from django.core.exceptions import ValidationError
from django.test import TestCase, RequestFactory
from django.utils import timezone
from django.utils.timezone import is_aware, make_aware

from rest_framework import serializers, versioning

from argus.auth.factories import SourceUserFactory
from argus.incident.factories import *
from argus.incident.models import Event
from argus.incident.views import EventViewSet
from argus.util.testing import disconnect_signals, connect_signals


class EventViewSetTestCase(TestCase):
def setUp(self):
disconnect_signals()
source_type = SourceSystemTypeFactory()
source_user = SourceUserFactory()
self.source = SourceSystemFactory(type=source_type, user=source_user)

def tearDown(self):
connect_signals()

def test_validate_event_type_for_incident_acknowledge_raises_validation_error(self):
incident = IncidentFactory(source=self.source)
viewfactory = RequestFactory()
request = viewfactory.get(f"/api/v1/incidents/{incident.pk}/events/")
request.versioning_scheme = versioning.NamespaceVersioning()
request.version = "v1"
view = EventViewSet()
view.request = request
with self.assertRaises(serializers.ValidationError):
view.validate_event_type_for_incident(Event.Type.ACKNOWLEDGE, incident)
6 changes: 3 additions & 3 deletions tests/notificationprofile/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ def teardown(self):

def test_incidents_filtered_by_notification_profile_view(self):
response = self.user1_rest_client.get(
reverse("notification-profile:notificationprofile-incidents", args=[self.notification_profile1.pk])
reverse("v1:notification-profile:notificationprofile-incidents", args=[self.notification_profile1.pk])
)
response.render()
self.assertEqual(response.content, self.incident1_json)

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

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

# TODO: test more endpoints

0 comments on commit ab5d0ae

Please sign in to comment.