django-rest-framework/tests/test_versioning.py

400 lines
15 KiB
Python

import pytest
from django.test import override_settings
from django.urls import ResolverMatch, include, path, re_path
from rest_framework import serializers, status, versioning
from rest_framework.decorators import APIView
from rest_framework.relations import PKOnlyObject
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.test import (
APIRequestFactory, APITestCase, URLPatternsTestCase
)
from rest_framework.versioning import NamespaceVersioning
class RequestVersionView(APIView):
def get(self, request, *args, **kwargs):
return Response({'version': request.version})
class ReverseView(APIView):
def get(self, request, *args, **kwargs):
return Response({'url': reverse('another', request=request)})
class AllowedVersionsView(RequestVersionView):
def determine_version(self, request, *args, **kwargs):
scheme = self.versioning_class()
scheme.allowed_versions = ('v1', 'v2')
return (scheme.determine_version(request, *args, **kwargs), scheme)
class AllowedAndDefaultVersionsView(RequestVersionView):
def determine_version(self, request, *args, **kwargs):
scheme = self.versioning_class()
scheme.allowed_versions = ('v1', 'v2')
scheme.default_version = 'v2'
return (scheme.determine_version(request, *args, **kwargs), scheme)
class AllowedWithNoneVersionsView(RequestVersionView):
def determine_version(self, request, *args, **kwargs):
scheme = self.versioning_class()
scheme.allowed_versions = ('v1', 'v2', None)
return (scheme.determine_version(request, *args, **kwargs), scheme)
class AllowedWithNoneAndDefaultVersionsView(RequestVersionView):
def determine_version(self, request, *args, **kwargs):
scheme = self.versioning_class()
scheme.allowed_versions = ('v1', 'v2', None)
scheme.default_version = 'v2'
return (scheme.determine_version(request, *args, **kwargs), scheme)
factory = APIRequestFactory()
def dummy_view(request):
pass
def dummy_pk_view(request, pk):
pass
class TestRequestVersion:
def test_unversioned(self):
view = RequestVersionView.as_view()
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'version': None}
def test_query_param_versioning(self):
scheme = versioning.QueryParameterVersioning
view = RequestVersionView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/?version=1.2.3')
response = view(request)
assert response.data == {'version': '1.2.3'}
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'version': None}
@override_settings(ALLOWED_HOSTS=['*'])
def test_host_name_versioning(self):
scheme = versioning.HostNameVersioning
view = RequestVersionView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_HOST='v1.example.org')
response = view(request)
assert response.data == {'version': 'v1'}
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'version': None}
def test_accept_header_versioning(self):
scheme = versioning.AcceptHeaderVersioning
view = RequestVersionView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=1.2.3')
response = view(request)
assert response.data == {'version': '1.2.3'}
request = factory.get('/endpoint/', HTTP_ACCEPT='*/*; version=1.2.3')
response = view(request)
assert response.data == {'version': '1.2.3'}
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
response = view(request)
assert response.data == {'version': None}
def test_url_path_versioning(self):
scheme = versioning.URLPathVersioning
view = RequestVersionView.as_view(versioning_class=scheme)
request = factory.get('/1.2.3/endpoint/')
response = view(request, version='1.2.3')
assert response.data == {'version': '1.2.3'}
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'version': None}
def test_namespace_versioning(self):
class FakeResolverMatch(ResolverMatch):
namespace = 'v1'
scheme = versioning.NamespaceVersioning
view = RequestVersionView.as_view(versioning_class=scheme)
request = factory.get('/v1/endpoint/')
request.resolver_match = FakeResolverMatch
response = view(request, version='v1')
assert response.data == {'version': 'v1'}
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'version': None}
class TestURLReversing(URLPatternsTestCase, APITestCase):
included = [
path('namespaced/', dummy_view, name='another'),
path('example/<int:pk>/', dummy_pk_view, name='example-detail')
]
urlpatterns = [
path('v1/', include((included, 'v1'), namespace='v1')),
path('another/', dummy_view, name='another'),
re_path(r'^(?P<version>[v1|v2]+)/another/$', dummy_view, name='another'),
re_path(r'^(?P<foo>.+)/unversioned/$', dummy_view, name='unversioned'),
]
def test_reverse_unversioned(self):
view = ReverseView.as_view()
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'url': 'http://testserver/another/'}
def test_reverse_query_param_versioning(self):
scheme = versioning.QueryParameterVersioning
view = ReverseView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/?version=v1')
response = view(request)
assert response.data == {'url': 'http://testserver/another/?version=v1'}
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'url': 'http://testserver/another/'}
@override_settings(ALLOWED_HOSTS=['*'])
def test_reverse_host_name_versioning(self):
scheme = versioning.HostNameVersioning
view = ReverseView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_HOST='v1.example.org')
response = view(request)
assert response.data == {'url': 'http://v1.example.org/another/'}
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'url': 'http://testserver/another/'}
def test_reverse_url_path_versioning(self):
scheme = versioning.URLPathVersioning
view = ReverseView.as_view(versioning_class=scheme)
request = factory.get('/v1/endpoint/')
response = view(request, version='v1')
assert response.data == {'url': 'http://testserver/v1/another/'}
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'url': 'http://testserver/another/'}
# Test fallback when kwargs is not None
request = factory.get('/v1/endpoint/')
request.versioning_scheme = scheme()
request.version = 'v1'
reversed_url = reverse('unversioned', request=request, kwargs={'foo': 'bar'})
assert reversed_url == 'http://testserver/bar/unversioned/'
def test_reverse_namespace_versioning(self):
class FakeResolverMatch(ResolverMatch):
namespace = 'v1'
scheme = versioning.NamespaceVersioning
view = ReverseView.as_view(versioning_class=scheme)
request = factory.get('/v1/endpoint/')
request.resolver_match = FakeResolverMatch
response = view(request, version='v1')
assert response.data == {'url': 'http://testserver/v1/namespaced/'}
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'url': 'http://testserver/another/'}
class TestInvalidVersion:
def test_invalid_query_param_versioning(self):
scheme = versioning.QueryParameterVersioning
view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/?version=v3')
response = view(request)
assert response.status_code == status.HTTP_404_NOT_FOUND
@override_settings(ALLOWED_HOSTS=['*'])
def test_invalid_host_name_versioning(self):
scheme = versioning.HostNameVersioning
view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_HOST='v3.example.org')
response = view(request)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_invalid_accept_header_versioning(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=v3')
response = view(request)
assert response.status_code == status.HTTP_406_NOT_ACCEPTABLE
def test_invalid_url_path_versioning(self):
scheme = versioning.URLPathVersioning
view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/v3/endpoint/')
response = view(request, version='v3')
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_invalid_namespace_versioning(self):
class FakeResolverMatch(ResolverMatch):
namespace = 'v3'
scheme = versioning.NamespaceVersioning
view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/v3/endpoint/')
request.resolver_match = FakeResolverMatch
response = view(request, version='v3')
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAllowedAndDefaultVersion:
def test_missing_without_default(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
response = view(request)
assert response.status_code == status.HTTP_406_NOT_ACCEPTABLE
def test_missing_with_default(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
response = view(request)
assert response.status_code == status.HTTP_200_OK
assert response.data == {'version': 'v2'}
def test_with_default(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/',
HTTP_ACCEPT='application/json; version=v2')
response = view(request)
assert response.status_code == status.HTTP_200_OK
def test_missing_without_default_but_none_allowed(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedWithNoneVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
response = view(request)
assert response.status_code == status.HTTP_200_OK
assert response.data == {'version': None}
def test_missing_with_default_and_none_allowed(self):
scheme = versioning.AcceptHeaderVersioning
view = AllowedWithNoneAndDefaultVersionsView.as_view(versioning_class=scheme)
request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
response = view(request)
assert response.status_code == status.HTTP_200_OK
assert response.data == {'version': 'v2'}
class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase):
included = [
path('namespaced/<int:pk>/', dummy_pk_view, name='namespaced'),
]
urlpatterns = [
path('v1/', include((included, 'v1'), namespace='v1')),
path('v2/', include((included, 'v2'), namespace='v2'))
]
def setUp(self):
super().setUp()
class MockQueryset:
def get(self, pk):
return 'object %s' % pk
self.field = serializers.HyperlinkedRelatedField(
view_name='namespaced',
queryset=MockQueryset()
)
request = factory.get('/')
request.versioning_scheme = NamespaceVersioning()
request.version = 'v1'
self.field._context = {'request': request}
def test_bug_2489(self):
assert self.field.to_internal_value('/v1/namespaced/3/') == 'object 3'
with pytest.raises(serializers.ValidationError):
self.field.to_internal_value('/v2/namespaced/3/')
class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase, APITestCase):
nested = [
path('namespaced/<int:pk>/', dummy_pk_view, name='nested'),
]
included = [
path('namespaced/<int:pk>/', dummy_pk_view, name='namespaced'),
path('nested/', include((nested, 'nested-namespace'), namespace='nested-namespace'))
]
urlpatterns = [
path('v1/', include((included, 'restframeworkv1'), namespace='v1')),
path('v2/', include((included, 'restframeworkv2'), namespace='v2')),
path('non-api/<int:pk>/', dummy_pk_view, name='non-api-view')
]
def _create_field(self, view_name, version):
request = factory.get("/")
request.versioning_scheme = NamespaceVersioning()
request.version = version
field = serializers.HyperlinkedRelatedField(
view_name=view_name,
read_only=True)
field._context = {'request': request}
return field
def test_api_url_is_properly_reversed_with_v1(self):
field = self._create_field('namespaced', 'v1')
assert field.to_representation(PKOnlyObject(3)) == 'http://testserver/v1/namespaced/3/'
def test_api_url_is_properly_reversed_with_v2(self):
field = self._create_field('namespaced', 'v2')
assert field.to_representation(PKOnlyObject(5)) == 'http://testserver/v2/namespaced/5/'
def test_api_url_is_properly_reversed_with_nested(self):
field = self._create_field('nested', 'v1:nested-namespace')
assert field.to_representation(PKOnlyObject(3)) == 'http://testserver/v1/nested/namespaced/3/'
def test_non_api_url_is_properly_reversed_regardless_of_the_version(self):
"""
Regression test for #2711
"""
field = self._create_field('non-api-view', 'v1')
assert field.to_representation(PKOnlyObject(10)) == 'http://testserver/non-api/10/'
field = self._create_field('non-api-view', 'v2')
assert field.to_representation(PKOnlyObject(10)) == 'http://testserver/non-api/10/'