Add more tests and cleanup coverage reports (#13)

Closes #11
openapi3
Cristi Vîjdea 2017-12-12 17:30:58 +01:00 committed by GitHub
parent 53ac55a24b
commit 8883894775
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 385 additions and 134 deletions

View File

@ -35,8 +35,8 @@ You want to contribute some code? Great! Here are a few steps to get you started
$ virtualenv venv $ virtualenv venv
$ source venv/bin/activate $ source venv/bin/activate
(venv) $ pip install -e .[validation,test] (venv) $ pip install -e .[validation]
(venv) $ pip install -r requirements/dev.txt (venv) $ pip install -rrequirements/dev.txt -rrequirements/test.txt
#. Make your changes and check them against the test project #. Make your changes and check them against the test project

View File

@ -280,6 +280,18 @@ This method is currently the only way to get both syntactic and semantic validat
The other validators only provide JSON schema-level validation, but miss things like duplicate operation names, The other validators only provide JSON schema-level validation, but miss things like duplicate operation names,
improper content types, etc improper content types, etc
5. Code generation
==================
You can use the specification outputted by this library together with
`swagger-codegen <https://github.com/swagger-api/swagger-codegen>`_ to generate client code in your language of choice:
.. code:: console
$ docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate -i /local/tests/reference.yaml -l javascript -o /local/.codegen/js
See the github page linked above for more details.
********** **********
Background Background
********** **********

6
package-lock.json generated
View File

@ -4,9 +4,9 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"dependencies": { "dependencies": {
"swagger-ui-dist": { "swagger-ui-dist": {
"version": "3.5.0", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.5.0.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.6.1.tgz",
"integrity": "sha1-JuvzMRqaYP6dFwYS7tUoKmW8kiY=" "integrity": "sha1-uzQgV/h2COTs2DlGMDSJxjYicgY="
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "drf-swagger", "name": "drf-swagger",
"dependencies": { "dependencies": {
"swagger-ui-dist": "^3.5.0" "swagger-ui-dist": "^3.6.1"
} }
} }

View File

@ -1,7 +1,9 @@
# pytest runner + plugins # pytest runner + plugins
pytest-django>=3.1.2 pytest>=2.9
pytest-pythonpath>=0.7.1 pytest-pythonpath>=0.7.1
pytest-cov>=2.5.1 pytest-cov>=2.5.1
# latest pip version of pytest-django is more than a year old and does not support Django 2.0
git+https://github.com/pytest-dev/pytest-django.git@94cccb956435dd7a719606744ee7608397e1eafb
datadiff==2.0.0 datadiff==2.0.0
# test project requirements # test project requirements

View File

@ -16,7 +16,6 @@ with io.open('README.rst', encoding='utf-8') as readme:
requirements = ['djangorestframework>=3.7.0'] + read_req('base.txt') requirements = ['djangorestframework>=3.7.0'] + read_req('base.txt')
requirements_validation = read_req('validation.txt') requirements_validation = read_req('validation.txt')
requirements_test = read_req('test.txt')
setup( setup(
name='drf-swagger', name='drf-swagger',
@ -25,10 +24,8 @@ setup(
package_dir={'': 'src'}, package_dir={'': 'src'},
include_package_data=True, include_package_data=True,
install_requires=requirements, install_requires=requirements,
tests_require=requirements_test,
extras_require={ extras_require={
'validation': requirements_validation, 'validation': requirements_validation,
'test': requirements_test,
}, },
license='BSD License', license='BSD License',
description='Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code.', description='Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code.',

View File

@ -1,5 +1,5 @@
from django.conf import settings from django.conf import settings
from rest_framework.settings import APISettings from rest_framework.settings import perform_import
SWAGGER_DEFAULTS = { SWAGGER_DEFAULTS = {
'USE_SESSION_AUTH': True, 'USE_SESSION_AUTH': True,
@ -30,16 +30,49 @@ REDOC_DEFAULTS = {
IMPORT_STRINGS = [] IMPORT_STRINGS = []
class AppSettings(object):
"""
Stolen from Django Rest Framework, removed caching for easier testing
"""
def __init__(self, user_settings, defaults, import_strings=None):
self._user_settings = user_settings
self.defaults = defaults
self.import_strings = import_strings or []
@property
def user_settings(self):
return getattr(settings, self._user_settings, {})
def __getattr__(self, attr):
if attr not in self.defaults:
raise AttributeError("Invalid setting: '%s'" % attr) # pragma: no cover
try:
# Check if present in user settings
val = self.user_settings[attr]
except KeyError:
# Fall back to defaults
val = self.defaults[attr]
# Coerce import strings into classes
if attr in self.import_strings:
val = perform_import(val, attr)
return val
#: #:
swagger_settings = APISettings( swagger_settings = AppSettings(
user_settings=getattr(settings, 'SWAGGER_SETTINGS', {}), user_settings='SWAGGER_SETTINGS',
defaults=SWAGGER_DEFAULTS, defaults=SWAGGER_DEFAULTS,
import_strings=IMPORT_STRINGS, import_strings=IMPORT_STRINGS,
) )
#: #:
redoc_settings = APISettings( redoc_settings = AppSettings(
user_settings=getattr(settings, 'REDOC_SETTINGS', {}), user_settings='REDOC_SETTINGS',
defaults=REDOC_DEFAULTS, defaults=REDOC_DEFAULTS,
import_strings=IMPORT_STRINGS, import_strings=IMPORT_STRINGS,
) )

View File

@ -39,9 +39,6 @@ VALIDATORS = {
class _OpenAPICodec(object): class _OpenAPICodec(object):
media_type = None media_type = None
#: Allows easier mocking of settings
settings = swagger_settings
def __init__(self, validators): def __init__(self, validators):
self._validators = validators self._validators = validators
@ -89,7 +86,7 @@ class _OpenAPICodec(object):
:return: swagger spec as dict :return: swagger spec as dict
:rtype: OrderedDict :rtype: OrderedDict
""" """
swagger.security_definitions = self.settings.SECURITY_DEFINITIONS swagger.security_definitions = swagger_settings.SECURITY_DEFINITIONS
return swagger return swagger

View File

@ -65,10 +65,10 @@ class OpenAPISchemaGenerator(object):
view = self._gen.create_view(callback, method, request) view = self._gen.create_view(callback, method, request)
overrides = getattr(callback, 'swagger_auto_schema', None) overrides = getattr(callback, 'swagger_auto_schema', None)
if overrides is not None: if overrides is not None:
# decorated function based view must have its decorator information passed on to th re-instantiated view # decorated function based view must have its decorator information passed on to the re-instantiated view
for method, _ in overrides.items(): for method, _ in overrides.items():
view_method = getattr(view, method, None) view_method = getattr(view, method, None)
if view_method is not None: if view_method is not None: # pragma: no cover
setattr(view_method.__func__, 'swagger_auto_schema', overrides) setattr(view_method.__func__, 'swagger_auto_schema', overrides)
return view return view
@ -137,6 +137,7 @@ class OpenAPISchemaGenerator(object):
schema = auto_schema_cls(view, path, method, overrides, components) schema = auto_schema_cls(view, path, method, overrides, components)
operations[method.lower()] = schema.get_operation(operation_keys) operations[method.lower()] = schema.get_operation(operation_keys)
if operations:
paths[path] = openapi.PathItem(parameters=path_parameters, **operations) paths[path] = openapi.PathItem(parameters=path_parameters, **operations)
return openapi.Paths(paths=paths) return openapi.Paths(paths=paths)
@ -178,7 +179,7 @@ class OpenAPISchemaGenerator(object):
try: try:
model_field = model._meta.get_field(variable) model_field = model._meta.get_field(variable)
except Exception: except Exception:
model_field = None model_field = None # pragma: no cover
if model_field is not None and model_field.help_text: if model_field is not None and model_field.help_text:
description = force_text(model_field.help_text) description = force_text(model_field.help_text)

View File

@ -166,9 +166,9 @@ class SwaggerAutoSchema(object):
parameters = OrderedDict(((param.name, param.in_), param) for param in parameters) parameters = OrderedDict(((param.name, param.in_), param) for param in parameters)
manual_parameters = self.overrides.get('manual_parameters', None) or [] manual_parameters = self.overrides.get('manual_parameters', None) or []
if any(param.in_ == openapi.IN_BODY for param in manual_parameters): if any(param.in_ == openapi.IN_BODY for param in manual_parameters): # pragma: no cover
raise SwaggerGenerationError("specify the body parameter as a Schema or Serializer in request_body") raise SwaggerGenerationError("specify the body parameter as a Schema or Serializer in request_body")
if any(param.in_ == openapi.IN_FORM for param in manual_parameters): if any(param.in_ == openapi.IN_FORM for param in manual_parameters): # pragma: no cover
if any(param.in_ == openapi.IN_BODY for param in parameters.values()): if any(param.in_ == openapi.IN_BODY for param in parameters.values()):
raise SwaggerGenerationError("cannot add form parameters when the request has a request schema; " raise SwaggerGenerationError("cannot add form parameters when the request has a request schema; "
"did you forget to set an appropriate parser class on the view?") "did you forget to set an appropriate parser class on the view?")

View File

@ -5,7 +5,6 @@ from coreapi.compat import urlparse
from future.utils import raise_from from future.utils import raise_from
from inflection import camelize from inflection import camelize
TYPE_OBJECT = "object" #: TYPE_OBJECT = "object" #:
TYPE_STRING = "string" #: TYPE_STRING = "string" #:
TYPE_NUMBER = "number" #: TYPE_NUMBER = "number" #:
@ -212,7 +211,7 @@ class Paths(SwaggerDict):
super(Paths, self).__init__(**extra) super(Paths, self).__init__(**extra)
for path, path_obj in paths.items(): for path, path_obj in paths.items():
assert path.startswith("/") assert path.startswith("/")
if path_obj is not None: if path_obj is not None: # pragma: no cover
self[path] = path_obj self[path] = path_obj
self._insert_extras__() self._insert_extras__()
@ -403,7 +402,7 @@ class Responses(SwaggerDict):
""" """
super(Responses, self).__init__(**extra) super(Responses, self).__init__(**extra)
for status, response in responses.items(): for status, response in responses.items():
if response is not None: if response is not None: # pragma: no cover
self[str(status)] = response self[str(status)] = response
self.default = default self.default = default
self._insert_extras__() self._insert_extras__()

View File

@ -1,28 +1,28 @@
{ {
"_from": "swagger-ui-dist", "_from": "swagger-ui-dist@3.6.1",
"_id": "swagger-ui-dist@3.5.0", "_id": "swagger-ui-dist@3.6.1",
"_inBundle": false, "_inBundle": false,
"_integrity": "sha1-JuvzMRqaYP6dFwYS7tUoKmW8kiY=", "_integrity": "sha1-uzQgV/h2COTs2DlGMDSJxjYicgY=",
"_location": "/swagger-ui-dist", "_location": "/swagger-ui-dist",
"_phantomChildren": {}, "_phantomChildren": {},
"_requested": { "_requested": {
"type": "tag", "type": "version",
"registry": true, "registry": true,
"raw": "swagger-ui-dist", "raw": "swagger-ui-dist@3.6.1",
"name": "swagger-ui-dist", "name": "swagger-ui-dist",
"escapedName": "swagger-ui-dist", "escapedName": "swagger-ui-dist",
"rawSpec": "", "rawSpec": "3.6.1",
"saveSpec": null, "saveSpec": null,
"fetchSpec": "latest" "fetchSpec": "3.6.1"
}, },
"_requiredBy": [ "_requiredBy": [
"#USER", "#USER",
"/" "/"
], ],
"_resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.5.0.tgz", "_resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.6.1.tgz",
"_shasum": "26ebf3311a9a60fe9d170612eed5282a65bc9226", "_shasum": "bb342057f87608e4ecd83946303489c636227206",
"_spec": "swagger-ui-dist", "_spec": "swagger-ui-dist@3.6.1",
"_where": "C:\\Projects\\drf_openapi", "_where": "C:\\Projects\\drf-swagger",
"bugs": { "bugs": {
"url": "https://github.com/swagger-api/swagger-ui/issues" "url": "https://github.com/swagger-api/swagger-ui/issues"
}, },
@ -68,5 +68,5 @@
"type": "git", "type": "git",
"url": "git+ssh://git@github.com/swagger-api/swagger-ui.git" "url": "git+ssh://git@github.com/swagger-api/swagger-ui.git"
}, },
"version": "3.5.0" "version": "3.6.1"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -89,6 +89,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
it will automatically be converted into a :class:`.Schema` it will automatically be converted into a :class:`.Schema`
""" """
def decorator(view_method): def decorator(view_method):
data = { data = {
'auto_schema': auto_schema, 'auto_schema': auto_schema,
@ -165,6 +166,10 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
def SwaggerType(**instance_kwargs): def SwaggerType(**instance_kwargs):
if swagger_object_type == openapi.Parameter: if swagger_object_type == openapi.Parameter:
instance_kwargs['required'] = field.required instance_kwargs['required'] = field.required
if swagger_object_type != openapi.Items:
default = getattr(field, 'default', serializers.empty)
if default is not serializers.empty:
instance_kwargs['default'] = default
instance_kwargs.update(kwargs) instance_kwargs.update(kwargs)
return swagger_object_type(title=title, description=description, **instance_kwargs) return swagger_object_type(title=title, description=description, **instance_kwargs)
@ -249,7 +254,7 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
elif isinstance(field, serializers.RegexField): elif isinstance(field, serializers.RegexField):
return SwaggerType(type=openapi.TYPE_STRING, pattern=find_regex(field)) return SwaggerType(type=openapi.TYPE_STRING, pattern=find_regex(field))
elif isinstance(field, serializers.SlugField): elif isinstance(field, serializers.SlugField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_SLUG) return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_SLUG, pattern=find_regex(field))
elif isinstance(field, serializers.URLField): elif isinstance(field, serializers.URLField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI) return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI)
elif isinstance(field, serializers.IPAddressField): elif isinstance(field, serializers.IPAddressField):
@ -272,11 +277,6 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
if swagger_object_type != openapi.Parameter: if swagger_object_type != openapi.Parameter:
raise SwaggerGenerationError("parameter of type file is supported only in formData Parameter") raise SwaggerGenerationError("parameter of type file is supported only in formData Parameter")
return SwaggerType(type=openapi.TYPE_FILE) return SwaggerType(type=openapi.TYPE_FILE)
elif isinstance(field, serializers.JSONField):
return SwaggerType(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_BINARY if field.binary else None
)
elif isinstance(field, serializers.DictField) and swagger_object_type == openapi.Schema: elif isinstance(field, serializers.DictField) and swagger_object_type == openapi.Schema:
child_schema = serializer_field_to_swagger(field.child, ChildSwaggerType, definitions) child_schema = serializer_field_to_swagger(field.child, ChildSwaggerType, definitions)
return SwaggerType( return SwaggerType(
@ -284,7 +284,7 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
additional_properties=child_schema additional_properties=child_schema
) )
# TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField? # TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField? JSONField
# everything else gets string by default # everything else gets string by default
return SwaggerType(type=openapi.TYPE_STRING) return SwaggerType(type=openapi.TYPE_STRING)
@ -302,7 +302,7 @@ def find_regex(regex_field):
if isinstance(validator, RegexValidator): if isinstance(validator, RegexValidator):
if regex_validator is not None: if regex_validator is not None:
# bail if multiple validators are found - no obvious way to choose # bail if multiple validators are found - no obvious way to choose
return None return None # pragma: no cover
regex_validator = validator regex_validator = validator
# regex_validator.regex should be a compiled re object... # regex_validator.regex should be a compiled re object...

View File

@ -4,13 +4,24 @@ from articles.models import Article
class ArticleSerializer(serializers.ModelSerializer): class ArticleSerializer(serializers.ModelSerializer):
references = serializers.DictField(
help_text="this is a really bad example",
child=serializers.URLField(help_text="but i needed to test these 2 fields somehow"),
)
uuid = serializers.UUIDField(help_text="should articles have UUIDs?")
class Meta: class Meta:
model = Article model = Article
fields = ('title', 'body', 'slug', 'date_created', 'date_modified') fields = ('title', 'body', 'slug', 'date_created', 'date_modified', 'references', 'uuid')
read_only_fields = ('date_created', 'date_modified') read_only_fields = ('date_created', 'date_modified')
lookup_field = 'slug' lookup_field = 'slug'
extra_kwargs = {'body': {'help_text': 'body serializer help_text'}} extra_kwargs = {'body': {'help_text': 'body serializer help_text'}}
class ImageUploadSerializer(serializers.Serializer): class ImageUploadSerializer(serializers.Serializer):
what_am_i_doing = serializers.RegexField(regex=r"^69$", help_text="test")
image_styles = serializers.ListSerializer(
child=serializers.ChoiceField(choices=['wide', 'tall', 'thumb', 'social']),
help_text="Parameter with Items"
)
upload = serializers.ImageField(help_text="image serializer help_text") upload = serializers.ImageField(help_text="image serializer help_text")

View File

@ -28,9 +28,10 @@ class SnippetSerializer(serializers.Serializer):
code = serializers.CharField(style={'base_template': 'textarea.html'}) code = serializers.CharField(style={'base_template': 'textarea.html'})
linenos = serializers.BooleanField(required=False) linenos = serializers.BooleanField(required=False)
language = LanguageSerializer(help_text="Sample help text for language") language = LanguageSerializer(help_text="Sample help text for language")
style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['friendly'])
lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False) lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False)
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer()) example_projects = serializers.ListSerializer(child=ExampleProjectSerializer())
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField")
def create(self, validated_data): def create(self, validated_data):
""" """

View File

@ -97,6 +97,12 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'LOGIN_URL': '/admin/login', 'LOGIN_URL': '/admin/login',
'LOGOUT_URL': '/admin/logout', 'LOGOUT_URL': '/admin/logout',

View File

@ -1,6 +1,7 @@
from django.conf.urls import url, include from django.conf.urls import url, include
from django.contrib import admin from django.contrib import admin
from rest_framework import permissions from rest_framework import permissions
from rest_framework.decorators import api_view
from drf_swagger import openapi from drf_swagger import openapi
from drf_swagger.views import get_schema_view from drf_swagger.views import get_schema_view
@ -19,6 +20,12 @@ schema_view = get_schema_view(
permission_classes=(permissions.AllowAny,), permission_classes=(permissions.AllowAny,),
) )
@api_view(['GET'])
def plain_view(request):
pass
urlpatterns = [ urlpatterns = [
url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
@ -28,4 +35,5 @@ urlpatterns = [
url(r'^snippets/', include('snippets.urls')), url(r'^snippets/', include('snippets.urls')),
url(r'^articles/', include('articles.urls')), url(r'^articles/', include('articles.urls')),
url(r'^users/', include('users.urls')), url(r'^users/', include('users.urls')),
url(r'^plain/', plain_view),
] ]

View File

@ -4,9 +4,11 @@ from rest_framework import serializers
from snippets.models import Snippet from snippets.models import Snippet
class UserSerializer(serializers.ModelSerializer): class UserSerializerrr(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all()) snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
last_connected_ip = serializers.IPAddressField(help_text="i'm out of ideas", protocol='ipv4', read_only=True)
last_connected_at = serializers.DateField(help_text="really?", read_only=True)
class Meta: class Meta:
model = User model = User
fields = ('id', 'username', 'snippets') fields = ('id', 'username', 'email', 'snippets', 'last_connected_ip', 'last_connected_at')

View File

@ -7,16 +7,16 @@ from rest_framework.views import APIView
from drf_swagger import openapi from drf_swagger import openapi
from drf_swagger.utils import swagger_auto_schema, no_body from drf_swagger.utils import swagger_auto_schema, no_body
from users.serializers import UserSerializer from users.serializers import UserSerializerrr
class UserList(APIView): class UserList(APIView):
"""UserList cbv classdoc""" """UserList cbv classdoc"""
@swagger_auto_schema(responses={200: UserSerializer(many=True)}) @swagger_auto_schema(responses={200: UserSerializerrr(many=True)})
def get(self, request): def get(self, request):
queryset = User.objects.all() queryset = User.objects.all()
serializer = UserSerializer(queryset, many=True) serializer = UserSerializerrr(queryset, many=True)
return Response(serializer.data) return Response(serializer.data)
@swagger_auto_schema(operation_description="apiview post description override", request_body=openapi.Schema( @swagger_auto_schema(operation_description="apiview post description override", request_body=openapi.Schema(
@ -27,7 +27,7 @@ class UserList(APIView):
}, },
)) ))
def post(self, request): def post(self, request):
serializer = UserSerializer(request.data) serializer = UserSerializerrr(request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@ -37,15 +37,15 @@ class UserList(APIView):
pass pass
@swagger_auto_schema(method='put', request_body=UserSerializer) @swagger_auto_schema(method='put', request_body=UserSerializerrr)
@swagger_auto_schema(methods=['get'], manual_parameters=[ @swagger_auto_schema(methods=['get'], manual_parameters=[
openapi.Parameter('test', openapi.IN_QUERY, "test manual param", type=openapi.TYPE_BOOLEAN), openapi.Parameter('test', openapi.IN_QUERY, "test manual param", type=openapi.TYPE_BOOLEAN),
], responses={ ], responses={
200: openapi.Response('response description', UserSerializer), 200: openapi.Response('response description', UserSerializerrr),
}) })
@api_view(['GET', 'PUT']) @api_view(['GET', 'PUT'])
def user_detail(request, pk): def user_detail(request, pk):
"""user_detail fbv docstring""" """user_detail fbv docstring"""
user = get_object_or_404(User.objects, pk=pk) user = get_object_or_404(User.objects, pk=pk)
serializer = UserSerializer(user) serializer = UserSerializerrr(user)
return Response(serializer.data) return Response(serializer.data)

View File

@ -1,3 +1,4 @@
import copy
import json import json
import os import os
@ -27,8 +28,13 @@ def codec_yaml():
@pytest.fixture @pytest.fixture
def swagger_dict(): def swagger(generator):
swagger = generator().get_schema(None, True) return generator.get_schema(None, True)
@pytest.fixture
def swagger_dict(generator):
swagger = generator.get_schema(None, True)
json_bytes = codec_json().encode(swagger) json_bytes = codec_json().encode(swagger)
return json.loads(json_bytes.decode('utf-8')) return json.loads(json_bytes.decode('utf-8'))
@ -46,16 +52,17 @@ def validate_schema():
@pytest.fixture @pytest.fixture
def bad_settings(): def swagger_settings(settings):
from drf_swagger.app_settings import swagger_settings, SWAGGER_DEFAULTS swagger_settings = copy.deepcopy(settings.SWAGGER_SETTINGS)
bad_security = { settings.SWAGGER_SETTINGS = swagger_settings
'bad': { return swagger_settings
'bad_attribute': 'should not be accepted'
}
} @pytest.fixture
SWAGGER_DEFAULTS['SECURITY_DEFINITIONS'].update(bad_security) def redoc_settings(settings):
yield swagger_settings redoc_settings = copy.deepcopy(settings.REDOC_SETTINGS)
del SWAGGER_DEFAULTS['SECURITY_DEFINITIONS']['bad'] settings.REDOC_SETTINGS = redoc_settings
return redoc_settings
@pytest.fixture @pytest.fixture

View File

@ -141,17 +141,6 @@ paths:
- application/json - application/json
tags: tags:
- articles - articles
delete:
operationId: articles_delete
description: destroy method docstring
parameters: []
responses:
'204':
description: ''
consumes:
- application/json
tags:
- articles
patch: patch:
operationId: articles_partial_update operationId: articles_partial_update
description: partial_update description override description: partial_update description override
@ -172,6 +161,17 @@ paths:
- application/json - application/json
tags: tags:
- articles - articles
delete:
operationId: articles_delete
description: destroy method docstring
parameters: []
responses:
'204':
description: ''
consumes:
- application/json
tags:
- articles
parameters: parameters:
- name: slug - name: slug
in: path in: path
@ -197,6 +197,24 @@ paths:
operationId: articles_image_create operationId: articles_image_create
description: image method docstring description: image method docstring
parameters: parameters:
- name: what_am_i_doing
in: formData
description: test
required: true
type: string
pattern: ^69$
- name: image_styles
in: formData
description: Parameter with Items
required: true
type: array
items:
type: string
enum:
- wide
- tall
- thumb
- social
- name: upload - name: upload
in: formData in: formData
description: image serializer help_text description: image serializer help_text
@ -216,6 +234,19 @@ paths:
required: true required: true
type: string type: string
pattern: '[a-z0-9]+(?:-[a-z0-9]+)' pattern: '[a-z0-9]+(?:-[a-z0-9]+)'
/plain/:
get:
operationId: plain_list
description: ''
parameters: []
responses:
'200':
description: ''
consumes:
- application/json
tags:
- plain
parameters: []
/snippets/: /snippets/:
get: get:
operationId: snippets_list operationId: snippets_list
@ -283,17 +314,6 @@ paths:
- application/json - application/json
tags: tags:
- snippets - snippets
delete:
operationId: snippets_delete
description: delete method docstring
parameters: []
responses:
'204':
description: ''
consumes:
- application/json
tags:
- snippets
patch: patch:
operationId: snippets_partial_update operationId: snippets_partial_update
description: patch method docstring description: patch method docstring
@ -312,6 +332,17 @@ paths:
- application/json - application/json
tags: tags:
- snippets - snippets
delete:
operationId: snippets_delete
description: delete method docstring
parameters: []
responses:
'204':
description: ''
consumes:
- application/json
tags:
- snippets
parameters: parameters:
- name: id - name: id
in: path in: path
@ -329,7 +360,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: '#/definitions/User' $ref: '#/definitions/UserSerializerrr'
consumes: consumes:
- application/json - application/json
tags: tags:
@ -381,7 +412,7 @@ paths:
'200': '200':
description: response description description: response description
schema: schema:
$ref: '#/definitions/User' $ref: '#/definitions/UserSerializerrr'
consumes: consumes:
- application/json - application/json
tags: tags:
@ -394,12 +425,12 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: '#/definitions/User' $ref: '#/definitions/UserSerializerrr'
responses: responses:
'200': '200':
description: '' description: ''
schema: schema:
$ref: '#/definitions/User' $ref: '#/definitions/UserSerializerrr'
consumes: consumes:
- application/json - application/json
tags: tags:
@ -414,6 +445,8 @@ definitions:
required: required:
- title - title
- body - body
- references
- uuid
type: object type: object
properties: properties:
title: title:
@ -426,6 +459,7 @@ definitions:
description: slug model help_text description: slug model help_text
type: string type: string
format: slug format: slug
pattern: ^[-a-zA-Z0-9_]+$
date_created: date_created:
type: string type: string
format: date-time format: date-time
@ -434,6 +468,17 @@ definitions:
type: string type: string
format: date-time format: date-time
readOnly: true readOnly: true
references:
description: this is a really bad example
type: object
additionalProperties:
description: but i needed to test these 2 fields somehow
type: string
format: uri
uuid:
description: should articles have UUIDs?
type: string
format: uuid
Project: Project:
required: required:
- project_name - project_name
@ -451,6 +496,7 @@ definitions:
- code - code
- language - language
- example_projects - example_projects
- difficulty_factor
type: object type: object
properties: properties:
id: id:
@ -908,7 +954,10 @@ definitions:
- yaml - yaml
- yaml+jinja - yaml+jinja
- zephir - zephir
style: default: python
styles:
type: array
items:
type: string type: string
enum: enum:
- abap - abap
@ -940,6 +989,8 @@ definitions:
- vim - vim
- vs - vs
- xcode - xcode
default:
- friendly
lines: lines:
type: array type: array
items: items:
@ -948,7 +999,10 @@ definitions:
type: array type: array
items: items:
$ref: '#/definitions/Project' $ref: '#/definitions/Project'
User: difficulty_factor:
description: this is here just to test FloatField
type: number
UserSerializerrr:
required: required:
- username - username
- snippets - snippets
@ -961,11 +1015,24 @@ definitions:
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
only. only.
type: string type: string
email:
type: string
format: email
snippets: snippets:
type: array type: array
items: items:
type: string type: string
uniqueItems: true uniqueItems: true
last_connected_ip:
description: i'm out of ideas
type: string
format: ipv4
readOnly: true
last_connected_at:
description: really?
type: string
format: date
readOnly: true
securityDefinitions: securityDefinitions:
basic: basic:
type: basic type: basic

View File

@ -0,0 +1,50 @@
import json
import pytest
from drf_swagger import renderers
def _check_swagger_setting(swagger, setting, expected):
context = {}
renderer = renderers.SwaggerUIRenderer()
renderer.set_context(context, swagger)
swagger_settings = json.loads(context['swagger_settings'])
assert swagger_settings[setting] == expected
def _check_setting(swagger, setting, expected):
context = {}
renderer = renderers.SwaggerUIRenderer()
renderer.set_context(context, swagger)
assert context[setting] == expected
def test_validator_url(swagger_settings, swagger):
swagger_settings['VALIDATOR_URL'] = None
_check_swagger_setting(swagger, 'validatorUrl', None)
swagger_settings['VALIDATOR_URL'] = 'not none'
_check_swagger_setting(swagger, 'validatorUrl', 'not none')
with pytest.raises(KeyError):
swagger_settings['VALIDATOR_URL'] = ''
_check_swagger_setting(swagger, 'validatorUrl', None)
@pytest.mark.urls('urlconfs.login_test_urls')
def test_login_logout(swagger_settings, swagger):
swagger_settings['LOGIN_URL'] = 'login'
_check_setting(swagger, 'LOGIN_URL', '/test/login')
swagger_settings['LOGOUT_URL'] = 'logout'
_check_setting(swagger, 'LOGOUT_URL', '/test/logout')
with pytest.raises(KeyError):
swagger_settings['LOGIN_URL'] = None
_check_setting(swagger, 'LOGIN_URL', None)
with pytest.raises(KeyError):
swagger_settings['LOGOUT_URL'] = None
_check_setting(swagger, 'LOGOUT_URL', None)

View File

@ -12,7 +12,7 @@ def test_schema_generates_without_errors(generator):
def test_schema_is_valid(generator, codec_yaml): def test_schema_is_valid(generator, codec_yaml):
swagger = generator.get_schema(None, True) swagger = generator.get_schema(request=None, public=False)
codec_yaml.encode(swagger) codec_yaml.encode(swagger)
@ -42,3 +42,17 @@ def test_yaml_codec_roundtrip(codec_yaml, generator, validate_schema):
yaml_bytes = codec_yaml.encode(swagger) yaml_bytes = codec_yaml.encode(swagger)
assert b'omap' not in yaml_bytes assert b'omap' not in yaml_bytes
validate_schema(yaml.safe_load(yaml_bytes.decode('utf-8'))) validate_schema(yaml.safe_load(yaml_bytes.decode('utf-8')))
def test_basepath_only():
generator = OpenAPISchemaGenerator(
info=openapi.Info(title="Test generator", default_version="v1"),
version="v2",
url='/basepath/',
)
swagger = generator.get_schema(None, public=True)
assert 'host' not in swagger
assert 'schemes' not in swagger
assert swagger['basePath'] == '/' # base path is not implemented for now
assert swagger['info']['version'] == 'v2'

View File

@ -1,5 +1,6 @@
import json import json
import pytest
from ruamel import yaml from ruamel import yaml
@ -23,7 +24,13 @@ def test_swagger_yaml(client, validate_schema):
_validate_text_schema_view(client, validate_schema, "/swagger.yaml", yaml.safe_load) _validate_text_schema_view(client, validate_schema, "/swagger.yaml", yaml.safe_load)
def test_exception_middleware(client, bad_settings): def test_exception_middleware(client, swagger_settings):
swagger_settings['SECURITY_DEFINITIONS'] = {
'bad': {
'bad_attribute': 'should not be accepted'
}
}
response = client.get('/swagger.json') response = client.get('/swagger.json')
assert response.status_code == 500 assert response.status_code == 500
assert 'errors' in json.loads(response.content.decode('utf-8')) assert 'errors' in json.loads(response.content.decode('utf-8'))
@ -37,3 +44,10 @@ def test_swagger_ui(client, validate_schema):
def test_redoc(client, validate_schema): def test_redoc(client, validate_schema):
_validate_ui_schema_view(client, '/redoc/', 'redoc/redoc.min.js') _validate_ui_schema_view(client, '/redoc/', 'redoc/redoc.min.js')
_validate_text_schema_view(client, validate_schema, '/redoc/?format=openapi', json.loads) _validate_text_schema_view(client, validate_schema, '/redoc/?format=openapi', json.loads)
@pytest.mark.urls('urlconfs.non_public_urls')
def test_non_public(client):
response = client.get('/private/swagger.yaml')
swagger = yaml.safe_load(response.content.decode('utf-8'))
assert len(swagger['paths']) == 0

View File

View File

@ -0,0 +1,11 @@
from django.conf.urls import url
def dummy(request):
pass
urlpatterns = [
url(r'^test/login$', dummy, name='login'),
url(r'^test/logout$', dummy, name='logout'),
]

View File

@ -0,0 +1,19 @@
from django.conf.urls import url
from django.conf.urls import include
from rest_framework import permissions
import testproj.urls
from drf_swagger import openapi
from drf_swagger.views import get_schema_view
view = get_schema_view(
openapi.Info('bla', 'ble'),
public=False,
permission_classes=(permissions.AllowAny,)
)
view = view.without_ui(cache_timeout=None)
urlpatterns = [
url(r'^', include(testproj.urls)),
url(r'^private/swagger.yaml', view, name='schema-private'),
]