import uuid import warnings import pytest from django.db import models from django.test import RequestFactory, TestCase, override_settings from django.urls import path from django.utils.safestring import SafeString from django.utils.translation import gettext_lazy as _ from rest_framework import filters, generics, pagination, routers, serializers from rest_framework.authtoken.views import obtain_auth_token from rest_framework.compat import uritemplate from rest_framework.parsers import JSONParser, MultiPartParser from rest_framework.renderers import ( BaseRenderer, BrowsableAPIRenderer, JSONOpenAPIRenderer, JSONRenderer, OpenAPIRenderer ) from rest_framework.request import Request from rest_framework.schemas.openapi import AutoSchema, SchemaGenerator from ..models import BasicModel 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): uuid1 = uuid.uuid4() uuid2 = uuid.uuid4() 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'}), (serializers.ListField(child=serializers.IntegerField(max_value=4294967295)), {'items': {'type': 'integer', 'maximum': 4294967295, 'format': 'int64'}, 'type': 'array'}), (serializers.ListField(child=serializers.ChoiceField(choices=[('a', 'Choice A'), ('b', 'Choice B')])), {'items': {'enum': ['a', 'b'], 'type': 'string'}, 'type': 'array'}), (serializers.ListField(child=serializers.ChoiceField(choices=[(1, 'One'), (2, 'Two')])), {'items': {'enum': [1, 2], 'type': 'integer'}, 'type': 'array'}), (serializers.ListField(child=serializers.ChoiceField(choices=[(1.1, 'First'), (2.2, 'Second')])), {'items': {'enum': [1.1, 2.2], 'type': 'number'}, 'type': 'array'}), (serializers.ListField(child=serializers.ChoiceField(choices=[(True, 'true'), (False, 'false')])), {'items': {'enum': [True, False], 'type': 'boolean'}, 'type': 'array'}), (serializers.ListField(child=serializers.ChoiceField(choices=[(uuid1, 'uuid1'), (uuid2, 'uuid2')])), {'items': {'enum': [uuid1, uuid2]}, 'type': 'array'}), (serializers.ListField(child=serializers.ChoiceField(choices=[(1, 'One'), ('a', 'Choice A')])), {'items': {'enum': [1, 'a']}, 'type': 'array'}), (serializers.ListField(child=serializers.ChoiceField(choices=[ (1, 'One'), ('a', 'Choice A'), (1.1, 'First'), (1.1, 'First'), (1, 'One'), ('a', 'Choice A'), (1, 'One') ])), {'items': {'enum': [1, 'a', 1.1]}, 'type': 'array'}), (serializers.ListField(child=serializers.ChoiceField(choices=[ (1, 'One'), (2, 'Two'), (3, 'Three'), (2, 'Two'), (3, 'Three'), (1, 'One'), ])), {'items': {'enum': [1, 2, 3], 'type': 'integer'}, 'type': 'array'}), (serializers.IntegerField(min_value=2147483648), {'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}), ] for field, mapping in cases: with self.subTest(field=field): assert inspector.map_field(field) == mapping def test_lazy_string_field(self): class ItemSerializer(serializers.Serializer): text = serializers.CharField(help_text=_('lazy string')) inspector = AutoSchema() data = inspector.map_serializer(ItemSerializer()) assert isinstance(data['properties']['text']['description'], str), "description must be str" def test_boolean_default_field(self): class Serializer(serializers.Serializer): default_true = serializers.BooleanField(default=True) default_false = serializers.BooleanField(default=False) without_default = serializers.BooleanField() inspector = AutoSchema() data = inspector.map_serializer(Serializer()) assert data['properties']['default_true']['default'] is True, "default must be true" assert data['properties']['default_false']['default'] is False, "default must be false" assert 'default' not in data['properties']['without_default'], "default must not be defined" def test_custom_field_name(self): class CustomSchema(AutoSchema): def get_field_name(self, field): return 'custom_' + field.field_name class Serializer(serializers.Serializer): text_field = serializers.CharField() inspector = CustomSchema() data = inspector.map_serializer(Serializer()) assert 'custom_text_field' in data['properties'] assert 'text_field' not in data['properties'] def test_nullable_fields(self): class Model(models.Model): rw_field = models.CharField(null=True) ro_field = models.CharField(null=True) class Serializer(serializers.ModelSerializer): class Meta: model = Model fields = ["rw_field", "ro_field"] read_only_fields = ["ro_field"] inspector = AutoSchema() data = inspector.map_serializer(Serializer()) assert data['properties']['rw_field']['nullable'], "rw_field nullable must be true" assert data['properties']['ro_field']['nullable'], "ro_field nullable must be true" assert data['properties']['ro_field']['readOnly'], "ro_field read_only must be true" def test_primary_key_related_field(self): class PrimaryKeyRelatedFieldSerializer(serializers.Serializer): basic = serializers.PrimaryKeyRelatedField(queryset=BasicModel.objects.all()) uuid = serializers.PrimaryKeyRelatedField(queryset=BasicModel.objects.all(), pk_field=serializers.UUIDField()) char = serializers.PrimaryKeyRelatedField(queryset=BasicModel.objects.all(), pk_field=serializers.CharField()) serializer = PrimaryKeyRelatedFieldSerializer() inspector = AutoSchema() data = inspector.map_serializer(serializer=serializer) assert data['properties']['basic']['type'] == "integer" assert data['properties']['uuid']['format'] == "uuid" assert data['properties']['char']['type'] == "string" @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.DocStringExampleListView, method, create_request(path) ) inspector = AutoSchema() inspector.view = view operation = inspector.get_operation(path, method) assert operation == { 'operationId': 'listDocStringExamples', 'description': 'A description of my GET operation.', 'parameters': [], 'tags': ['example'], 'responses': { '200': { 'description': '', 'content': { 'application/json': { 'schema': { 'type': 'array', 'items': {}, }, }, }, }, }, } def test_path_with_id_parameter(self): path = '/example/{id}/' method = 'GET' view = create_view( views.DocStringExampleDetailView, method, create_request(path) ) inspector = AutoSchema() inspector.view = view operation = inspector.get_operation(path, method) assert operation == { 'operationId': 'retrieveDocStringExampleDetail', 'description': 'A description of my GET operation.', 'parameters': [{ 'description': '', 'in': 'path', 'name': 'id', 'required': True, 'schema': { 'type': 'string', }, }], 'tags': ['example'], 'responses': { '200': { 'description': '', 'content': { 'application/json': { 'schema': { }, }, }, }, }, } def test_request_body(self): path = '/' method = 'POST' class ItemSerializer(serializers.Serializer): text = serializers.CharField() read_only = serializers.CharField(read_only=True) class View(generics.GenericAPIView): serializer_class = ItemSerializer view = create_view( View, method, create_request(path) ) inspector = AutoSchema() inspector.view = view request_body = inspector.get_request_body(path, method) print(request_body) assert request_body['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item' components = inspector.get_components(path, method) assert components['Item']['required'] == ['text'] assert sorted(list(components['Item']['properties'].keys())) == ['read_only', 'text'] def test_invalid_serializer_class_name(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 serializer = inspector.get_serializer(path, method) with pytest.raises(Exception) as exc: inspector.get_component_name(serializer) assert "is an invalid class name for schema generation" in str(exc.value) def test_empty_required(self): path = '/' method = 'POST' class ItemSerializer(serializers.Serializer): read_only = serializers.CharField(read_only=True) write_only = serializers.CharField(write_only=True, required=False) class View(generics.GenericAPIView): serializer_class = ItemSerializer view = create_view( View, method, create_request(path) ) inspector = AutoSchema() inspector.view = view components = inspector.get_components(path, method) component = components['Item'] # there should be no empty 'required' property, see #6834 assert 'required' not in component for response in inspector.get_responses(path, method).values(): assert 'required' not in component def test_empty_required_with_patch_method(self): path = '/' method = 'PATCH' class ItemSerializer(serializers.Serializer): read_only = serializers.CharField(read_only=True) write_only = serializers.CharField(write_only=True, required=False) class View(generics.GenericAPIView): serializer_class = ItemSerializer view = create_view( View, method, create_request(path) ) inspector = AutoSchema() inspector.view = view components = inspector.get_components(path, method) component = components['Item'] # there should be no empty 'required' property, see #6834 assert 'required' not in component for response in inspector.get_responses(path, method).values(): assert 'required' not in component def test_response_body_generation(self): path = '/' method = 'POST' class ItemSerializer(serializers.Serializer): text = serializers.CharField() write_only = serializers.CharField(write_only=True) 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['201']['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item' components = inspector.get_components(path, method) assert sorted(components['Item']['required']) == ['text', 'write_only'] assert sorted(list(components['Item']['properties'].keys())) == ['text', 'write_only'] assert 'description' in responses['201'] def test_response_body_nested_serializer(self): path = '/' method = 'POST' class NestedSerializer(serializers.Serializer): number = serializers.IntegerField() class ItemSerializer(serializers.Serializer): text = serializers.CharField() nested = NestedSerializer() 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['201']['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item' components = inspector.get_components(path, method) assert components['Item'] schema = components['Item'] 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_response_body_partial_serializer(self): path = '/' method = 'GET' class ItemSerializer(serializers.Serializer): text = serializers.CharField() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.partial = True 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': { 'description': '', 'content': { 'application/json': { 'schema': { 'type': 'array', 'items': { '$ref': '#/components/schemas/Item' }, }, }, }, }, } components = inspector.get_components(path, method) assert components == { 'Item': { 'type': 'object', 'properties': { 'text': { 'type': 'string', }, }, } } 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': { 'description': '', 'content': { 'application/json': { 'schema': { 'type': 'array', 'items': { '$ref': '#/components/schemas/Item' }, }, }, }, }, } components = inspector.get_components(path, method) assert components == { 'Item': { 'type': 'object', 'properties': { 'text': { 'type': 'string', }, }, 'required': ['text'], } } def test_paginated_list_response_body_generation(self): """Test that pagination properties are added for a paginated list view.""" path = '/' method = 'GET' class Pagination(pagination.BasePagination): def get_paginated_response_schema(self, schema): return { 'type': 'object', 'item': schema, } class ItemSerializer(serializers.Serializer): text = serializers.CharField() class View(generics.GenericAPIView): serializer_class = ItemSerializer pagination_class = Pagination view = create_view( View, method, create_request(path), ) inspector = AutoSchema() inspector.view = view responses = inspector.get_responses(path, method) assert responses == { '200': { 'description': '', 'content': { 'application/json': { 'schema': { 'type': 'object', 'item': { 'type': 'array', 'items': { '$ref': '#/components/schemas/Item' }, }, }, }, }, }, } components = inspector.get_components(path, method) assert components == { 'Item': { 'type': 'object', 'properties': { 'text': { 'type': 'string', }, }, 'required': ['text'], } } def test_delete_response_body_generation(self): """Test that a view's delete method generates a proper response body schema.""" path = '/{id}/' method = 'DELETE' class View(generics.DestroyAPIView): serializer_class = views.ExampleSerializer view = create_view( View, method, create_request(path), ) inspector = AutoSchema() inspector.view = view responses = inspector.get_responses(path, method) assert responses == { '204': { 'description': '', }, } def test_parser_mapping(self): """Test that view's parsers are mapped to OA media types""" path = '/{id}/' method = 'POST' class View(generics.CreateAPIView): serializer_class = views.ExampleSerializer parser_classes = [JSONParser, MultiPartParser] view = create_view( View, method, create_request(path), ) inspector = AutoSchema() inspector.view = view request_body = inspector.get_request_body(path, method) assert len(request_body['content'].keys()) == 2 assert 'multipart/form-data' in request_body['content'] assert 'application/json' in request_body['content'] def test_renderer_mapping(self): """Test that view's renderers are mapped to OA media types""" path = '/{id}/' method = 'GET' class CustomBrowsableAPIRenderer(BrowsableAPIRenderer): media_type = 'image/jpeg' # that's a wild API renderer class TextRenderer(BaseRenderer): media_type = 'text/plain' format = 'text' class View(generics.CreateAPIView): serializer_class = views.ExampleSerializer renderer_classes = [JSONRenderer, TextRenderer, BrowsableAPIRenderer, CustomBrowsableAPIRenderer] view = create_view( View, method, create_request(path), ) inspector = AutoSchema() inspector.view = view responses = inspector.get_responses(path, method) # TODO this should be changed once the multiple response # schema support is there success_response = responses['200'] # Check that the API renderers aren't included, but custom renderers are assert set(success_response['content']) == {'application/json', 'text/plain'} def test_openapi_yaml_rendering_without_aliases(self): renderer = OpenAPIRenderer() reused_object = {'test': 'test'} data = { 'o1': reused_object, 'o2': reused_object, } assert ( renderer.render(data) == b'o1:\n test: test\no2:\n test: test\n' or renderer.render(data) == b'o2:\n test: test\no1:\n test: test\n' # py <= 3.5 ) def test_openapi_yaml_safestring_render(self): renderer = OpenAPIRenderer() data = {'o1': SafeString('test')} assert renderer.render(data) == b'o1: test\n' def test_serializer_filefield(self): path = '/{id}/' method = 'POST' class ItemSerializer(serializers.Serializer): attachment = serializers.FileField() class View(generics.CreateAPIView): serializer_class = ItemSerializer view = create_view( View, method, create_request(path), ) inspector = AutoSchema() inspector.view = view components = inspector.get_components(path, method) component = components['Item'] properties = component['properties'] assert properties['attachment']['format'] == 'binary' def test_retrieve_response_body_generation(self): """ Test that a list of properties is returned for retrieve item views. Pagination properties should not be added as the view represents a single item. """ path = '/{id}/' method = 'GET' class Pagination(pagination.BasePagination): def get_paginated_response_schema(self, schema): return { 'type': 'object', 'item': schema, } class ItemSerializer(serializers.Serializer): text = serializers.CharField() class View(generics.GenericAPIView): serializer_class = ItemSerializer pagination_class = Pagination view = create_view( View, method, create_request(path), ) inspector = AutoSchema() inspector.view = view responses = inspector.get_responses(path, method) assert responses == { '200': { 'description': '', 'content': { 'application/json': { 'schema': { '$ref': '#/components/schemas/Item' }, }, }, }, } components = inspector.get_components(path, method) assert components == { 'Item': { 'type': 'object', '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_operation_id_custom_operation_id_base(self): path = '/' method = 'GET' view = create_view( views.ExampleGenericAPIView, method, create_request(path), ) inspector = AutoSchema(operation_id_base="Ulysse") inspector.view = view operationId = inspector.get_operation_id(path, method) assert operationId == 'listUlysses' def test_operation_id_custom_name(self): path = '/' method = 'GET' view = create_view( views.ExampleGenericAPIView, method, create_request(path), ) inspector = AutoSchema(operation_id_base='Ulysse') inspector.view = view operationId = inspector.get_operation_id(path, method) assert operationId == 'listUlysses' def test_operation_id_plural(self): path = '/' method = 'GET' view = create_view( views.ExampleGenericAPIView, method, create_request(path), ) inspector = AutoSchema(operation_id_base='City') inspector.view = view operationId = inspector.get_operation_id(path, method) assert operationId == 'listCities' def test_operation_id_override_get(self): class CustomSchema(AutoSchema): def get_operation_id(self, path, method): return 'myCustomOperationId' path = '/' method = 'GET' view = create_view( views.ExampleGenericAPIView, method, create_request(path), ) inspector = CustomSchema() inspector.view = view operationId = inspector.get_operation_id(path, method) assert operationId == 'myCustomOperationId' def test_operation_id_override_base(self): class CustomSchema(AutoSchema): def get_operation_id_base(self, path, method, action): return 'Item' path = '/' method = 'GET' view = create_view( views.ExampleGenericAPIView, method, create_request(path), ) inspector = CustomSchema() inspector.view = view operationId = inspector.get_operation_id(path, method) assert operationId == 'listItem' def test_different_request_response_objects(self): class RequestSerializer(serializers.Serializer): text = serializers.CharField() class ResponseSerializer(serializers.Serializer): text = serializers.BooleanField() class CustomSchema(AutoSchema): def get_request_serializer(self, path, method): return RequestSerializer() def get_response_serializer(self, path, method): return ResponseSerializer() path = '/' method = 'POST' view = create_view( views.ExampleGenericAPIView, method, create_request(path), ) inspector = CustomSchema() inspector.view = view components = inspector.get_components(path, method) assert components == { 'Request': { 'properties': { 'text': { 'type': 'string' } }, 'required': ['text'], 'type': 'object' }, 'Response': { 'properties': { 'text': { 'type': 'boolean' } }, 'required': ['text'], 'type': 'object' } } operation = inspector.get_operation(path, method) assert operation == { 'operationId': 'createExample', 'description': '', 'parameters': [], 'requestBody': { 'content': { 'application/json': { 'schema': { '$ref': '#/components/schemas/Request' } }, 'application/x-www-form-urlencoded': { 'schema': { '$ref': '#/components/schemas/Request' } }, 'multipart/form-data': { 'schema': { '$ref': '#/components/schemas/Request' } } } }, 'responses': { '201': { 'content': { 'application/json': { 'schema': { '$ref': '#/components/schemas/Response' } } }, 'description': '' } }, 'tags': [''] } 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_duplicate_operation_id(self): patterns = [ path('duplicate1/', views.ExampleOperationIdDuplicate1.as_view()), path('duplicate2/', views.ExampleOperationIdDuplicate2.as_view()), ] generator = SchemaGenerator(patterns=patterns) request = create_request('/') with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') generator.get_schema(request=request) assert len(w) == 1 assert issubclass(w[-1].category, UserWarning) print(str(w[-1].message)) assert 'You have a duplicated operationId' in str(w[-1].message) def test_operation_id_viewset(self): router = routers.SimpleRouter() router.register('account', views.ExampleViewSet, basename="account") urlpatterns = router.urls generator = SchemaGenerator(patterns=urlpatterns) request = create_request('/') schema = generator.get_schema(request=request) print(schema) assert schema['paths']['/account/']['get']['operationId'] == 'listExampleViewSets' assert schema['paths']['/account/']['post']['operationId'] == 'createExampleViewSet' assert schema['paths']['/account/{id}/']['get']['operationId'] == 'retrieveExampleViewSet' assert schema['paths']['/account/{id}/']['put']['operationId'] == 'updateExampleViewSet' assert schema['paths']['/account/{id}/']['patch']['operationId'] == 'partialUpdateExampleViewSet' assert schema['paths']['/account/{id}/']['delete']['operationId'] == 'destroyExampleViewSet' def test_serializer_datefield(self): path = '/' method = 'GET' view = create_view( views.ExampleGenericAPIView, method, create_request(path), ) inspector = AutoSchema() inspector.view = view components = inspector.get_components(path, method) component = components['Example'] properties = component['properties'] assert properties['date']['type'] == properties['datetime']['type'] == 'string' assert properties['date']['format'] == 'date' assert properties['datetime']['format'] == 'date-time' def test_serializer_hstorefield(self): path = '/' method = 'GET' view = create_view( views.ExampleGenericAPIView, method, create_request(path), ) inspector = AutoSchema() inspector.view = view components = inspector.get_components(path, method) component = components['Example'] properties = component['properties'] assert properties['hstore']['type'] == 'object' def test_serializer_callable_default(self): path = '/' method = 'GET' view = create_view( views.ExampleGenericAPIView, method, create_request(path), ) inspector = AutoSchema() inspector.view = view components = inspector.get_components(path, method) component = components['Example'] properties = component['properties'] assert 'default' not in properties['uuid_field'] def test_serializer_validators(self): path = '/' method = 'GET' view = create_view( views.ExampleValidatedAPIView, method, create_request(path), ) inspector = AutoSchema() inspector.view = view components = inspector.get_components(path, method) component = components['ExampleValidated'] properties = component['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['lst']['minItems'] == 2 assert properties['lst']['maxItems'] == 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['decimal3'] == { 'type': 'string', 'format': 'decimal', 'maximum': 1000000, 'minimum': -1000000, 'multipleOf': 0.01 } assert properties['decimal4'] == { 'type': 'string', 'format': 'decimal', 'maximum': 1000000, 'minimum': -1000000, 'multipleOf': 0.01 } assert properties['decimal5'] == { 'type': 'string', 'format': 'decimal', 'maximum': 10000, 'minimum': -10000, 'multipleOf': 0.01 } 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 '\\Z' not in properties['url']['pattern'] 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'] def test_overridden_tags(self): class ExampleStringTagsViewSet(views.ExampleGenericAPIView): schema = AutoSchema(tags=['example1', 'example2']) url_patterns = [ path('test/', ExampleStringTagsViewSet.as_view()), ] generator = SchemaGenerator(patterns=url_patterns) schema = generator.get_schema(request=create_request('/')) assert schema['paths']['/test/']['get']['tags'] == ['example1', 'example2'] def test_overridden_get_tags_method(self): class MySchema(AutoSchema): def get_tags(self, path, method): if path.endswith('/new/'): tags = ['tag1', 'tag2'] elif path.endswith('/old/'): tags = ['tag2', 'tag3'] else: tags = ['tag4', 'tag5'] return tags class ExampleStringTagsViewSet(views.ExampleGenericViewSet): schema = MySchema() router = routers.SimpleRouter() router.register('example', ExampleStringTagsViewSet, basename="example") generator = SchemaGenerator(patterns=router.urls) schema = generator.get_schema(request=create_request('/')) assert schema['paths']['/example/new/']['get']['tags'] == ['tag1', 'tag2'] assert schema['paths']['/example/old/']['get']['tags'] == ['tag2', 'tag3'] def test_auto_generated_apiview_tags(self): class RestaurantAPIView(views.ExampleGenericAPIView): schema = AutoSchema(operation_id_base="restaurant") pass class BranchAPIView(views.ExampleGenericAPIView): pass url_patterns = [ path('any-dash_underscore/', RestaurantAPIView.as_view()), path('restaurants/branches/', BranchAPIView.as_view()) ] generator = SchemaGenerator(patterns=url_patterns) schema = generator.get_schema(request=create_request('/')) assert schema['paths']['/any-dash_underscore/']['get']['tags'] == ['any-dash-underscore'] assert schema['paths']['/restaurants/branches/']['get']['tags'] == ['restaurants'] @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 = [ path('example/', views.ExampleListView.as_view()), ] generator = SchemaGenerator(patterns=patterns) generator._initialise_endpoints() paths = generator.get_schema()["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 = [ path('v1/example/', views.ExampleListView.as_view()), path('v1/example/{pk}/', views.ExampleDetailView.as_view()), ] generator = SchemaGenerator(patterns=patterns) generator._initialise_endpoints() paths = generator.get_schema()["paths"] assert '/v1/example/' in paths assert '/v1/example/{id}/' in paths def test_mount_url_prefixed_to_paths(self): patterns = [ path('example/', views.ExampleListView.as_view()), path('example/{pk}/', views.ExampleDetailView.as_view()), ] generator = SchemaGenerator(patterns=patterns, url='/api') generator._initialise_endpoints() paths = generator.get_schema()["paths"] assert '/api/example/' in paths assert '/api/example/{id}/' in paths def test_schema_construction(self): """Construction of the top level dictionary.""" patterns = [ path('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 def test_schema_rendering_to_json(self): patterns = [ path('example/', views.ExampleGenericAPIView.as_view()), ] generator = SchemaGenerator(patterns=patterns) request = create_request('/') schema = generator.get_schema(request=request) ret = JSONOpenAPIRenderer().render(schema) assert b'"openapi": "' in ret assert b'"default": "0.0"' in ret def test_schema_rendering_to_yaml(self): patterns = [ path('example/', views.ExampleGenericAPIView.as_view()), ] generator = SchemaGenerator(patterns=patterns) request = create_request('/') schema = generator.get_schema(request=request) ret = OpenAPIRenderer().render(schema) assert b"openapi: " in ret assert b"default: '0.0'" in ret def test_schema_rendering_timedelta_to_yaml_with_validator(self): patterns = [ path('example/', views.ExampleValidatedAPIView.as_view()), ] generator = SchemaGenerator(patterns=patterns) request = create_request('/') schema = generator.get_schema(request=request) ret = OpenAPIRenderer().render(schema) assert b"openapi: " in ret assert b"duration:\n type: string\n minimum: \'10.0\'\n" in ret def test_schema_with_no_paths(self): patterns = [] generator = SchemaGenerator(patterns=patterns) request = create_request('/') schema = generator.get_schema(request=request) assert schema['paths'] == {} def test_schema_information(self): """Construction of the top level dictionary.""" patterns = [ path('example/', views.ExampleListView.as_view()), ] generator = SchemaGenerator(patterns=patterns, title='My title', version='1.2.3', description='My description') request = create_request('/') schema = generator.get_schema(request=request) assert schema['info']['title'] == 'My title' assert schema['info']['version'] == '1.2.3' assert schema['info']['description'] == 'My description' def test_schema_information_empty(self): """Construction of the top level dictionary.""" patterns = [ path('example/', views.ExampleListView.as_view()), ] generator = SchemaGenerator(patterns=patterns) request = create_request('/') schema = generator.get_schema(request=request) assert schema['info']['title'] == '' assert schema['info']['version'] == '' def test_serializer_model(self): """Construction of the top level dictionary.""" patterns = [ path('example/', views.ExampleGenericAPIViewModel.as_view()), ] generator = SchemaGenerator(patterns=patterns) request = create_request('/') schema = generator.get_schema(request=request) print(schema) assert 'components' in schema assert 'schemas' in schema['components'] assert 'ExampleModel' in schema['components']['schemas'] def test_authtoken_serializer(self): patterns = [ path('api-token-auth/', obtain_auth_token) ] generator = SchemaGenerator(patterns=patterns) request = create_request('/') schema = generator.get_schema(request=request) print(schema) route = schema['paths']['/api-token-auth/']['post'] body_schema = route['requestBody']['content']['application/json']['schema'] assert body_schema == { '$ref': '#/components/schemas/AuthToken' } assert schema['components']['schemas']['AuthToken'] == { 'type': 'object', 'properties': { 'username': {'type': 'string', 'writeOnly': True}, 'password': {'type': 'string', 'writeOnly': True}, 'token': {'type': 'string', 'readOnly': True}, }, 'required': ['username', 'password'] } def test_component_name(self): patterns = [ path('example/', views.ExampleAutoSchemaComponentName.as_view()), ] generator = SchemaGenerator(patterns=patterns) request = create_request('/') schema = generator.get_schema(request=request) print(schema) assert 'components' in schema assert 'schemas' in schema['components'] assert 'Ulysses' in schema['components']['schemas'] def test_duplicate_component_name(self): patterns = [ path('duplicate1/', views.ExampleAutoSchemaDuplicate1.as_view()), path('duplicate2/', views.ExampleAutoSchemaDuplicate2.as_view()), ] generator = SchemaGenerator(patterns=patterns) request = create_request('/') with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') schema = generator.get_schema(request=request) assert len(w) == 1 assert issubclass(w[-1].category, UserWarning) assert 'has been overridden with a different value.' in str(w[-1].message) assert 'components' in schema assert 'schemas' in schema['components'] assert 'Duplicate' in schema['components']['schemas'] def test_component_should_not_be_generated_for_delete_method(self): class ExampleView(generics.DestroyAPIView): schema = AutoSchema(operation_id_base='example') url_patterns = [ path('example/', ExampleView.as_view()), ] generator = SchemaGenerator(patterns=url_patterns) schema = generator.get_schema(request=create_request('/')) assert 'components' not in schema assert 'content' not in schema['paths']['/example/']['delete']['responses']['204']