def test_namespace_versioning_urlpatterns_simplification(no_warnings): class NamespaceVersioningAPIView(generics.RetrieveUpdateDestroyAPIView): versioning_class = NamespaceVersioning serializer_class = Xv1Serializer queryset = VersioningModel.objects.all() urls = ( path('x/<int:pk>/', NamespaceVersioningAPIView.as_view()), path('y/<pk>/', NamespaceVersioningAPIView.as_view()), re_path('z/(?P<pk>[0-9A-Fa-f-]+)/', NamespaceVersioningAPIView.as_view()), ) generator = SchemaGenerator( patterns=[ path('v1/<int:some_param>/', include((urls, 'v1'))), ], api_version='v1', ) schema = generator.get_schema(request=None, public=True) for s in ['x', 'y', 'z']: parameters = schema['paths'][f'/v1/{{some_param}}/{s}/{{id}}/']['get'][ 'parameters'] parameters = {p['name']: p for p in parameters} assert parameters['id']['schema']['type'] == 'integer' assert parameters['some_param']['schema']['type'] == 'integer'
def test_serializer_name_reuse(capsys): from rest_framework import routers from drf_spectacular.generators import SchemaGenerator router = routers.SimpleRouter() def x1(): class XSerializer(serializers.Serializer): uuid = serializers.UUIDField() return XSerializer def x2(): class XSerializer(serializers.Serializer): integer = serializers.IntegerField() return XSerializer class X1Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = x1() router.register('x1', X1Viewset, basename='x1') class X2Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = x2() router.register('x2', X2Viewset, basename='x2') generator = SchemaGenerator(patterns=router.urls) generator.get_schema(request=None, public=True) stderr = capsys.readouterr().err assert 'Encountered 2 components with identical names "X" and different classes' in stderr
def test_serializer_retrieval_from_view(no_warnings): class UnusedSerializer(serializers.Serializer): pass # pragma: no cover class XSerializer(serializers.Serializer): id = serializers.UUIDField() class YSerializer(serializers.Serializer): id = serializers.UUIDField() class X1Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = UnusedSerializer def get_serializer(self): return XSerializer() class X2Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): def get_serializer_class(self): return YSerializer router = routers.SimpleRouter() router.register('x1', X1Viewset, basename='x1') router.register('x2', X2Viewset, basename='x2') generator = SchemaGenerator(patterns=router.urls) schema = generator.get_schema(request=None, public=True) validate_schema(schema) assert len(schema['components']['schemas']) == 2 assert 'Unused' not in schema['components']['schemas']
def test_serializer_name_reuse(warnings): from rest_framework import routers from drf_spectacular.generators import SchemaGenerator router = routers.SimpleRouter() def x1(): class XSerializer(serializers.Serializer): uuid = serializers.UUIDField() return XSerializer def x2(): class XSerializer(serializers.Serializer): integer = serializers.IntegerField return XSerializer class X1Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = x1() router.register('x1', X1Viewset, basename='x1') class X2Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = x2() router.register('x2', X2Viewset, basename='x2') generator = SchemaGenerator(patterns=router.urls) generator.get_schema(request=None, public=True)
def generate_schema(route, viewset=None, view=None, view_function=None, patterns=None): from django.urls import path from rest_framework import routers from rest_framework.viewsets import ViewSetMixin from drf_spectacular.generators import SchemaGenerator if viewset: assert issubclass(viewset, ViewSetMixin) router = routers.SimpleRouter() router.register(route, viewset, basename=route) patterns = router.urls elif view: patterns = [path(route, view.as_view())] elif view_function: patterns = [path(route, view_function)] else: assert route is None and isinstance(patterns, list) generator = SchemaGenerator(patterns=patterns) schema = generator.get_schema(request=None, public=True) validate_schema(schema) # make sure generated schemas are always valid return schema
def test_namespace_versioning_urlpatterns_simplification(no_warnings, path_func, path_str, pattern): class LookupModel(models.Model): field = models.IntegerField() class LookupSerializer(serializers.ModelSerializer): class Meta: model = LookupModel fields = '__all__' class NamespaceVersioningAPIView(generics.RetrieveUpdateDestroyAPIView): versioning_class = NamespaceVersioning serializer_class = LookupSerializer queryset = LookupModel.objects.all() # make sure regex are valid if path_func == re_path: re.compile(pattern) patterns_v1 = [path_func(pattern, NamespaceVersioningAPIView.as_view())] generator = SchemaGenerator( patterns=[path('v1/<int:some_param>/', include((patterns_v1, 'v1')))], api_version='v1', ) schema = generator.get_schema(request=None, public=True) parameters = schema['paths']['/v1/{some_param}/' + path_str]['get']['parameters'] for p in parameters: assert p['schema']['type'] == 'integer'
def test_schema_contains_only_urlpatterns_first_match(no_warnings): class XSerializer(serializers.Serializer): integer = serializers.IntegerField() class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover class YSerializer(serializers.Serializer): integer = serializers.DateTimeField() class YAPIView(APIView): @extend_schema(responses=YSerializer) def get(self, request): pass # pragma: no cover urlpatterns = [ path('api/x/', XAPIView.as_view()), # only first occurrence is used path('api/x/', YAPIView.as_view()), ] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) validate_schema(schema) assert len(schema['components']['schemas']) == 1 assert 'X' in schema['components']['schemas'] operation = schema['paths']['/api/x/']['get'] assert '#/components/schemas/X' in get_response_schema(operation)['$ref']
def test_basics(no_warnings): router = routers.SimpleRouter() router.register('albums', AlbumModelViewset, basename="album") generator = SchemaGenerator(patterns=router.urls) schema = generator.get_schema(request=None, public=True) assert_schema(schema, 'tests/test_basic.yml')
def test_url_path_versioning(no_warnings, viewset_cls, version): router = routers.SimpleRouter() router.register('x', viewset_cls, basename='x') generator = SchemaGenerator( patterns=[re_path(r'^(?P<version>[v1|v2]+)/', include((router.urls, 'x')))], api_version=version, ) schema = generator.get_schema(request=None, public=True) assert_schema(schema, f'tests/test_versioning_{version}.yml')
def test_accept_header_versioning(no_warnings, viewset_cls, version): router = routers.SimpleRouter() router.register('x', viewset_cls, basename='x') generator = SchemaGenerator( patterns=[ path('', include((router.urls, 'x'))), ], api_version=version, ) schema = generator.get_schema(request=None, public=True) assert_schema(schema, f'tests/test_versioning_accept_{version}.yml')
def test_rest_auth(no_warnings): urlpatterns = [ path('rest-auth/', include('dj_rest_auth.urls')), path('rest-auth/registration/', include('dj_rest_auth.registration.urls')), ] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) assert_schema( schema, 'tests/contrib/test_rest_auth.yml', transforms=transforms )
def test_fields(no_warnings): from django.core.validators import URLValidator generator = SchemaGenerator() schema = generator.get_schema(request=None, public=True) # url pattern changed between django 3.0 and 3.1 field_url = schema['components']['schemas']['AllFields']['properties']['field_url'] assert field_url['pattern'] == URLValidator.regex.pattern field_url['pattern'] = 'URL_REGEX_PATTERN' assert_schema(schema, 'tests/test_fields.yml')
def test_drf_jwt(no_warnings): router = routers.SimpleRouter() router.register('x', XViewset, basename="x") urlpatterns = [ *router.urls, path('api-token-auth/', obtain_jwt_token, name='get_token'), ] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) assert_schema(schema, 'tests/contrib/test_drf_jwt.yml')
def test_regex_path_parameter_discovery(no_warnings): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def pi(request, foo): pass # pragma: no cover urlpatterns = [url(r'^/pi/<int:precision>', pi)] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) parameter = schema['paths']['/pi/{precision}']['get']['parameters'][0] assert parameter['name'] == 'precision' assert parameter['in'] == 'path' assert parameter['schema']['type'] == 'integer'
def test_simplejwt(no_warnings): router = routers.SimpleRouter() router.register('x', XViewset, basename="x") urlpatterns = [ *router.urls, path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), ] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) assert_schema(schema, 'tests/contrib/test_simplejwt.yml')
def test_oauth2_toolkit(no_warnings): router = routers.SimpleRouter() router.register('x', XViewset, basename="x") urlpatterns = [ *router.urls, path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) assert_schema(schema, 'tests/contrib/test_oauth_toolkit.yml')
def test_append_extra_components(no_warnings): class XSerializer(serializers.Serializer): id = serializers.UUIDField() class XAPIView(APIView): @extend_schema(responses={200: XSerializer}) def get(self, request): pass # pragma: no cover generator = SchemaGenerator(patterns=[ url(r'^x$', XAPIView.as_view(), name='x'), ]) schema = generator.get_schema(request=None, public=True) assert len(schema['components']['schemas']) == 2 validate_schema(schema)
def generate_schema(route, viewset=None, view=None): from django.urls import path from rest_framework import routers from drf_spectacular.generators import SchemaGenerator patterns = [] if viewset: router = routers.SimpleRouter() router.register(route, viewset, basename=route) patterns = router.urls if view: patterns = [path(route, view.as_view())] generator = SchemaGenerator(patterns=patterns) return generator.get_schema(request=None, public=True)
def test_drf_format_suffix_parameter_exclude(no_warnings): from rest_framework.urlpatterns import format_suffix_patterns @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover urlpatterns = format_suffix_patterns([ path('pi', view_func), ]) generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) validate_schema(schema) assert list(schema['paths'].keys()) == ['/pi']
def test_oauth2_toolkit(no_warnings): router = routers.SimpleRouter() router.register('TokenHasReadWriteScope', TokenHasReadWriteScopeViewset, basename="x1") router.register('TokenHasResourceScope', TokenHasResourceScopeViewset, basename="x2") router.register('IsAuthenticatedOrTokenHasScope', IsAuthenticatedOrTokenHasScopeViewset, basename="x3") urlpatterns = [ *router.urls, path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) assert_schema(schema, 'tests/contrib/test_oauth_toolkit.yml')
class DrfSpectacularSchemaLoader(BaseSchemaLoader): """ Loads OpenAPI schema generated by drf_spectacular. """ def __init__(self, field_key_map: Optional[Dict[str, str]] = None) -> None: super().__init__(field_key_map=field_key_map) from drf_spectacular.generators import SchemaGenerator self.schema_generator = SchemaGenerator() def load_schema(self) -> dict: """ Loads generated schema from drf_spectacular and returns it as a dict. """ return loads(dumps(self.schema_generator.get_schema(public=True))) def resolve_path(self, endpoint_path: str, method: str) -> Tuple[str, ResolverMatch]: from drf_spectacular.settings import spectacular_settings de_parameterized_path, resolved_path = super().resolve_path( endpoint_path=endpoint_path, method=method) return ( de_parameterized_path[len(spectacular_settings. SCHEMA_PATH_PREFIX or ""):], resolved_path, )
def test_mock_request_symmetry_plain(no_warnings): response = APIClient().get('/api/schema-plain/', **{'HTTP_X_SPECIAL_HEADER': '1'}) assert response.status_code == 200 schema_online = yaml.load(response.content, Loader=yaml.SafeLoader) schema_offline = SchemaGenerator().get_schema(public=True) assert schema_offline == schema_online
def test_operation_id_collision_resolution(capsys): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover urlpatterns = [ path('pi/<int:foo>', view_func), path('pi/', view_func), ] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) assert schema['paths']['/pi/']['get']['operationId'] == 'pi_retrieve' assert schema['paths']['/pi/{foo}']['get']['operationId'] == 'pi_retrieve_2' assert 'operationId "pi_retrieve" has collisions' in capsys.readouterr().err
def test_free_form_responses(no_warnings): class XAPIView(APIView): @extend_schema(responses={200: OpenApiTypes.OBJECT}) def get(self, request): pass # pragma: no cover class YAPIView(APIView): @extend_schema(responses=OpenApiTypes.OBJECT) def get(self, request): pass # pragma: no cover generator = SchemaGenerator(patterns=[ url(r'^x$', XAPIView.as_view(), name='x'), url(r'^y$', YAPIView.as_view(), name='y'), ]) schema = generator.get_schema(request=None, public=True) validate_schema(schema)
def test_rest_auth_token(no_warnings, settings): settings.REST_USE_JWT = True # flush module import cache to re-evaluate conditional import import dj_rest_auth.urls reload(dj_rest_auth.urls) urlpatterns = [ # path('rest-auth/', include(urlpatterns)), path('rest-auth/', include('dj_rest_auth.urls')), path('rest-auth/registration/', include('dj_rest_auth.registration.urls')), ] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) assert_schema( schema, 'tests/contrib/test_rest_auth_token.yml', transforms=transforms )
def test_drf_format_suffix_parameter(no_warnings): from rest_framework.urlpatterns import format_suffix_patterns @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def pi(request, format=None): pass # pragma: no cover urlpatterns = [path(r'/pi', pi)] urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'html']) generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) assert len(schema['paths']) == 2 format_parameter = schema['paths']['/pi{format}']['get']['parameters'][0] assert format_parameter['name'] == 'format' assert format_parameter['required'] is False assert format_parameter['in'] == 'path'
def test_basic_viewset_without_queryset_with_explicit_pk_typing(no_warnings): class XSerializer(serializers.Serializer): field = fields.IntegerField() class XViewset(viewsets.ViewSet): serializer_class = XSerializer def retrieve(self, request, *args, **kwargs): pass # pragma: no cover urlpatterns = [ path("api/<path:some_var>/<uuid:pk>/", XViewset.as_view({"get": "retrieve"})) ] generator = SchemaGenerator(patterns=urlpatterns) schema = generator.get_schema(request=None, public=True) validate_schema(schema) operation = schema['paths']['/api/{some_var}/{id}/']['get'] assert operation['parameters'][0]['name'] == 'id' assert operation['parameters'][0]['schema']['format'] == 'uuid'
def test_mock_request_symmetry_version(no_warnings): response = APIClient().get( '/api/schema-versioned/', **{ 'HTTP_ACCEPT': 'application/json; version=v2', }) assert response.status_code == 200 schema_online = yaml.load(response.content, Loader=yaml.SafeLoader) schema_offline = SchemaGenerator(api_version='v2').get_schema(public=True) assert schema_offline == schema_online assert schema_online['info']['version'] == '0.0.0 (v2)'
def test_pagination_reusage(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer pagination_class = pagination.LimitOffsetPagination @extend_schema(responses={'200': SimpleSerializer(many=True)}) @action(methods=['GET'], detail=False) def custom_action(self): pass # pragma: no cover class YViewset(XViewset): serializer_class = SimpleSerializer router = routers.SimpleRouter() router.register('x', XViewset, basename='x') router.register('y', YViewset, basename='y') generator = SchemaGenerator(patterns=router.urls) schema = generator.get_schema(request=None, public=True) validate_schema(schema)
def test_accept_header_versioning(no_warnings, viewset_cls, version, with_request): router = routers.SimpleRouter() router.register('x', viewset_cls, basename='x') generator = SchemaGenerator( patterns=[ path('', include((router.urls, 'x'))), ], api_version=version, ) if with_request: view = SpectacularAPIView( versioning_class=AcceptHeaderVersioning, ) factory = APIRequestFactory() request = factory.get('x', content_type='application/vnd.oai.openapi+json') request = view.initialize_request(request) else: request = None schema = generator.get_schema(request=request, public=True) assert_schema(schema, f'tests/test_versioning_accept_{version}.yml')