mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-11-30 06:45:57 +03:00
This is the first part of #6846. Previously, the response schema for list views was an object representing a single item. However, list views return a list of items, and hence it should be an array. Further work will need to be done to support how pagination classes modify list responses. There should be no change for views not determined to be list views.
439 lines
14 KiB
Python
439 lines
14 KiB
Python
import pytest
|
|
from django.conf.urls import url
|
|
from django.test import RequestFactory, TestCase, override_settings
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from rest_framework import filters, generics, pagination, routers, serializers
|
|
from rest_framework.compat import uritemplate
|
|
from rest_framework.request import Request
|
|
from rest_framework.schemas.openapi import AutoSchema, SchemaGenerator
|
|
|
|
from . import views
|
|
|
|
|
|
def create_request(path):
|
|
factory = RequestFactory()
|
|
request = Request(factory.get(path))
|
|
return request
|
|
|
|
|
|
def create_view(view_cls, method, request):
|
|
generator = SchemaGenerator()
|
|
view = generator.create_view(view_cls.as_view(), method, request)
|
|
return view
|
|
|
|
|
|
class TestBasics(TestCase):
|
|
def dummy_view(request):
|
|
pass
|
|
|
|
def test_filters(self):
|
|
classes = [filters.SearchFilter, filters.OrderingFilter]
|
|
for c in classes:
|
|
f = c()
|
|
assert f.get_schema_operation_parameters(self.dummy_view)
|
|
|
|
def test_pagination(self):
|
|
classes = [pagination.PageNumberPagination, pagination.LimitOffsetPagination, pagination.CursorPagination]
|
|
for c in classes:
|
|
f = c()
|
|
assert f.get_schema_operation_parameters(self.dummy_view)
|
|
|
|
|
|
class TestFieldMapping(TestCase):
|
|
def test_list_field_mapping(self):
|
|
inspector = AutoSchema()
|
|
cases = [
|
|
(serializers.ListField(), {'items': {}, 'type': 'array'}),
|
|
(serializers.ListField(child=serializers.BooleanField()), {'items': {'type': 'boolean'}, 'type': 'array'}),
|
|
(serializers.ListField(child=serializers.FloatField()), {'items': {'type': 'number'}, 'type': 'array'}),
|
|
(serializers.ListField(child=serializers.CharField()), {'items': {'type': 'string'}, 'type': 'array'}),
|
|
]
|
|
for field, mapping in cases:
|
|
with self.subTest(field=field):
|
|
assert inspector._map_field(field) == mapping
|
|
|
|
def test_lazy_string_field(self):
|
|
class Serializer(serializers.Serializer):
|
|
text = serializers.CharField(help_text=_('lazy string'))
|
|
|
|
inspector = AutoSchema()
|
|
|
|
data = inspector._map_serializer(Serializer())
|
|
assert isinstance(data['properties']['text']['description'], str), "description must be str"
|
|
|
|
|
|
@pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.')
|
|
class TestOperationIntrospection(TestCase):
|
|
|
|
def test_path_without_parameters(self):
|
|
path = '/example/'
|
|
method = 'GET'
|
|
|
|
view = create_view(
|
|
views.ExampleListView,
|
|
method,
|
|
create_request(path)
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
operation = inspector.get_operation(path, method)
|
|
assert operation == {
|
|
'operationId': 'ListExamples',
|
|
'parameters': [],
|
|
'responses': {
|
|
'200': {
|
|
'content': {
|
|
'application/json': {
|
|
'schema': {
|
|
'type': 'array',
|
|
'items': {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
def test_path_with_id_parameter(self):
|
|
path = '/example/{id}/'
|
|
method = 'GET'
|
|
|
|
view = create_view(
|
|
views.ExampleDetailView,
|
|
method,
|
|
create_request(path)
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
parameters = inspector._get_path_parameters(path, method)
|
|
assert parameters == [{
|
|
'description': '',
|
|
'in': 'path',
|
|
'name': 'id',
|
|
'required': True,
|
|
'schema': {
|
|
'type': 'string',
|
|
},
|
|
}]
|
|
|
|
def test_request_body(self):
|
|
path = '/'
|
|
method = 'POST'
|
|
|
|
class Serializer(serializers.Serializer):
|
|
text = serializers.CharField()
|
|
read_only = serializers.CharField(read_only=True)
|
|
|
|
class View(generics.GenericAPIView):
|
|
serializer_class = Serializer
|
|
|
|
view = create_view(
|
|
View,
|
|
method,
|
|
create_request(path)
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
request_body = inspector._get_request_body(path, method)
|
|
assert request_body['content']['application/json']['schema']['required'] == ['text']
|
|
assert list(request_body['content']['application/json']['schema']['properties'].keys()) == ['text']
|
|
|
|
def test_response_body_generation(self):
|
|
path = '/'
|
|
method = 'POST'
|
|
|
|
class Serializer(serializers.Serializer):
|
|
text = serializers.CharField()
|
|
write_only = serializers.CharField(write_only=True)
|
|
|
|
class View(generics.GenericAPIView):
|
|
serializer_class = Serializer
|
|
|
|
view = create_view(
|
|
View,
|
|
method,
|
|
create_request(path)
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
responses = inspector._get_responses(path, method)
|
|
assert responses['200']['content']['application/json']['schema']['required'] == ['text']
|
|
assert list(responses['200']['content']['application/json']['schema']['properties'].keys()) == ['text']
|
|
|
|
def test_response_body_nested_serializer(self):
|
|
path = '/'
|
|
method = 'POST'
|
|
|
|
class NestedSerializer(serializers.Serializer):
|
|
number = serializers.IntegerField()
|
|
|
|
class Serializer(serializers.Serializer):
|
|
text = serializers.CharField()
|
|
nested = NestedSerializer()
|
|
|
|
class View(generics.GenericAPIView):
|
|
serializer_class = Serializer
|
|
|
|
view = create_view(
|
|
View,
|
|
method,
|
|
create_request(path),
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
responses = inspector._get_responses(path, method)
|
|
schema = responses['200']['content']['application/json']['schema']
|
|
assert sorted(schema['required']) == ['nested', 'text']
|
|
assert sorted(list(schema['properties'].keys())) == ['nested', 'text']
|
|
assert schema['properties']['nested']['type'] == 'object'
|
|
assert list(schema['properties']['nested']['properties'].keys()) == ['number']
|
|
assert schema['properties']['nested']['required'] == ['number']
|
|
|
|
def test_list_response_body_generation(self):
|
|
"""Test that an array schema is returned for list views."""
|
|
path = '/'
|
|
method = 'GET'
|
|
|
|
class ItemSerializer(serializers.Serializer):
|
|
text = serializers.CharField()
|
|
|
|
class View(generics.GenericAPIView):
|
|
serializer_class = ItemSerializer
|
|
|
|
view = create_view(
|
|
View,
|
|
method,
|
|
create_request(path),
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
responses = inspector._get_responses(path, method)
|
|
assert responses == {
|
|
'200': {
|
|
'content': {
|
|
'application/json': {
|
|
'schema': {
|
|
'type': 'array',
|
|
'items': {
|
|
'properties': {
|
|
'text': {
|
|
'type': 'string',
|
|
},
|
|
},
|
|
'required': ['text'],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
def test_retrieve_response_body_generation(self):
|
|
"""Test that a list of properties is returned for retrieve item views."""
|
|
path = '/{id}/'
|
|
method = 'GET'
|
|
|
|
class ItemSerializer(serializers.Serializer):
|
|
text = serializers.CharField()
|
|
|
|
class View(generics.GenericAPIView):
|
|
serializer_class = ItemSerializer
|
|
|
|
view = create_view(
|
|
View,
|
|
method,
|
|
create_request(path),
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
responses = inspector._get_responses(path, method)
|
|
assert responses == {
|
|
'200': {
|
|
'content': {
|
|
'application/json': {
|
|
'schema': {
|
|
'properties': {
|
|
'text': {
|
|
'type': 'string',
|
|
},
|
|
},
|
|
'required': ['text'],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
def test_operation_id_generation(self):
|
|
path = '/'
|
|
method = 'GET'
|
|
|
|
view = create_view(
|
|
views.ExampleGenericAPIView,
|
|
method,
|
|
create_request(path),
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
operationId = inspector._get_operation_id(path, method)
|
|
assert operationId == 'ListExamples'
|
|
|
|
def test_repeat_operation_ids(self):
|
|
router = routers.SimpleRouter()
|
|
router.register('account', views.ExampleGenericViewSet, basename="account")
|
|
urlpatterns = router.urls
|
|
|
|
generator = SchemaGenerator(patterns=urlpatterns)
|
|
|
|
request = create_request('/')
|
|
schema = generator.get_schema(request=request)
|
|
schema_str = str(schema)
|
|
print(schema_str)
|
|
assert schema_str.count("operationId") == 2
|
|
assert schema_str.count("newExample") == 1
|
|
assert schema_str.count("oldExample") == 1
|
|
|
|
def test_serializer_datefield(self):
|
|
path = '/'
|
|
method = 'GET'
|
|
view = create_view(
|
|
views.ExampleGenericAPIView,
|
|
method,
|
|
create_request(path),
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
responses = inspector._get_responses(path, method)
|
|
response_schema = responses['200']['content']['application/json']['schema']
|
|
properties = response_schema['items']['properties']
|
|
assert properties['date']['type'] == properties['datetime']['type'] == 'string'
|
|
assert properties['date']['format'] == 'date'
|
|
assert properties['datetime']['format'] == 'date-time'
|
|
|
|
def test_serializer_validators(self):
|
|
path = '/'
|
|
method = 'GET'
|
|
view = create_view(
|
|
views.ExampleValidatedAPIView,
|
|
method,
|
|
create_request(path),
|
|
)
|
|
inspector = AutoSchema()
|
|
inspector.view = view
|
|
|
|
responses = inspector._get_responses(path, method)
|
|
response_schema = responses['200']['content']['application/json']['schema']
|
|
properties = response_schema['items']['properties']
|
|
|
|
assert properties['integer']['type'] == 'integer'
|
|
assert properties['integer']['maximum'] == 99
|
|
assert properties['integer']['minimum'] == -11
|
|
|
|
assert properties['string']['minLength'] == 2
|
|
assert properties['string']['maxLength'] == 10
|
|
|
|
assert properties['regex']['pattern'] == r'[ABC]12{3}'
|
|
assert properties['regex']['description'] == 'must have an A, B, or C followed by 1222'
|
|
|
|
assert properties['decimal1']['type'] == 'number'
|
|
assert properties['decimal1']['multipleOf'] == .01
|
|
assert properties['decimal1']['maximum'] == 10000
|
|
assert properties['decimal1']['minimum'] == -10000
|
|
|
|
assert properties['decimal2']['type'] == 'number'
|
|
assert properties['decimal2']['multipleOf'] == .0001
|
|
|
|
assert properties['email']['type'] == 'string'
|
|
assert properties['email']['format'] == 'email'
|
|
assert properties['email']['default'] == 'foo@bar.com'
|
|
|
|
assert properties['url']['type'] == 'string'
|
|
assert properties['url']['nullable'] is True
|
|
assert properties['url']['default'] == 'http://www.example.com'
|
|
|
|
assert properties['uuid']['type'] == 'string'
|
|
assert properties['uuid']['format'] == 'uuid'
|
|
|
|
assert properties['ip4']['type'] == 'string'
|
|
assert properties['ip4']['format'] == 'ipv4'
|
|
|
|
assert properties['ip6']['type'] == 'string'
|
|
assert properties['ip6']['format'] == 'ipv6'
|
|
|
|
assert properties['ip']['type'] == 'string'
|
|
assert 'format' not in properties['ip']
|
|
|
|
|
|
@pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.')
|
|
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema'})
|
|
class TestGenerator(TestCase):
|
|
|
|
def test_override_settings(self):
|
|
assert isinstance(views.ExampleListView.schema, AutoSchema)
|
|
|
|
def test_paths_construction(self):
|
|
"""Construction of the `paths` key."""
|
|
patterns = [
|
|
url(r'^example/?$', views.ExampleListView.as_view()),
|
|
]
|
|
generator = SchemaGenerator(patterns=patterns)
|
|
generator._initialise_endpoints()
|
|
|
|
paths = generator.get_paths()
|
|
|
|
assert '/example/' in paths
|
|
example_operations = paths['/example/']
|
|
assert len(example_operations) == 2
|
|
assert 'get' in example_operations
|
|
assert 'post' in example_operations
|
|
|
|
def test_prefixed_paths_construction(self):
|
|
"""Construction of the `paths` key maintains a common prefix."""
|
|
patterns = [
|
|
url(r'^v1/example/?$', views.ExampleListView.as_view()),
|
|
url(r'^v1/example/{pk}/?$', views.ExampleDetailView.as_view()),
|
|
]
|
|
generator = SchemaGenerator(patterns=patterns)
|
|
generator._initialise_endpoints()
|
|
|
|
paths = generator.get_paths()
|
|
|
|
assert '/v1/example/' in paths
|
|
assert '/v1/example/{id}/' in paths
|
|
|
|
def test_mount_url_prefixed_to_paths(self):
|
|
patterns = [
|
|
url(r'^example/?$', views.ExampleListView.as_view()),
|
|
url(r'^example/{pk}/?$', views.ExampleDetailView.as_view()),
|
|
]
|
|
generator = SchemaGenerator(patterns=patterns, url='/api')
|
|
generator._initialise_endpoints()
|
|
|
|
paths = generator.get_paths()
|
|
|
|
assert '/api/example/' in paths
|
|
assert '/api/example/{id}/' in paths
|
|
|
|
def test_schema_construction(self):
|
|
"""Construction of the top level dictionary."""
|
|
patterns = [
|
|
url(r'^example/?$', views.ExampleListView.as_view()),
|
|
]
|
|
generator = SchemaGenerator(patterns=patterns)
|
|
|
|
request = create_request('/')
|
|
schema = generator.get_schema(request=request)
|
|
|
|
assert 'openapi' in schema
|
|
assert 'paths' in schema
|