2016-07-04 18:38:17 +03:00
import unittest
2017-09-14 11:46:34 +03:00
import pytest
2016-07-04 18:38:17 +03:00
from django . conf . urls import include , url
2016-11-02 12:04:01 +03:00
from django . core . exceptions import PermissionDenied
from django . http import Http404
2016-07-04 18:38:17 +03:00
from django . test import TestCase , override_settings
2017-10-05 12:06:09 +03:00
from rest_framework import (
filters , generics , pagination , permissions , serializers
)
2017-12-20 11:10:28 +03:00
from rest_framework . compat import coreapi , coreschema , get_regex_pattern , path
2018-01-25 11:40:49 +03:00
from rest_framework . decorators import action , api_view , schema
2016-11-02 12:04:01 +03:00
from rest_framework . request import Request
2017-10-05 12:06:09 +03:00
from rest_framework . routers import DefaultRouter , SimpleRouter
2017-09-14 11:46:34 +03:00
from rest_framework . schemas import (
AutoSchema , ManualSchema , SchemaGenerator , get_schema_view
)
2017-09-20 12:29:47 +03:00
from rest_framework . schemas . generators import EndpointEnumerator
2017-10-05 21:41:14 +03:00
from rest_framework . schemas . utils import is_list_view
2016-11-02 12:04:01 +03:00
from rest_framework . test import APIClient , APIRequestFactory
2017-09-14 14:20:41 +03:00
from rest_framework . utils import formatting
2016-07-27 17:36:36 +03:00
from rest_framework . views import APIView
2017-10-05 12:06:09 +03:00
from rest_framework . viewsets import GenericViewSet , ModelViewSet
2018-02-01 18:14:35 +03:00
from . models import BasicModel , ForeignKeySource
2016-07-04 18:38:17 +03:00
2016-11-02 12:04:01 +03:00
factory = APIRequestFactory ( )
2016-07-04 18:38:17 +03:00
class MockUser ( object ) :
def is_authenticated ( self ) :
return True
class ExamplePagination ( pagination . PageNumberPagination ) :
page_size = 100
2016-10-12 18:09:45 +03:00
page_size_query_param = ' page_size '
2016-07-04 18:38:17 +03:00
2016-10-10 15:03:46 +03:00
class EmptySerializer ( serializers . Serializer ) :
pass
2016-07-04 18:38:17 +03:00
class ExampleSerializer ( serializers . Serializer ) :
2016-08-11 18:18:33 +03:00
a = serializers . CharField ( required = True , help_text = ' A field description ' )
2016-07-04 18:38:17 +03:00
b = serializers . CharField ( required = False )
2016-08-22 18:53:53 +03:00
read_only = serializers . CharField ( read_only = True )
hidden = serializers . HiddenField ( default = ' hello ' )
2016-07-04 18:38:17 +03:00
2018-01-23 23:06:24 +03:00
class AnotherSerializerWithDictField ( serializers . Serializer ) :
a = serializers . DictField ( )
2017-08-20 19:12:56 +03:00
class AnotherSerializerWithListFields ( serializers . Serializer ) :
a = serializers . ListField ( child = serializers . IntegerField ( ) )
b = serializers . ListSerializer ( child = serializers . CharField ( ) )
2016-08-05 13:19:39 +03:00
class AnotherSerializer ( serializers . Serializer ) :
c = serializers . CharField ( required = True )
d = serializers . CharField ( required = False )
2016-07-04 18:38:17 +03:00
class ExampleViewSet ( ModelViewSet ) :
pagination_class = ExamplePagination
permission_classes = [ permissions . IsAuthenticatedOrReadOnly ]
filter_backends = [ filters . OrderingFilter ]
serializer_class = ExampleSerializer
2018-01-25 11:40:49 +03:00
@action ( methods = [ ' post ' ] , detail = True , serializer_class = AnotherSerializer )
2016-08-05 13:19:39 +03:00
def custom_action ( self , request , pk ) :
2016-10-10 15:03:46 +03:00
"""
A description of custom action .
"""
2016-08-05 13:19:39 +03:00
return super ( ExampleSerializer , self ) . retrieve ( self , request )
2018-01-23 23:06:24 +03:00
@action ( methods = [ ' post ' ] , detail = True , serializer_class = AnotherSerializerWithDictField )
def custom_action_with_dict_field ( self , request , pk ) :
"""
A custom action using a dict field in the serializer .
"""
return super ( ExampleSerializer , self ) . retrieve ( self , request )
2018-01-25 11:40:49 +03:00
@action ( methods = [ ' post ' ] , detail = True , serializer_class = AnotherSerializerWithListFields )
2017-08-20 19:12:56 +03:00
def custom_action_with_list_fields ( self , request , pk ) :
"""
A custom action using both list field and list serializer in the serializer .
"""
return super ( ExampleSerializer , self ) . retrieve ( self , request )
2018-01-25 11:40:49 +03:00
@action ( detail = False )
2016-08-11 16:07:40 +03:00
def custom_list_action ( self , request ) :
return super ( ExampleViewSet , self ) . list ( self , request )
2018-01-25 11:40:49 +03:00
@action ( methods = [ ' post ' , ' get ' ] , detail = False , serializer_class = EmptySerializer )
2016-10-10 15:03:46 +03:00
def custom_list_action_multiple_methods ( self , request ) :
return super ( ExampleViewSet , self ) . list ( self , request )
2016-08-11 13:27:28 +03:00
def get_serializer ( self , * args , * * kwargs ) :
assert self . request
2016-08-15 19:10:55 +03:00
assert self . action
2016-08-11 13:27:28 +03:00
return super ( ExampleViewSet , self ) . get_serializer ( * args , * * kwargs )
2017-05-29 22:15:07 +03:00
2016-10-10 15:03:46 +03:00
if coreapi :
schema_view = get_schema_view ( title = ' Example API ' )
else :
def schema_view ( request ) :
pass
2016-07-27 17:36:36 +03:00
2016-10-10 15:03:46 +03:00
router = DefaultRouter ( )
2016-07-04 18:38:17 +03:00
router . register ( ' example ' , ExampleViewSet , base_name = ' example ' )
urlpatterns = [
2016-10-10 15:03:46 +03:00
url ( r ' ^$ ' , schema_view ) ,
2016-07-04 18:38:17 +03:00
url ( r ' ^ ' , include ( router . urls ) )
]
@unittest.skipUnless ( coreapi , ' coreapi is not installed ' )
@override_settings ( ROOT_URLCONF = ' tests.test_schemas ' )
class TestRouterGeneratedSchema ( TestCase ) :
def test_anonymous_request ( self ) :
client = APIClient ( )
2016-10-10 15:03:46 +03:00
response = client . get ( ' / ' , HTTP_ACCEPT = ' application/coreapi+json ' )
2017-01-12 19:33:24 +03:00
assert response . status_code == 200
2016-07-04 18:38:17 +03:00
expected = coreapi . Document (
2017-03-03 18:24:37 +03:00
url = ' http://testserver/ ' ,
2016-07-04 18:38:17 +03:00
title = ' Example API ' ,
content = {
' example ' : {
' list ' : coreapi . Link (
url = ' /example/ ' ,
action = ' get ' ,
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' page ' , required = False , location = ' query ' , schema = coreschema . Integer ( title = ' Page ' , description = ' A page number within the paginated result set. ' ) ) ,
coreapi . Field ( ' page_size ' , required = False , location = ' query ' , schema = coreschema . Integer ( title = ' Page size ' , description = ' Number of results to return per page. ' ) ) ,
coreapi . Field ( ' ordering ' , required = False , location = ' query ' , schema = coreschema . String ( title = ' Ordering ' , description = ' Which field to use when ordering the results. ' ) )
2016-07-04 18:38:17 +03:00
]
) ,
2016-08-11 16:07:40 +03:00
' custom_list_action ' : coreapi . Link (
url = ' /example/custom_list_action/ ' ,
action = ' get '
) ,
2016-10-10 15:03:46 +03:00
' custom_list_action_multiple_methods ' : {
' read ' : coreapi . Link (
url = ' /example/custom_list_action_multiple_methods/ ' ,
action = ' get '
)
} ,
' read ' : coreapi . Link (
url = ' /example/ {id} / ' ,
2016-07-04 18:38:17 +03:00
action = ' get ' ,
fields = [
2017-09-27 10:13:10 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) ) ,
coreapi . Field ( ' ordering ' , required = False , location = ' query ' , schema = coreschema . String ( title = ' Ordering ' , description = ' Which field to use when ordering the results. ' ) )
2016-07-04 18:38:17 +03:00
]
)
}
}
)
2017-01-12 19:33:24 +03:00
assert response . data == expected
2016-07-04 18:38:17 +03:00
def test_authenticated_request ( self ) :
client = APIClient ( )
client . force_authenticate ( MockUser ( ) )
2016-10-10 15:03:46 +03:00
response = client . get ( ' / ' , HTTP_ACCEPT = ' application/coreapi+json ' )
2017-01-12 19:33:24 +03:00
assert response . status_code == 200
2016-07-04 18:38:17 +03:00
expected = coreapi . Document (
2017-03-03 18:24:37 +03:00
url = ' http://testserver/ ' ,
2016-07-04 18:38:17 +03:00
title = ' Example API ' ,
content = {
' example ' : {
' list ' : coreapi . Link (
url = ' /example/ ' ,
action = ' get ' ,
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' page ' , required = False , location = ' query ' , schema = coreschema . Integer ( title = ' Page ' , description = ' A page number within the paginated result set. ' ) ) ,
coreapi . Field ( ' page_size ' , required = False , location = ' query ' , schema = coreschema . Integer ( title = ' Page size ' , description = ' Number of results to return per page. ' ) ) ,
coreapi . Field ( ' ordering ' , required = False , location = ' query ' , schema = coreschema . String ( title = ' Ordering ' , description = ' Which field to use when ordering the results. ' ) )
2016-07-04 18:38:17 +03:00
]
) ,
' create ' : coreapi . Link (
url = ' /example/ ' ,
action = ' post ' ,
encoding = ' application/json ' ,
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' a ' , required = True , location = ' form ' , schema = coreschema . String ( title = ' A ' , description = ' A field description ' ) ) ,
coreapi . Field ( ' b ' , required = False , location = ' form ' , schema = coreschema . String ( title = ' B ' ) )
2016-07-04 18:38:17 +03:00
]
) ,
2016-10-10 15:03:46 +03:00
' read ' : coreapi . Link (
url = ' /example/ {id} / ' ,
2016-07-04 18:38:17 +03:00
action = ' get ' ,
fields = [
2017-09-27 10:13:10 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) ) ,
coreapi . Field ( ' ordering ' , required = False , location = ' query ' , schema = coreschema . String ( title = ' Ordering ' , description = ' Which field to use when ordering the results. ' ) )
2016-07-04 18:38:17 +03:00
]
) ,
2016-08-05 13:19:39 +03:00
' custom_action ' : coreapi . Link (
2016-10-10 15:03:46 +03:00
url = ' /example/ {id} /custom_action/ ' ,
2016-08-05 13:19:39 +03:00
action = ' post ' ,
encoding = ' application/json ' ,
2016-10-10 15:03:46 +03:00
description = ' A description of custom action. ' ,
2016-08-05 13:19:39 +03:00
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) ) ,
coreapi . Field ( ' c ' , required = True , location = ' form ' , schema = coreschema . String ( title = ' C ' ) ) ,
coreapi . Field ( ' d ' , required = False , location = ' form ' , schema = coreschema . String ( title = ' D ' ) ) ,
2016-08-05 13:19:39 +03:00
]
) ,
2018-01-23 23:06:24 +03:00
' custom_action_with_dict_field ' : coreapi . Link (
url = ' /example/ {id} /custom_action_with_dict_field/ ' ,
action = ' post ' ,
encoding = ' application/json ' ,
description = ' A custom action using a dict field in the serializer. ' ,
fields = [
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) ) ,
coreapi . Field ( ' a ' , required = True , location = ' form ' , schema = coreschema . Object ( title = ' A ' ) ) ,
]
) ,
2017-08-20 19:12:56 +03:00
' custom_action_with_list_fields ' : coreapi . Link (
url = ' /example/ {id} /custom_action_with_list_fields/ ' ,
action = ' post ' ,
encoding = ' application/json ' ,
description = ' A custom action using both list field and list serializer in the serializer. ' ,
fields = [
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) ) ,
coreapi . Field ( ' a ' , required = True , location = ' form ' , schema = coreschema . Array ( title = ' A ' , items = coreschema . Integer ( ) ) ) ,
coreapi . Field ( ' b ' , required = True , location = ' form ' , schema = coreschema . Array ( title = ' B ' , items = coreschema . String ( ) ) ) ,
]
) ,
2016-08-11 16:07:40 +03:00
' custom_list_action ' : coreapi . Link (
url = ' /example/custom_list_action/ ' ,
action = ' get '
) ,
2016-10-10 15:03:46 +03:00
' custom_list_action_multiple_methods ' : {
' read ' : coreapi . Link (
url = ' /example/custom_list_action_multiple_methods/ ' ,
action = ' get '
) ,
' create ' : coreapi . Link (
url = ' /example/custom_list_action_multiple_methods/ ' ,
action = ' post '
)
} ,
2016-07-04 18:38:17 +03:00
' update ' : coreapi . Link (
2016-10-10 15:03:46 +03:00
url = ' /example/ {id} / ' ,
2016-07-04 18:38:17 +03:00
action = ' put ' ,
encoding = ' application/json ' ,
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) ) ,
coreapi . Field ( ' a ' , required = True , location = ' form ' , schema = coreschema . String ( title = ' A ' , description = ( ' A field description ' ) ) ) ,
2017-09-27 10:13:10 +03:00
coreapi . Field ( ' b ' , required = False , location = ' form ' , schema = coreschema . String ( title = ' B ' ) ) ,
coreapi . Field ( ' ordering ' , required = False , location = ' query ' , schema = coreschema . String ( title = ' Ordering ' , description = ' Which field to use when ordering the results. ' ) )
2016-07-04 18:38:17 +03:00
]
) ,
' partial_update ' : coreapi . Link (
2016-10-10 15:03:46 +03:00
url = ' /example/ {id} / ' ,
2016-07-04 18:38:17 +03:00
action = ' patch ' ,
encoding = ' application/json ' ,
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) ) ,
coreapi . Field ( ' a ' , required = False , location = ' form ' , schema = coreschema . String ( title = ' A ' , description = ' A field description ' ) ) ,
2017-09-27 10:13:10 +03:00
coreapi . Field ( ' b ' , required = False , location = ' form ' , schema = coreschema . String ( title = ' B ' ) ) ,
coreapi . Field ( ' ordering ' , required = False , location = ' query ' , schema = coreschema . String ( title = ' Ordering ' , description = ' Which field to use when ordering the results. ' ) )
2016-07-04 18:38:17 +03:00
]
) ,
2016-10-10 15:03:46 +03:00
' delete ' : coreapi . Link (
url = ' /example/ {id} / ' ,
2016-07-04 18:38:17 +03:00
action = ' delete ' ,
fields = [
2017-09-27 10:13:10 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) ) ,
coreapi . Field ( ' ordering ' , required = False , location = ' query ' , schema = coreschema . String ( title = ' Ordering ' , description = ' Which field to use when ordering the results. ' ) )
2016-07-04 18:38:17 +03:00
]
)
}
}
)
2017-01-12 19:33:24 +03:00
assert response . data == expected
2016-07-27 17:36:36 +03:00
2016-11-02 12:04:01 +03:00
class DenyAllUsingHttp404 ( permissions . BasePermission ) :
def has_permission ( self , request , view ) :
raise Http404 ( )
def has_object_permission ( self , request , view , obj ) :
raise Http404 ( )
class DenyAllUsingPermissionDenied ( permissions . BasePermission ) :
def has_permission ( self , request , view ) :
raise PermissionDenied ( )
def has_object_permission ( self , request , view , obj ) :
raise PermissionDenied ( )
class Http404ExampleViewSet ( ExampleViewSet ) :
permission_classes = [ DenyAllUsingHttp404 ]
class PermissionDeniedExampleViewSet ( ExampleViewSet ) :
permission_classes = [ DenyAllUsingPermissionDenied ]
2017-04-19 06:44:18 +03:00
class MethodLimitedViewSet ( ExampleViewSet ) :
permission_classes = [ ]
http_method_names = [ ' get ' , ' head ' , ' options ' ]
2016-10-10 15:03:46 +03:00
class ExampleListView ( APIView ) :
permission_classes = [ permissions . IsAuthenticatedOrReadOnly ]
def get ( self , * args , * * kwargs ) :
pass
def post ( self , request , * args , * * kwargs ) :
pass
class ExampleDetailView ( APIView ) :
permission_classes = [ permissions . IsAuthenticatedOrReadOnly ]
def get ( self , * args , * * kwargs ) :
pass
2016-07-27 17:36:36 +03:00
@unittest.skipUnless ( coreapi , ' coreapi is not installed ' )
class TestSchemaGenerator ( TestCase ) :
2016-10-10 15:03:46 +03:00
def setUp ( self ) :
self . patterns = [
2017-11-05 21:09:38 +03:00
url ( r ' ^example/?$ ' , ExampleListView . as_view ( ) ) ,
url ( r ' ^example/(?P<pk> \ d+)/?$ ' , ExampleDetailView . as_view ( ) ) ,
url ( r ' ^example/(?P<pk> \ d+)/sub/?$ ' , ExampleDetailView . as_view ( ) ) ,
2016-10-10 15:03:46 +03:00
]
def test_schema_for_regular_views ( self ) :
"""
Ensure that schema generation works for APIView classes .
"""
generator = SchemaGenerator ( title = ' Example API ' , patterns = self . patterns )
2017-12-20 11:10:28 +03:00
schema = generator . get_schema ( )
expected = coreapi . Document (
url = ' ' ,
title = ' Example API ' ,
content = {
' example ' : {
' create ' : coreapi . Link (
url = ' /example/ ' ,
action = ' post ' ,
fields = [ ]
) ,
' list ' : coreapi . Link (
url = ' /example/ ' ,
action = ' get ' ,
fields = [ ]
) ,
' read ' : coreapi . Link (
url = ' /example/ {id} / ' ,
action = ' get ' ,
fields = [
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) )
]
) ,
' sub ' : {
' list ' : coreapi . Link (
url = ' /example/ {id} /sub/ ' ,
action = ' get ' ,
fields = [
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) )
]
)
}
}
}
)
assert schema == expected
@unittest.skipUnless ( coreapi , ' coreapi is not installed ' )
@unittest.skipUnless ( path , ' needs Django 2 ' )
class TestSchemaGeneratorDjango2 ( TestCase ) :
def setUp ( self ) :
self . patterns = [
path ( ' example/ ' , ExampleListView . as_view ( ) ) ,
path ( ' example/<int:pk>/ ' , ExampleDetailView . as_view ( ) ) ,
path ( ' example/<int:pk>/sub/ ' , ExampleDetailView . as_view ( ) ) ,
]
def test_schema_for_regular_views ( self ) :
"""
Ensure that schema generation works for APIView classes .
"""
generator = SchemaGenerator ( title = ' Example API ' , patterns = self . patterns )
2016-10-10 15:03:46 +03:00
schema = generator . get_schema ( )
2016-07-27 17:36:36 +03:00
expected = coreapi . Document (
url = ' ' ,
2016-10-10 15:03:46 +03:00
title = ' Example API ' ,
2016-07-27 17:36:36 +03:00
content = {
2016-10-10 15:03:46 +03:00
' example ' : {
2016-07-27 17:36:36 +03:00
' create ' : coreapi . Link (
2016-10-10 15:03:46 +03:00
url = ' /example/ ' ,
2016-07-27 17:36:36 +03:00
action = ' post ' ,
fields = [ ]
) ,
2016-10-10 15:03:46 +03:00
' list ' : coreapi . Link (
url = ' /example/ ' ,
action = ' get ' ,
fields = [ ]
) ,
2016-07-27 17:36:36 +03:00
' read ' : coreapi . Link (
2016-10-10 15:03:46 +03:00
url = ' /example/ {id} / ' ,
2016-07-27 17:36:36 +03:00
action = ' get ' ,
2016-10-10 15:03:46 +03:00
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) )
2016-10-10 15:03:46 +03:00
]
) ,
' sub ' : {
' list ' : coreapi . Link (
url = ' /example/ {id} /sub/ ' ,
action = ' get ' ,
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) )
2016-10-10 15:03:46 +03:00
]
)
}
}
}
)
2017-01-12 19:33:24 +03:00
assert schema == expected
2016-10-10 15:03:46 +03:00
@unittest.skipUnless ( coreapi , ' coreapi is not installed ' )
class TestSchemaGeneratorNotAtRoot ( TestCase ) :
def setUp ( self ) :
self . patterns = [
2017-11-05 21:09:38 +03:00
url ( r ' ^api/v1/example/?$ ' , ExampleListView . as_view ( ) ) ,
url ( r ' ^api/v1/example/(?P<pk> \ d+)/?$ ' , ExampleDetailView . as_view ( ) ) ,
url ( r ' ^api/v1/example/(?P<pk> \ d+)/sub/?$ ' , ExampleDetailView . as_view ( ) ) ,
2016-10-10 15:03:46 +03:00
]
def test_schema_for_regular_views ( self ) :
"""
Ensure that schema generation with an API that is not at the URL
root continues to use correct structure for link keys .
"""
generator = SchemaGenerator ( title = ' Example API ' , patterns = self . patterns )
schema = generator . get_schema ( )
expected = coreapi . Document (
url = ' ' ,
title = ' Example API ' ,
content = {
' example ' : {
' create ' : coreapi . Link (
url = ' /api/v1/example/ ' ,
action = ' post ' ,
2016-07-27 17:36:36 +03:00
fields = [ ]
2016-10-10 15:03:46 +03:00
) ,
' list ' : coreapi . Link (
url = ' /api/v1/example/ ' ,
action = ' get ' ,
fields = [ ]
) ,
' read ' : coreapi . Link (
url = ' /api/v1/example/ {id} / ' ,
action = ' get ' ,
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) )
2016-10-10 15:03:46 +03:00
]
) ,
' sub ' : {
' list ' : coreapi . Link (
url = ' /api/v1/example/ {id} /sub/ ' ,
action = ' get ' ,
fields = [
2017-03-03 18:24:37 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) )
2016-10-10 15:03:46 +03:00
]
)
}
2016-07-27 17:36:36 +03:00
}
}
)
2017-01-12 19:33:24 +03:00
assert schema == expected
2016-10-21 18:59:34 +03:00
2017-04-19 06:44:18 +03:00
@unittest.skipUnless ( coreapi , ' coreapi is not installed ' )
class TestSchemaGeneratorWithMethodLimitedViewSets ( TestCase ) :
def setUp ( self ) :
router = DefaultRouter ( )
router . register ( ' example1 ' , MethodLimitedViewSet , base_name = ' example1 ' )
self . patterns = [
url ( r ' ^ ' , include ( router . urls ) )
]
def test_schema_for_regular_views ( self ) :
"""
Ensure that schema generation works for ViewSet classes
with method limitation by Django CBV ' s http_method_names attribute
"""
generator = SchemaGenerator ( title = ' Example API ' , patterns = self . patterns )
request = factory . get ( ' /example1/ ' )
schema = generator . get_schema ( Request ( request ) )
expected = coreapi . Document (
url = ' http://testserver/example1/ ' ,
title = ' Example API ' ,
content = {
' example1 ' : {
' list ' : coreapi . Link (
url = ' /example1/ ' ,
action = ' get ' ,
fields = [
coreapi . Field ( ' page ' , required = False , location = ' query ' , schema = coreschema . Integer ( title = ' Page ' , description = ' A page number within the paginated result set. ' ) ) ,
coreapi . Field ( ' page_size ' , required = False , location = ' query ' , schema = coreschema . Integer ( title = ' Page size ' , description = ' Number of results to return per page. ' ) ) ,
coreapi . Field ( ' ordering ' , required = False , location = ' query ' , schema = coreschema . String ( title = ' Ordering ' , description = ' Which field to use when ordering the results. ' ) )
]
) ,
' custom_list_action ' : coreapi . Link (
url = ' /example1/custom_list_action/ ' ,
action = ' get '
) ,
' custom_list_action_multiple_methods ' : {
' read ' : coreapi . Link (
url = ' /example1/custom_list_action_multiple_methods/ ' ,
action = ' get '
)
} ,
' read ' : coreapi . Link (
url = ' /example1/ {id} / ' ,
action = ' get ' ,
fields = [
2017-09-27 10:13:10 +03:00
coreapi . Field ( ' id ' , required = True , location = ' path ' , schema = coreschema . String ( ) ) ,
coreapi . Field ( ' ordering ' , required = False , location = ' query ' , schema = coreschema . String ( title = ' Ordering ' , description = ' Which field to use when ordering the results. ' ) )
2017-04-19 06:44:18 +03:00
]
)
}
}
)
assert schema == expected
2016-11-02 12:04:01 +03:00
@unittest.skipUnless ( coreapi , ' coreapi is not installed ' )
class TestSchemaGeneratorWithRestrictedViewSets ( TestCase ) :
def setUp ( self ) :
router = DefaultRouter ( )
router . register ( ' example1 ' , Http404ExampleViewSet , base_name = ' example1 ' )
router . register ( ' example2 ' , PermissionDeniedExampleViewSet , base_name = ' example2 ' )
self . patterns = [
url ( ' ^example/?$ ' , ExampleListView . as_view ( ) ) ,
url ( r ' ^ ' , include ( router . urls ) )
]
def test_schema_for_regular_views ( self ) :
"""
Ensure that schema generation works for ViewSet classes
with permission classes raising exceptions .
"""
generator = SchemaGenerator ( title = ' Example API ' , patterns = self . patterns )
request = factory . get ( ' / ' )
schema = generator . get_schema ( Request ( request ) )
expected = coreapi . Document (
2017-03-03 18:24:37 +03:00
url = ' http://testserver/ ' ,
2016-11-02 12:04:01 +03:00
title = ' Example API ' ,
content = {
' example ' : {
' list ' : coreapi . Link (
url = ' /example/ ' ,
action = ' get ' ,
fields = [ ]
) ,
} ,
}
)
2017-01-12 19:33:24 +03:00
assert schema == expected
2016-11-02 12:04:01 +03:00
2018-02-01 18:14:35 +03:00
class ForeignKeySourceSerializer ( serializers . ModelSerializer ) :
class Meta :
model = ForeignKeySource
fields = ( ' id ' , ' name ' , ' target ' )
class ForeignKeySourceView ( generics . CreateAPIView ) :
queryset = ForeignKeySource . objects . all ( )
serializer_class = ForeignKeySourceSerializer
@unittest.skipUnless ( coreapi , ' coreapi is not installed ' )
class TestSchemaGeneratorWithForeignKey ( TestCase ) :
def setUp ( self ) :
self . patterns = [
url ( r ' ^example/?$ ' , ForeignKeySourceView . as_view ( ) ) ,
]
def test_schema_for_regular_views ( self ) :
"""
Ensure that AutoField foreign keys are output as Integer .
"""
generator = SchemaGenerator ( title = ' Example API ' , patterns = self . patterns )
schema = generator . get_schema ( )
expected = coreapi . Document (
url = ' ' ,
title = ' Example API ' ,
content = {
' example ' : {
' create ' : coreapi . Link (
url = ' /example/ ' ,
action = ' post ' ,
encoding = ' application/json ' ,
fields = [
coreapi . Field ( ' name ' , required = True , location = ' form ' , schema = coreschema . String ( title = ' Name ' ) ) ,
coreapi . Field ( ' target ' , required = True , location = ' form ' , schema = coreschema . Integer ( description = ' Target ' , title = ' Target ' ) ) ,
]
)
}
}
)
assert schema == expected
2016-10-21 18:59:34 +03:00
@unittest.skipUnless ( coreapi , ' coreapi is not installed ' )
class Test4605Regression ( TestCase ) :
def test_4605_regression ( self ) :
generator = SchemaGenerator ( )
prefix = generator . determine_path_prefix ( [
' /api/v1/items/ ' ,
' /auth/convert-token/ '
] )
assert prefix == ' / '
2017-09-14 11:46:34 +03:00
2017-12-14 13:24:21 +03:00
class CustomViewInspector ( AutoSchema ) :
""" A dummy AutoSchema subclass """
pass
2017-12-04 11:07:43 +03:00
class TestAutoSchema ( TestCase ) :
2017-09-14 11:46:34 +03:00
def test_apiview_schema_descriptor ( self ) :
view = APIView ( )
assert hasattr ( view , ' schema ' )
assert isinstance ( view . schema , AutoSchema )
2017-12-14 13:24:21 +03:00
def test_set_custom_inspector_class_on_view ( self ) :
class CustomView ( APIView ) :
schema = CustomViewInspector ( )
view = CustomView ( )
assert isinstance ( view . schema , CustomViewInspector )
def test_set_custom_inspector_class_via_settings ( self ) :
with override_settings ( REST_FRAMEWORK = { ' DEFAULT_SCHEMA_CLASS ' : ' tests.test_schemas.CustomViewInspector ' } ) :
view = APIView ( )
assert isinstance ( view . schema , CustomViewInspector )
2017-09-14 11:46:34 +03:00
def test_get_link_requires_instance ( self ) :
descriptor = APIView . schema # Accessed from class
with pytest . raises ( AssertionError ) :
descriptor . get_link ( None , None , None ) # ???: Do the dummy arguments require a tighter assert?
2017-12-04 11:07:43 +03:00
def test_update_fields ( self ) :
"""
That updating fields by - name helper is correct
Recall : ` update_fields ( fields , update_with ) `
"""
schema = AutoSchema ( )
fields = [ ]
# Adds a field...
fields = schema . update_fields ( fields , [
coreapi . Field (
" my_field " ,
required = True ,
location = " path " ,
schema = coreschema . String ( )
) ,
] )
assert len ( fields ) == 1
assert fields [ 0 ] . name == " my_field "
# Replaces a field...
fields = schema . update_fields ( fields , [
coreapi . Field (
" my_field " ,
required = False ,
location = " path " ,
schema = coreschema . String ( )
) ,
] )
assert len ( fields ) == 1
assert fields [ 0 ] . required is False
def test_get_manual_fields ( self ) :
""" That get_manual_fields is applied during get_link """
2017-09-14 11:46:34 +03:00
class CustomView ( APIView ) :
schema = AutoSchema ( manual_fields = [
coreapi . Field (
" my_extra_field " ,
required = True ,
location = " path " ,
schema = coreschema . String ( )
) ,
] )
view = CustomView ( )
link = view . schema . get_link ( ' /a/url/ {id} / ' , ' GET ' , ' ' )
fields = link . fields
assert len ( fields ) == 2
assert " my_extra_field " in [ f . name for f in fields ]
def test_view_with_manual_schema ( self ) :
path = ' /example '
method = ' get '
base_url = None
fields = [
coreapi . Field (
" first_field " ,
required = True ,
location = " path " ,
schema = coreschema . String ( )
) ,
coreapi . Field (
" second_field " ,
required = True ,
location = " path " ,
schema = coreschema . String ( )
) ,
coreapi . Field (
" third_field " ,
required = True ,
location = " path " ,
schema = coreschema . String ( )
) ,
]
description = " A test endpoint "
class CustomView ( APIView ) :
"""
ManualSchema takes list of fields for endpoint .
- Provides url and action , which are always dynamic
"""
schema = ManualSchema ( fields , description )
expected = coreapi . Link (
url = path ,
action = method ,
fields = fields ,
description = description
)
view = CustomView ( )
link = view . schema . get_link ( path , method , base_url )
assert link == expected
2017-09-14 14:20:41 +03:00
def test_docstring_is_not_stripped_by_get_description ( ) :
class ExampleDocstringAPIView ( APIView ) :
"""
== = title
* item a
* item a - a
* item a - b
* item b
- item 1
- item 2
code block begin
code
code
code
code block end
the end
"""
def get ( self , * args , * * kwargs ) :
pass
def post ( self , request , * args , * * kwargs ) :
pass
view = ExampleDocstringAPIView ( )
schema = view . schema
descr = schema . get_description ( ' example ' , ' get ' )
# the first and last character are '\n' correctly removed by get_description
assert descr == formatting . dedent ( ExampleDocstringAPIView . __doc__ [ 1 : ] [ : - 1 ] )
2017-09-20 12:29:47 +03:00
# Views for SchemaGenerationExclusionTests
class ExcludedAPIView ( APIView ) :
schema = None
def get ( self , request , * args , * * kwargs ) :
pass
@api_view ( [ ' GET ' ] )
@schema ( None )
def excluded_fbv ( request ) :
pass
@api_view ( [ ' GET ' ] )
def included_fbv ( request ) :
pass
@unittest.skipUnless ( coreapi , ' coreapi is not installed ' )
class SchemaGenerationExclusionTests ( TestCase ) :
def setUp ( self ) :
self . patterns = [
url ( ' ^excluded-cbv/$ ' , ExcludedAPIView . as_view ( ) ) ,
url ( ' ^excluded-fbv/$ ' , excluded_fbv ) ,
url ( ' ^included-fbv/$ ' , included_fbv ) ,
]
def test_schema_generator_excludes_correctly ( self ) :
""" Schema should not include excluded views """
generator = SchemaGenerator ( title = ' Exclusions ' , patterns = self . patterns )
schema = generator . get_schema ( )
expected = coreapi . Document (
url = ' ' ,
title = ' Exclusions ' ,
content = {
' included-fbv ' : {
' list ' : coreapi . Link ( url = ' /included-fbv/ ' , action = ' get ' )
}
}
)
assert len ( schema . data ) == 1
assert ' included-fbv ' in schema . data
assert schema == expected
def test_endpoint_enumerator_excludes_correctly ( self ) :
""" It is responsibility of EndpointEnumerator to exclude views """
inspector = EndpointEnumerator ( self . patterns )
endpoints = inspector . get_api_endpoints ( )
assert len ( endpoints ) == 1
path , method , callback = endpoints [ 0 ]
assert path == ' /included-fbv/ '
def test_should_include_endpoint_excludes_correctly ( self ) :
""" This is the specific method that should handle the exclusion """
inspector = EndpointEnumerator ( self . patterns )
# Not pretty. Mimics internals of EndpointEnumerator to put should_include_endpoint under test
2017-10-16 12:31:13 +03:00
pairs = [ ( inspector . get_path_from_regex ( get_regex_pattern ( pattern ) ) , pattern . callback )
2017-09-20 12:29:47 +03:00
for pattern in self . patterns ]
should_include = [
inspector . should_include_endpoint ( * pair ) for pair in pairs
]
expected = [ False , False , True ]
assert should_include == expected
def test_deprecations ( self ) :
with pytest . warns ( PendingDeprecationWarning ) as record :
@api_view ( [ " GET " ] , exclude_from_schema = True )
def view ( request ) :
pass
assert len ( record ) == 1
assert str ( record [ 0 ] . message ) == (
" The `exclude_from_schema` argument to `api_view` is pending "
" deprecation. Use the `schema` decorator instead, passing `None`. "
)
class OldFashionedExcludedView ( APIView ) :
exclude_from_schema = True
def get ( self , request , * args , * * kwargs ) :
pass
patterns = [
url ( ' ^excluded-old-fashioned/$ ' , OldFashionedExcludedView . as_view ( ) ) ,
]
inspector = EndpointEnumerator ( patterns )
with pytest . warns ( PendingDeprecationWarning ) as record :
inspector . get_api_endpoints ( )
assert len ( record ) == 1
assert str ( record [ 0 ] . message ) == (
" The `OldFashionedExcludedView.exclude_from_schema` attribute is "
" pending deprecation. Set `schema = None` instead. "
)
2017-10-05 12:06:09 +03:00
@api_view ( [ " GET " ] )
def simple_fbv ( request ) :
pass
class BasicModelSerializer ( serializers . ModelSerializer ) :
class Meta :
model = BasicModel
fields = " __all__ "
class NamingCollisionView ( generics . RetrieveUpdateDestroyAPIView ) :
queryset = BasicModel . objects . all ( )
serializer_class = BasicModelSerializer
2017-10-16 12:32:48 +03:00
class BasicNamingCollisionView ( generics . RetrieveAPIView ) :
queryset = BasicModel . objects . all ( )
2017-10-05 12:06:09 +03:00
class NamingCollisionViewSet ( GenericViewSet ) :
"""
Example via : https : / / stackoverflow . com / questions / 43778668 / django - rest - framwork - occured - typeerror - link - object - does - not - support - item - ass /
"""
permision_class = ( )
2018-01-25 11:40:49 +03:00
@action ( detail = False )
2017-10-05 12:06:09 +03:00
def detail ( self , request ) :
return { }
2018-01-25 11:40:49 +03:00
@action ( detail = False , url_path = ' detail/export ' )
2017-10-05 12:06:09 +03:00
def detail_export ( self , request ) :
return { }
naming_collisions_router = SimpleRouter ( )
naming_collisions_router . register ( r ' collision ' , NamingCollisionViewSet , base_name = " collision " )
class TestURLNamingCollisions ( TestCase ) :
"""
Ref : https : / / github . com / encode / django - rest - framework / issues / 4704
"""
def test_manually_routing_nested_routes ( self ) :
patterns = [
url ( r ' ^test ' , simple_fbv ) ,
url ( r ' ^test/list/ ' , simple_fbv ) ,
]
generator = SchemaGenerator ( title = ' Naming Colisions ' , patterns = patterns )
2017-10-16 12:32:48 +03:00
schema = generator . get_schema ( )
2017-10-05 12:06:09 +03:00
2017-10-16 12:32:48 +03:00
expected = coreapi . Document (
url = ' ' ,
title = ' Naming Colisions ' ,
content = {
' test ' : {
' list ' : {
' list ' : coreapi . Link ( url = ' /test/list/ ' , action = ' get ' )
} ,
' list_0 ' : coreapi . Link ( url = ' /test ' , action = ' get ' )
}
}
)
assert expected == schema
def _verify_cbv_links ( self , loc , url , methods = None , suffixes = None ) :
if methods is None :
methods = ( ' read ' , ' update ' , ' partial_update ' , ' delete ' )
if suffixes is None :
suffixes = ( None for m in methods )
for method , suffix in zip ( methods , suffixes ) :
if suffix is not None :
key = ' {} _ {} ' . format ( method , suffix )
else :
key = method
assert loc [ key ] . url == url
2017-10-05 12:06:09 +03:00
def test_manually_routing_generic_view ( self ) :
patterns = [
url ( r ' ^test ' , NamingCollisionView . as_view ( ) ) ,
url ( r ' ^test/retrieve/ ' , NamingCollisionView . as_view ( ) ) ,
url ( r ' ^test/update/ ' , NamingCollisionView . as_view ( ) ) ,
# Fails with method names:
url ( r ' ^test/get/ ' , NamingCollisionView . as_view ( ) ) ,
url ( r ' ^test/put/ ' , NamingCollisionView . as_view ( ) ) ,
url ( r ' ^test/delete/ ' , NamingCollisionView . as_view ( ) ) ,
]
generator = SchemaGenerator ( title = ' Naming Colisions ' , patterns = patterns )
2017-10-16 12:32:48 +03:00
schema = generator . get_schema ( )
self . _verify_cbv_links ( schema [ ' test ' ] [ ' delete ' ] , ' /test/delete/ ' )
self . _verify_cbv_links ( schema [ ' test ' ] [ ' put ' ] , ' /test/put/ ' )
self . _verify_cbv_links ( schema [ ' test ' ] [ ' get ' ] , ' /test/get/ ' )
self . _verify_cbv_links ( schema [ ' test ' ] [ ' update ' ] , ' /test/update/ ' )
self . _verify_cbv_links ( schema [ ' test ' ] [ ' retrieve ' ] , ' /test/retrieve/ ' )
self . _verify_cbv_links ( schema [ ' test ' ] , ' /test ' , suffixes = ( None , ' 0 ' , None , ' 0 ' ) )
2017-10-05 12:06:09 +03:00
def test_from_router ( self ) :
patterns = [
url ( r ' from-router ' , include ( naming_collisions_router . urls ) ) ,
]
generator = SchemaGenerator ( title = ' Naming Colisions ' , patterns = patterns )
2017-10-16 12:32:48 +03:00
schema = generator . get_schema ( )
2018-01-25 11:40:49 +03:00
# not important here
desc_0 = schema [ ' detail ' ] [ ' detail_export ' ] . description
desc_1 = schema [ ' detail_0 ' ] . description
2017-10-16 12:32:48 +03:00
expected = coreapi . Document (
url = ' ' ,
title = ' Naming Colisions ' ,
content = {
' detail ' : {
' detail_export ' : coreapi . Link (
url = ' /from-routercollision/detail/export/ ' ,
action = ' get ' ,
2018-01-25 11:40:49 +03:00
description = desc_0 )
2017-10-16 12:32:48 +03:00
} ,
' detail_0 ' : coreapi . Link (
url = ' /from-routercollision/detail/ ' ,
action = ' get ' ,
2018-01-25 11:40:49 +03:00
description = desc_1
2017-10-16 12:32:48 +03:00
)
}
)
assert schema == expected
def test_url_under_same_key_not_replaced ( self ) :
patterns = [
url ( r ' example/(?P<pk> \ d+)/$ ' , BasicNamingCollisionView . as_view ( ) ) ,
url ( r ' example/(?P<slug> \ w+)/$ ' , BasicNamingCollisionView . as_view ( ) ) ,
]
generator = SchemaGenerator ( title = ' Naming Colisions ' , patterns = patterns )
schema = generator . get_schema ( )
assert schema [ ' example ' ] [ ' read ' ] . url == ' /example/ {id} / '
assert schema [ ' example ' ] [ ' read_0 ' ] . url == ' /example/ {slug} / '
def test_url_under_same_key_not_replaced_another ( self ) :
patterns = [
url ( r ' ^test/list/ ' , simple_fbv ) ,
url ( r ' ^test/(?P<pk> \ d+)/list/ ' , simple_fbv ) ,
]
generator = SchemaGenerator ( title = ' Naming Colisions ' , patterns = patterns )
schema = generator . get_schema ( )
2017-10-05 12:06:09 +03:00
2017-10-16 12:32:48 +03:00
assert schema [ ' test ' ] [ ' list ' ] [ ' list ' ] . url == ' /test/list/ '
assert schema [ ' test ' ] [ ' list ' ] [ ' list_0 ' ] . url == ' /test/ {id} /list/ '
2017-10-05 21:41:14 +03:00
def test_is_list_view_recognises_retrieve_view_subclasses ( ) :
class TestView ( generics . RetrieveAPIView ) :
pass
path = ' /looks/like/a/list/view/ '
method = ' get '
view = TestView ( )
is_list = is_list_view ( path , method , view )
assert not is_list , " RetrieveAPIView subclasses should not be classified as list views. "
2017-10-25 11:56:40 +03:00
def test_head_and_options_methods_are_excluded ( ) :
"""
Regression test for #5528
https : / / github . com / encode / django - rest - framework / issues / 5528
Viewset OPTIONS actions were not being correctly excluded
Initial cases here shown to be working as expected .
"""
@api_view ( [ ' options ' , ' get ' ] )
def fbv ( request ) :
pass
inspector = EndpointEnumerator ( )
path = ' /a/path/ '
callback = fbv
assert inspector . should_include_endpoint ( path , callback )
assert inspector . get_allowed_methods ( callback ) == [ " GET " ]
class AnAPIView ( APIView ) :
def get ( self , request , * args , * * kwargs ) :
pass
def options ( self , request , * args , * * kwargs ) :
pass
callback = AnAPIView . as_view ( )
assert inspector . should_include_endpoint ( path , callback )
assert inspector . get_allowed_methods ( callback ) == [ " GET " ]
class AViewSet ( ModelViewSet ) :
2018-01-25 11:40:49 +03:00
@action ( methods = [ ' options ' , ' get ' ] , detail = True )
2017-10-25 11:56:40 +03:00
def custom_action ( self , request , pk ) :
pass
callback = AViewSet . as_view ( {
" options " : " custom_action " ,
" get " : " custom_action "
} )
assert inspector . should_include_endpoint ( path , callback )
assert inspector . get_allowed_methods ( callback ) == [ " GET " ]
2017-11-22 08:11:59 +03:00
class TestAutoSchemaAllowsFilters ( object ) :
class MockAPIView ( APIView ) :
filter_backends = [ filters . OrderingFilter ]
def _test ( self , method ) :
view = self . MockAPIView ( )
fields = view . schema . get_filter_fields ( ' ' , method )
field_names = [ f . name for f in fields ]
return ' ordering ' in field_names
def test_get ( self ) :
assert self . _test ( ' get ' )
def test_GET ( self ) :
assert self . _test ( ' GET ' )
def test_put ( self ) :
assert self . _test ( ' put ' )
def test_PUT ( self ) :
assert self . _test ( ' PUT ' )
def test_patch ( self ) :
assert self . _test ( ' patch ' )
def test_PATCH ( self ) :
assert self . _test ( ' PATCH ' )
def test_delete ( self ) :
assert self . _test ( ' delete ' )
def test_DELETE ( self ) :
assert self . _test ( ' DELETE ' )
def test_post ( self ) :
assert not self . _test ( ' post ' )
def test_POST ( self ) :
assert not self . _test ( ' POST ' )
def test_foo ( self ) :
assert not self . _test ( ' foo ' )
def test_FOO ( self ) :
assert not self . _test ( ' FOO ' )