def test_mocked_request_with_get_queryset_get_serializer_class(no_warnings): class M4(models.Model): pass class XSerializer(serializers.ModelSerializer): class Meta: fields = '__all__' model = M4 class XViewset(viewsets.ReadOnlyModelViewSet): def get_serializer_class(self): assert not self.request.user.is_authenticated assert self.action in ['retrieve', 'list'] return XSerializer def get_queryset(self): assert not self.request.user.is_authenticated assert self.request.method == 'GET' return M4.objects.none() generate_schema('x', XViewset)
def test_manual_security_method_addition(no_warnings): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) operation_security = schema['paths']['/x/']['get']['security'] schema_security = schema['components']['securitySchemes'] assert len(operation_security) == 4 and any( ['apiKeyAuth' in os for os in operation_security]) assert len(schema_security) == 3 and 'apiKeyAuth' in schema_security
def test_extend_schema_no_req_no_res(no_warnings): class XAPIView(APIView): @extend_schema(request=None, responses=None) def post(self, request): pass # pragma: no cover schema = generate_schema('/x', view=XAPIView) validate_schema(schema) operation = schema['paths']['/x']['post'] assert 'requestBody' not in operation assert len(operation['responses']['200']) == 1 assert 'description' in operation['responses']['200']
def test_token_auth_with_bearer_keyword(no_warnings): class CustomTokenAuthentication(TokenAuthentication): keyword = 'Bearer' @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover view_func.cls.authentication_classes = [CustomTokenAuthentication] schema = generate_schema('x', view_function=view_func) assert schema['components']['securitySchemes']['tokenAuth']['scheme'] == 'bearer'
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-sliding/', TokenObtainSlidingView.as_view(), name='token_obtain_sliding'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), ] schema = generate_schema(None, patterns=urlpatterns) assert_schema(schema, 'tests/contrib/test_simplejwt.yml')
def test_unable_to_follow_field_source_through_intermediate_property_warning( capsys): class FailingFieldSourceTraversalModel1(models.Model): @property def x(self): # missing type hint emits warning return # pragma: no cover class XSerializer(serializers.ModelSerializer): x = serializers.ReadOnlyField(source='x.y') class Meta: model = FailingFieldSourceTraversalModel1 fields = '__all__' class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) assert ('could not follow field source through intermediate property' ) in capsys.readouterr().err
def test_enum_collision_without_override(capsys): class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A')]) class YSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A'), ('B', 'B')]) class ZSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A'), ('B', 'B')]) class XAPIView(APIView): @extend_schema(responses=ZSerializer) def get(self, request): pass # pragma: no cover @extend_schema(request=XSerializer, responses=YSerializer) def post(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) assert 'enum naming encountered a non-optimally resolvable' in capsys.readouterr( ).err
def test_global_enum_naming_override(no_warnings): # the override will prevent the warning for multiple names class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=language_choices) bar = serializers.ChoiceField(choices=language_choices) class XView(generics.RetrieveAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert 'LanguageEnum' in schema['components']['schemas']['X']['properties']['foo']['$ref'] assert 'LanguageEnum' in schema['components']['schemas']['X']['properties']['bar']['$ref'] assert len(schema['components']['schemas']) == 2
def test_path_implicit_required(no_warnings): class M2Serializer(serializers.Serializer): pass # pragma: no cover class M2Viewset(viewsets.GenericViewSet): serializer_class = M2Serializer @extend_schema(parameters=[OpenApiParameter('id', str, 'path')]) def retrieve(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('m2', M2Viewset) validate_schema(schema)
def test_view_function_extension(no_warnings): class FixXFunctionView(OpenApiViewExtension): target_class = 'tests.test_extensions.x_view_function' def view_replacement(self): fixed = extend_schema(responses=OpenApiTypes.FLOAT)( self.target_class) return fixed schema = generate_schema('x', view_function=x_view_function) operation = schema['paths']['/x']['get'] assert get_response_schema(operation)['type'] == 'number' assert operation['description'].strip() == 'underspecified library view'
def test_multiple_media_types(no_warnings): @extend_schema(responses={ (200, 'application/json'): OpenApiTypes.OBJECT, (200, 'application/pdf'): OpenApiTypes.BINARY, }) class XAPIView(APIView): def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) content = schema['paths']['/x']['get']['responses']['200']['content'] assert content['application/pdf']['schema']['format'] == 'binary' assert content['application/json']['schema']['type'] == 'object'
def test_api_view_decorator_multi(no_warnings): @extend_schema(request=OpenApiTypes.FLOAT, responses=OpenApiTypes.INT, methods=['POST']) @extend_schema(responses=OpenApiTypes.FLOAT, methods=['GET']) @api_view(['GET', 'POST']) def pi(request): pass # pragma: no cover schema = generate_schema('x', view_function=pi) operation = schema['paths']['/x']['get'] assert get_response_schema(operation)['type'] == 'number' operation = schema['paths']['/x']['post'] assert get_request_schema(operation)['type'] == 'number' assert get_response_schema(operation)['type'] == 'integer'
def test_drf_jwt(no_warnings): from rest_framework_jwt.views import obtain_jwt_token router = routers.SimpleRouter() router.register('x', XViewset, basename="x") urlpatterns = [ *router.urls, path('api-token-auth/', obtain_jwt_token, name='get_token'), ] schema = generate_schema(None, patterns=urlpatterns) assert_schema(schema, 'tests/contrib/test_drf_jwt.yml')
def test_simplejwt_non_std_header_name(no_warnings): schema = generate_schema('/x', XViewset) assert schema['components']['securitySchemes'] == { 'jwtAuth': { 'type': 'apiKey', 'in': 'header', 'name': 'X-token', 'description': 'Token-based authentication with required prefix "Bearer"' } }
def test_extend_schema_field_exclusion(no_warnings): @extend_schema_field(None) class CustomField(serializers.IntegerField): pass # pragma: no cover class XSerializer(serializers.Serializer): id = serializers.IntegerField() hidden = CustomField() class XView(generics.CreateAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert 'hidden' not in schema['components']['schemas']['X']['properties']
def test_extend_schema_serializer_field_exclusion(no_warnings): @extend_schema_serializer(exclude_fields=['hidden1', 'hidden2']) class XSerializer(serializers.Serializer): integer = serializers.IntegerField() hidden1 = serializers.IntegerField() hidden2 = serializers.CharField() class XView(generics.ListCreateAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert 'integer' in schema['components']['schemas']['X']['properties'] assert 'hidden1' not in schema['components']['schemas']['X']['properties'] assert 'hidden2' not in schema['components']['schemas']['X']['properties']
def test_view_function_extension(no_warnings): class FixXFunctionView(OpenApiViewExtension): target_class = 'tests.test_extensions.x_view_function' def view_replacement(self): fixed = extend_schema(responses=OpenApiTypes.FLOAT)( self.target_class) return fixed schema = generate_schema('x', view_function=x_view_function) validate_schema(schema) operation = schema['paths']['/x']['get'] assert operation['responses']['200']['content']['application/json'][ 'schema']['type'] == 'number'
def test_layered_extend_schema_on_view_and_method_with_meta(no_warnings): class XSerializer(serializers.Serializer): field = serializers.IntegerField() @extend_schema(tags=['view_tag2']) @extend_schema(tags=['view_tag'], description='view_desc', summary='view_sum') class XViewset(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer @extend_schema(tags=['create_tag2']) @extend_schema(tags=['create_tag'], description='create_desc') def create(self, request, *args, **kwargs): super().create(request, *args, **kwargs) # pragma: no cover @extend_schema(tags=['extended_action_tag2']) @extend_schema(tags=['extended_action_tag'], description='extended_action_desc') @action(detail=False, methods=['GET']) def extended_action(self, request): return Response() # pragma: no cover @action(detail=False, methods=['GET']) def raw_action(self, request): return Response() # pragma: no cover schema = generate_schema('x', XViewset) create_op = schema['paths']['/x/']['post'] list_op = schema['paths']['/x/']['get'] raw_action_op = schema['paths']['/x/raw_action/']['get'] extended_action_op = schema['paths']['/x/extended_action/']['get'] assert create_op['tags'][0] == 'create_tag2' assert create_op['description'] == 'create_desc' assert create_op['summary'] == 'view_sum' assert list_op['tags'][0] == 'view_tag2' assert list_op['description'] == 'view_desc' assert list_op['summary'] == 'view_sum' assert raw_action_op['tags'][0] == 'view_tag2' assert raw_action_op['description'] == 'view_desc' assert raw_action_op['summary'] == 'view_sum' assert extended_action_op['tags'][0] == 'extended_action_tag2' assert extended_action_op['description'] == 'extended_action_desc' assert extended_action_op['summary'] == 'view_sum'
def test_view_extension(no_warnings): class FixXView(OpenApiViewExtension): target_class = 'tests.test_extensions.XView' def view_replacement(self): class Fixed(self.target_class): @extend_schema(responses=OpenApiTypes.FLOAT) def get(self, request): pass # pragma: no cover return Fixed schema = generate_schema('x', view=XView) operation = schema['paths']['/x']['get'] assert get_response_schema(operation)['type'] == 'number'
def test_resolvable_enum_collision(no_warnings): class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A')]) class YSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A'), ('B', 'B')]) class XAPIView(APIView): @extend_schema(request=XSerializer, responses=YSerializer) def post(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) assert 'XFooEnum' in schema['components']['schemas'] assert 'YFooEnum' in schema['components']['schemas']
def test_list_serializer_with_field_child_on_extend_schema(): class XAPIView(APIView): @extend_schema( request=serializers.ListSerializer(child=serializers.IntegerField()), responses=serializers.ListSerializer(child=serializers.IntegerField()), ) def post(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) req_schema = get_request_schema(schema['paths']['/x']['post']) res_schema = get_response_schema(schema['paths']['/x']['post']) for s in [req_schema, res_schema]: assert s['type'] == 'array' assert s['items']['type'] == 'integer'
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), ] schema = generate_schema(None, patterns=urlpatterns) 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_queryset_filter_and_ordering_only_on_list(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.none() serializer_class = SimpleSerializer filter_backends = (filters.SearchFilter, filters.OrderingFilter) schema = generate_schema('x', XViewset) retrieve_parameters = schema['paths']['/x/']['get']['parameters'] assert len(retrieve_parameters) == 2 assert retrieve_parameters[0]['name'] == 'ordering' assert retrieve_parameters[1]['name'] == 'search' list_parameters = schema['paths']['/x/{id}/']['get']['parameters'] assert len(list_parameters) == 1 assert list_parameters[0]['name'] == 'id'
def test_component_split_nested_explicit_ro_wo_serializer(no_warnings): class NestedSerializer(serializers.Serializer): field = serializers.IntegerField() class XSerializer(serializers.Serializer): ro = NestedSerializer(read_only=True) wo = NestedSerializer(write_only=True, required=False) class XView(generics.ListCreateAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert 'NestedRequest' in schema['components']['schemas'] assert 'Nested' in schema['components']['schemas'] assert len(schema['components']['schemas']['X']['properties']) == 1 assert len(schema['components']['schemas']['XRequest']['properties']) == 1
def test_manual_decimal_validator(): # manually test this validator as it is not part of the default workflow class XSerializer(serializers.Serializer): field = serializers.CharField(validators=[ validators.DecimalValidator(max_digits=4, decimal_places=2) ]) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) field = schema['components']['schemas']['X']['properties']['field'] assert field['maximum'] == 100 assert field['minimum'] == -100
def test_exclude_discovered_parameter(no_warnings): @extend_schema_view(list=extend_schema(parameters=[ # keep 'offset', remove 'limit', and add 'random' OpenApiParameter('limit', exclude=True), OpenApiParameter('random', bool), ])) class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer pagination_class = pagination.LimitOffsetPagination schema = generate_schema('x', XViewset) parameters = schema['paths']['/x/']['get']['parameters'] assert len(parameters) == 2 assert parameters[0]['name'] == 'offset' assert parameters[1]['name'] == 'random'
def test_serializer_field_extension(no_warnings): class Base64FieldExtension(OpenApiSerializerFieldExtension): target_class = 'tests.test_extensions.Base64Field' def map_serializer_field(self, auto_schema, direction): return build_basic_type(OpenApiTypes.BYTE) class XSerializer(serializers.Serializer): hash = Base64Field() class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer schema = generate_schema('x', XViewset) assert schema['components']['schemas']['X']['properties']['hash']['type'] == 'string' assert schema['components']['schemas']['X']['properties']['hash']['format'] == 'byte'
def test_multi_method_action(no_warnings): class DummySerializer(serializers.Serializer): id = serializers.UUIDField() class UpdateSerializer(serializers.Serializer): id = serializers.UUIDField() class CreateSerializer(serializers.Serializer): id = serializers.UUIDField() class XViewset(viewsets.GenericViewSet): serializer_class = DummySerializer # basic usage @extend_schema(request=UpdateSerializer, methods=['PUT']) @extend_schema(request=CreateSerializer, methods=['POST']) @action(detail=False, methods=['PUT', 'POST']) def multi(self, request, *args, **kwargs): pass # pragma: no cover # bolt-on decorator variation @extend_schema(request=CreateSerializer) @action(detail=False, methods=['POST']) def multi2(self, request, *args, **kwargs): pass # pragma: no cover @extend_schema(request=UpdateSerializer) @multi2.mapping.put def multi2put(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('x', XViewset) validate_schema(schema) def get_req_body(s): return s['requestBody']['content']['application/json']['schema'][ '$ref'] assert get_req_body( schema['paths']['/x/multi/']['put']) == '#/components/schemas/Update' assert get_req_body( schema['paths']['/x/multi/']['post']) == '#/components/schemas/Create' assert get_req_body( schema['paths']['/x/multi2/']['put']) == '#/components/schemas/Update' assert get_req_body( schema['paths']['/x/multi2/']['post']) == '#/components/schemas/Create'
def test_list_serializer_with_pagination(no_warnings): class GenreSerializer(serializers.Serializer): genre = serializers.CharField() class XViewSet(viewsets.GenericViewSet): pagination_class = pagination.LimitOffsetPagination @extend_schema(responses=GenreSerializer(many=True)) @action(methods=["GET"], detail=False) def genre(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('/x', XViewSet) response = get_response_schema(schema['paths']['/x/genre/']['get']) assert response['$ref'] == '#/components/schemas/PaginatedGenreList' assert 'PaginatedGenreList' in schema['components']['schemas'] assert 'Genre' in schema['components']['schemas']
def test_type_hint_extraction(no_warnings, type_hint, ref_schema): def func() -> type_hint: pass # pragma: no cover # check expected resolution schema = resolve_type_hint(typing.get_type_hints(func).get('return')) assert json.dumps(schema) == json.dumps(ref_schema) # check schema validity class XSerializer(serializers.Serializer): x = serializers.SerializerMethodField() XSerializer.get_x = func class XView(generics.RetrieveAPIView): serializer_class = XSerializer validate_schema(generate_schema('/x', view=XView))