From 0d62c5f37486f739c0e214ca75d7aae4576d9bbc Mon Sep 17 00:00:00 2001 From: Pieterjan Lambein Date: Thu, 10 Oct 2019 10:22:41 +0200 Subject: [PATCH 1/4] Add django wrapper --- openapi_core/wrappers/django.py | 104 ++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 openapi_core/wrappers/django.py diff --git a/openapi_core/wrappers/django.py b/openapi_core/wrappers/django.py new file mode 100644 index 0000000..01ec7a1 --- /dev/null +++ b/openapi_core/wrappers/django.py @@ -0,0 +1,104 @@ +"""OpenAPI core wrappers module""" +import re + +from openapi_core.wrappers.base import BaseOpenAPIRequest, BaseOpenAPIResponse + +# https://docs.djangoproject.com/en/2.2/topics/http/urls/ +# +# Currently unsupported are : +# - nested arguments, e.g.: ^comments/(?:page-(?P\d+)/)?$ +# - unnamed regex groups, e.g.: ^articles/([0-9]{4})/$ +# - multiple named parameters between a single pair of slashes e.g.: -/edit/ +# +# The regex matches everything, except a "/" until "<". Than only the name is exported, after which it matches ">" and +# everything until a "/". +PATH_PARAMETER_PATTERN = r'(?:[^\/]*?)<(?:(?:.*?:))*?(\w+)>(?:[^\/]*)' + + +class DjangoOpenAPIRequest(BaseOpenAPIRequest): + path_regex = re.compile(PATH_PARAMETER_PATTERN) + + def __init__(self, request): + self.request = request + + @property + def host_url(self): + """ + :return: The host with scheme as IRI. + """ + return self.request._current_scheme_host + + @property + def path(self): + """ + :return: Requested path as unicode. + """ + return self.request.path + + @property + def method(self): + """ + :return: The request method, in lowercase. + """ + return self.request.method.lower() + + @property + def path_pattern(self): + """ + :return: The matched url pattern. + """ + return self.path_regex.sub(r'{\1}', self.request.resolver_match.route) + + @property + def parameters(self): + """ + :return: A dictionary of all parameters. + """ + return { + 'path': self.request.resolver_match.kwargs, + 'query': self.request.GET, + 'header': self.request.headers, + 'cookie': self.request.COOKIES, + } + + @property + def body(self): + """ + :return: The request body, as string. + """ + return self.request.body + + @property + def mimetype(self): + """ + :return: Like content type, but without parameters (eg, without charset, type etc.) and always lowercase. + For example if the content type is "text/HTML; charset=utf-8" the mimetype would be "text/html". + """ + return self.request.content_type + + +class DjangoOpenAPIResponse(BaseOpenAPIResponse): + + def __init__(self, response): + self.response = response + + @property + def data(self): + """ + :return: The response body, as string. + """ + return self.response.content + + @property + def status_code(self): + """ + :return: The status code as integer. + """ + return self.response.status_code + + @property + def mimetype(self): + """ + :return: Lowercase content type without charset. + """ + return self.response["Content-Type"] From 2000b1215fb50c4f81e9835e836924a109a38412 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Sat, 19 Oct 2019 23:47:58 +0100 Subject: [PATCH 2/4] Django OpenAPI request/response factories --- openapi_core/contrib/django/__init__.py | 11 ++ openapi_core/contrib/django/requests.py | 51 +++++ openapi_core/contrib/django/responses.py | 14 ++ openapi_core/validation/request/datatypes.py | 17 ++ openapi_core/validation/response/datatypes.py | 10 + openapi_core/wrappers/django.py | 104 ---------- requirements_dev.txt | 3 +- setup.cfg | 1 + tests/integration/contrib/test_django.py | 180 ++++++++++++++++++ .../integration/data/v3.0/django_factory.yaml | 19 ++ 10 files changed, 305 insertions(+), 105 deletions(-) create mode 100644 openapi_core/contrib/django/__init__.py create mode 100644 openapi_core/contrib/django/requests.py create mode 100644 openapi_core/contrib/django/responses.py delete mode 100644 openapi_core/wrappers/django.py create mode 100644 tests/integration/contrib/test_django.py create mode 100644 tests/integration/data/v3.0/django_factory.yaml diff --git a/openapi_core/contrib/django/__init__.py b/openapi_core/contrib/django/__init__.py new file mode 100644 index 0000000..dbbd8f0 --- /dev/null +++ b/openapi_core/contrib/django/__init__.py @@ -0,0 +1,11 @@ +from openapi_core.contrib.django.requests import DjangoOpenAPIRequestFactory +from openapi_core.contrib.django.responses import DjangoOpenAPIResponseFactory + +# backward compatibility +DjangoOpenAPIRequest = DjangoOpenAPIRequestFactory.create +DjangoOpenAPIResponse = DjangoOpenAPIResponseFactory.create + +__all__ = [ + 'DjangoOpenAPIRequestFactory', 'DjangoOpenAPIResponseFactory', + 'DjangoOpenAPIRequest', 'DjangoOpenAPIResponse', +] diff --git a/openapi_core/contrib/django/requests.py b/openapi_core/contrib/django/requests.py new file mode 100644 index 0000000..5ed31f6 --- /dev/null +++ b/openapi_core/contrib/django/requests.py @@ -0,0 +1,51 @@ +"""OpenAPI core contrib django requests module""" +import re + +from openapi_core.validation.request.datatypes import ( + RequestParameters, OpenAPIRequest, +) + +# https://docs.djangoproject.com/en/2.2/topics/http/urls/ +# +# Currently unsupported are : +# - nested arguments, e.g.: ^comments/(?:page-(?P\d+)/)?$ +# - unnamed regex groups, e.g.: ^articles/([0-9]{4})/$ +# - multiple named parameters between a single pair of slashes +# e.g.: -/edit/ +# +# The regex matches everything, except a "/" until "<". Than only the name +# is exported, after which it matches ">" and everything until a "/". +PATH_PARAMETER_PATTERN = r'(?:[^\/]*?)<(?:(?:.*?:))*?(\w+)>(?:[^\/]*)' + + +class DjangoOpenAPIRequestFactory(object): + + path_regex = re.compile(PATH_PARAMETER_PATTERN) + + @classmethod + def create(cls, request): + method = request.method.lower() + + if request.resolver_match is None: + path_pattern = request.path + else: + route = cls.path_regex.sub( + r'{\1}', request.resolver_match.route) + path_pattern = '/' + route + + path = request.resolver_match and request.resolver_match.kwargs or {} + parameters = RequestParameters( + path=path, + query=request.GET, + header=request.headers, + cookie=request.COOKIES, + ) + return OpenAPIRequest( + host_url=request._current_scheme_host, + path=request.path, + method=method, + path_pattern=path_pattern, + parameters=parameters, + body=request.body, + mimetype=request.content_type, + ) diff --git a/openapi_core/contrib/django/responses.py b/openapi_core/contrib/django/responses.py new file mode 100644 index 0000000..efbe69d --- /dev/null +++ b/openapi_core/contrib/django/responses.py @@ -0,0 +1,14 @@ +"""OpenAPI core contrib django responses module""" +from openapi_core.validation.response.datatypes import OpenAPIResponse + + +class DjangoOpenAPIResponseFactory(object): + + @classmethod + def create(cls, response): + mimetype = response["Content-Type"] + return OpenAPIResponse( + data=response.content, + status_code=response.status_code, + mimetype=mimetype, + ) diff --git a/openapi_core/validation/request/datatypes.py b/openapi_core/validation/request/datatypes.py index 4158798..27c9c1b 100644 --- a/openapi_core/validation/request/datatypes.py +++ b/openapi_core/validation/request/datatypes.py @@ -21,6 +21,23 @@ class RequestParameters(object): @attr.s class OpenAPIRequest(object): + """OpenAPI request dataclass. + + Attributes: + path + Requested path as string. + path_pattern + The matched url pattern. + parameters + A RequestParameters object. + body + The request body, as string. + mimetype + Like content type, but without parameters (eg, without charset, + type etc.) and always lowercase. + For example if the content type is "text/HTML; charset=utf-8" + the mimetype would be "text/html". + """ host_url = attr.ib() path = attr.ib() diff --git a/openapi_core/validation/response/datatypes.py b/openapi_core/validation/response/datatypes.py index c41f5a2..f55fc17 100644 --- a/openapi_core/validation/response/datatypes.py +++ b/openapi_core/validation/response/datatypes.py @@ -6,6 +6,16 @@ from openapi_core.validation.datatypes import BaseValidationResult @attr.s class OpenAPIResponse(object): + """OpenAPI request dataclass. + + Attributes: + data + The response body, as string. + status_code + The status code as integer. + mimetype + Lowercase content type without charset. + """ data = attr.ib() status_code = attr.ib() diff --git a/openapi_core/wrappers/django.py b/openapi_core/wrappers/django.py deleted file mode 100644 index 01ec7a1..0000000 --- a/openapi_core/wrappers/django.py +++ /dev/null @@ -1,104 +0,0 @@ -"""OpenAPI core wrappers module""" -import re - -from openapi_core.wrappers.base import BaseOpenAPIRequest, BaseOpenAPIResponse - -# https://docs.djangoproject.com/en/2.2/topics/http/urls/ -# -# Currently unsupported are : -# - nested arguments, e.g.: ^comments/(?:page-(?P\d+)/)?$ -# - unnamed regex groups, e.g.: ^articles/([0-9]{4})/$ -# - multiple named parameters between a single pair of slashes e.g.: -/edit/ -# -# The regex matches everything, except a "/" until "<". Than only the name is exported, after which it matches ">" and -# everything until a "/". -PATH_PARAMETER_PATTERN = r'(?:[^\/]*?)<(?:(?:.*?:))*?(\w+)>(?:[^\/]*)' - - -class DjangoOpenAPIRequest(BaseOpenAPIRequest): - path_regex = re.compile(PATH_PARAMETER_PATTERN) - - def __init__(self, request): - self.request = request - - @property - def host_url(self): - """ - :return: The host with scheme as IRI. - """ - return self.request._current_scheme_host - - @property - def path(self): - """ - :return: Requested path as unicode. - """ - return self.request.path - - @property - def method(self): - """ - :return: The request method, in lowercase. - """ - return self.request.method.lower() - - @property - def path_pattern(self): - """ - :return: The matched url pattern. - """ - return self.path_regex.sub(r'{\1}', self.request.resolver_match.route) - - @property - def parameters(self): - """ - :return: A dictionary of all parameters. - """ - return { - 'path': self.request.resolver_match.kwargs, - 'query': self.request.GET, - 'header': self.request.headers, - 'cookie': self.request.COOKIES, - } - - @property - def body(self): - """ - :return: The request body, as string. - """ - return self.request.body - - @property - def mimetype(self): - """ - :return: Like content type, but without parameters (eg, without charset, type etc.) and always lowercase. - For example if the content type is "text/HTML; charset=utf-8" the mimetype would be "text/html". - """ - return self.request.content_type - - -class DjangoOpenAPIResponse(BaseOpenAPIResponse): - - def __init__(self, response): - self.response = response - - @property - def data(self): - """ - :return: The response body, as string. - """ - return self.response.content - - @property - def status_code(self): - """ - :return: The status code as integer. - """ - return self.response.status_code - - @property - def mimetype(self): - """ - :return: Lowercase content type without charset. - """ - return self.response["Content-Type"] diff --git a/requirements_dev.txt b/requirements_dev.txt index 7d4afaa..b711d43 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -2,4 +2,5 @@ mock==2.0.0 pytest==3.5.0 pytest-flake8 pytest-cov==2.5.1 -flask \ No newline at end of file +flask +django==2.2.6 diff --git a/setup.cfg b/setup.cfg index 44b4320..967a29d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,7 @@ exclude = tests [options.extras_require] +django = django flask = werkzeug [tool:pytest] diff --git a/tests/integration/contrib/test_django.py b/tests/integration/contrib/test_django.py new file mode 100644 index 0000000..03672a5 --- /dev/null +++ b/tests/integration/contrib/test_django.py @@ -0,0 +1,180 @@ +from django.http import HttpResponse, HttpResponseRedirect +from django.test.client import RequestFactory +from django.urls import resolve +import pytest +from six import b + +from openapi_core.contrib.django import ( + DjangoOpenAPIRequest, DjangoOpenAPIResponse, +) +from openapi_core.shortcuts import create_spec +from openapi_core.validation.request.datatypes import RequestParameters +from openapi_core.validation.request.validators import RequestValidator +from openapi_core.validation.response.validators import ResponseValidator + + +@pytest.fixture(autouse=True, scope='module') +def django_settings(): + import django + from django.conf import settings + from django.contrib import admin + from django.urls import path + settings.configure( + ALLOWED_HOSTS=[ + 'testserver', + ], + INSTALLED_APPS=[ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.messages', + 'django.contrib.sessions', + ], + MIDDLEWARE=[ + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ] + ) + django.setup() + settings.ROOT_URLCONF = ( + path('admin/', admin.site.urls), + ) + + +class TestDjangoOpenAPIRequest(object): + + @pytest.fixture + def request_factory(self): + return RequestFactory() + + def test_no_resolver(self, request_factory): + request = request_factory.get('/admin/') + + openapi_request = DjangoOpenAPIRequest(request) + + path = {} + query = {} + headers = { + 'Cookie': '', + } + cookies = {} + assert openapi_request.parameters == RequestParameters( + path=path, + query=query, + header=headers, + cookie=cookies, + ) + assert openapi_request.host_url == request._current_scheme_host + assert openapi_request.path == request.path + assert openapi_request.method == request.method.lower() + assert openapi_request.path_pattern == request.path + assert openapi_request.body == request.body + assert openapi_request.mimetype == request.content_type + + def test_simple(self, request_factory): + request = request_factory.get('/admin/') + request.resolver_match = resolve('/admin/') + + openapi_request = DjangoOpenAPIRequest(request) + + path = {} + query = {} + headers = { + 'Cookie': '', + } + cookies = {} + assert openapi_request.parameters == RequestParameters( + path=path, + query=query, + header=headers, + cookie=cookies, + ) + assert openapi_request.host_url == request._current_scheme_host + assert openapi_request.path == request.path + assert openapi_request.method == request.method.lower() + assert openapi_request.path_pattern == request.path + assert openapi_request.body == request.body + assert openapi_request.mimetype == request.content_type + + def test_url_rule(self, request_factory): + request = request_factory.get('/admin/auth/group/1/') + request.resolver_match = resolve('/admin/auth/group/1/') + + openapi_request = DjangoOpenAPIRequest(request) + + path = { + 'object_id': '1', + } + query = {} + headers = { + 'Cookie': '', + } + cookies = {} + assert openapi_request.parameters == RequestParameters( + path=path, + query=query, + header=headers, + cookie=cookies, + ) + assert openapi_request.host_url == request._current_scheme_host + assert openapi_request.path == request.path + assert openapi_request.method == request.method.lower() + assert openapi_request.path_pattern == \ + "/admin/auth/group/{object_id}/" + assert openapi_request.body == request.body + assert openapi_request.mimetype == request.content_type + + +class TestDjangoOpenAPIResponse: + + def test_stream_response(self): + response = HttpResponse() + response.writelines(['foo\n', 'bar\n', 'baz\n']) + + openapi_response = DjangoOpenAPIResponse(response) + + assert openapi_response.data == b('foo\nbar\nbaz\n') + assert openapi_response.status_code == response.status_code + assert openapi_response.mimetype == response["Content-Type"] + + def test_redirect_response(self): + response = HttpResponseRedirect('/redirected/') + + openapi_response = DjangoOpenAPIResponse(response) + + assert openapi_response.data == response.content + assert openapi_response.status_code == response.status_code + assert openapi_response.mimetype == response["Content-Type"] + + +class TestDjangoOpenAPIValidation(object): + + @pytest.fixture + def request_factory(self): + return RequestFactory() + + @pytest.fixture + def django_spec(self, factory): + specfile = 'data/v3.0/django_factory.yaml' + return create_spec(factory.spec_from_file(specfile)) + + def test_response_validator_path_pattern( + self, django_spec, request_factory): + validator = ResponseValidator(django_spec) + request = request_factory.get('/admin/auth/group/1/') + request.resolver_match = resolve('/admin/auth/group/1/') + openapi_request = DjangoOpenAPIRequest(request) + response = HttpResponse(b('Some item')) + openapi_response = DjangoOpenAPIResponse(response) + result = validator.validate(openapi_request, openapi_response) + assert not result.errors + + def test_request_validator_path_pattern( + self, django_spec, request_factory): + validator = RequestValidator(django_spec) + request = request_factory.get('/admin/auth/group/1/') + request.resolver_match = resolve('/admin/auth/group/1/') + openapi_request = DjangoOpenAPIRequest(request) + result = validator.validate(openapi_request) + assert not result.errors diff --git a/tests/integration/data/v3.0/django_factory.yaml b/tests/integration/data/v3.0/django_factory.yaml new file mode 100644 index 0000000..3304013 --- /dev/null +++ b/tests/integration/data/v3.0/django_factory.yaml @@ -0,0 +1,19 @@ +openapi: "3.0.0" +info: + title: Basic OpenAPI specification used with test_flask.TestFlaskOpenAPIIValidation + version: "0.1" +servers: + - url: 'http://testserver' +paths: + '/admin/auth/group/{object_id}/': + parameters: + - name: object_id + in: path + required: true + description: the ID of the resource to retrieve + schema: + type: integer + get: + responses: + default: + description: Return the resource. From 2e11553f3a9dede6a85e61c3b99fe607c4ea39bd Mon Sep 17 00:00:00 2001 From: p1c2u Date: Sun, 20 Oct 2019 00:39:13 +0100 Subject: [PATCH 3/4] README update with Django usage --- README.rst | 120 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index 9a414f4..22eac86 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,4 @@ +************ openapi-core ************ @@ -15,13 +16,13 @@ openapi-core :target: https://pypi.python.org/pypi/openapi-core About -===== +##### Openapi-core is a Python library that adds client-side and server-side support for the `OpenAPI Specification v3.0.0 `__. Installation -============ +############ Recommended way (via pip): @@ -37,7 +38,7 @@ Alternatively you can download the code and install from the repository: Usage -===== +##### Firstly create your specification: @@ -47,6 +48,9 @@ Firstly create your specification: spec = create_spec(spec_dict) +Request +******* + Now you can use it to validate requests .. code-block:: python @@ -83,27 +87,10 @@ or use shortcuts for simple validation validated_params = validate_parameters(spec, request) validated_body = validate_body(spec, request) -Request object should be instance of OpenAPIRequest class. You can use FlaskOpenAPIRequest a Flask/Werkzeug request factory: +Request object should be instance of OpenAPIRequest class (See `Integrations`_). -.. code-block:: python - - from openapi_core.shortcuts import RequestValidator - from openapi_core.contrib.flask import FlaskOpenAPIRequest - - openapi_request = FlaskOpenAPIRequest(flask_request) - validator = RequestValidator(spec) - result = validator.validate(openapi_request) - -or simply specify request factory for shortcuts - -.. code-block:: python - - from openapi_core import validate_parameters, validate_body - - validated_params = validate_parameters( - spec, request, request_factory=FlaskOpenAPIRequest) - validated_body = validate_body( - spec, request, request_factory=FlaskOpenAPIRequest) +Response +******** You can also validate responses @@ -138,7 +125,85 @@ or use shortcuts for simple validation validated_data = validate_data(spec, request, response) -Response object should be instance of OpenAPIResponse class. You can use FlaskOpenAPIResponse a Flask/Werkzeug response factory: +Response object should be instance of OpenAPIResponse class (See `Integrations`_). + + +Integrations +############ + +Django +****** + +You can use DjangoOpenAPIRequest a Django request factory: + +.. code-block:: python + + from openapi_core.shortcuts import RequestValidator + from openapi_core.contrib.django import DjangoOpenAPIRequest + + openapi_request = DjangoOpenAPIRequest(django_request) + validator = RequestValidator(spec) + result = validator.validate(openapi_request) + +or simply specify request factory for shortcuts + +.. code-block:: python + + from openapi_core import validate_parameters, validate_body + + validated_params = validate_parameters( + spec, request, request_factory=DjangoOpenAPIRequest) + validated_body = validate_body( + spec, request, request_factory=DjangoOpenAPIRequest) + +You can use DjangoOpenAPIResponse as a Django response factory: + +.. code-block:: python + + from openapi_core.shortcuts import ResponseValidator + from openapi_core.contrib.django import DjangoOpenAPIResponse + + openapi_response = DjangoOpenAPIResponse(django_response) + validator = ResponseValidator(spec) + result = validator.validate(openapi_request, openapi_response) + +or simply specify response factory for shortcuts + +.. code-block:: python + + from openapi_core import validate_parameters, validate_body + + validated_data = validate_data( + spec, request, response, + request_factory=DjangoOpenAPIRequest, + response_factory=DjangoOpenAPIResponse) + +Flask +***** + +You can use FlaskOpenAPIRequest a Flask/Werkzeug request factory: + +.. code-block:: python + + from openapi_core.shortcuts import RequestValidator + from openapi_core.contrib.flask import FlaskOpenAPIRequest + + openapi_request = FlaskOpenAPIRequest(flask_request) + validator = RequestValidator(spec) + result = validator.validate(openapi_request) + +or simply specify request factory for shortcuts + +.. code-block:: python + + from openapi_core import validate_parameters, validate_body + + validated_params = validate_parameters( + spec, request, request_factory=FlaskOpenAPIRequest) + validated_body = validate_body( + spec, request, request_factory=FlaskOpenAPIRequest) + +You can use FlaskOpenAPIResponse as a Flask/Werkzeug response factory: .. code-block:: python @@ -160,7 +225,12 @@ or simply specify response factory for shortcuts request_factory=FlaskOpenAPIRequest, response_factory=FlaskOpenAPIResponse) +Pyramid +******* + +See `pyramid_openapi3 `_ project. + Related projects -================ +################ * `openapi-spec-validator `__ * `pyramid_openapi3 `__ From eb2530590d804f6446a047ae865a4e901b167f66 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Sun, 20 Oct 2019 03:00:10 +0100 Subject: [PATCH 4/4] Django 2.2 for python3 setup --- README.rst | 2 +- requirements_dev.txt | 2 +- setup.cfg | 2 +- tests/integration/contrib/test_django.py | 102 +++++++++++++---------- 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/README.rst b/README.rst index 22eac86..89c9f7d 100644 --- a/README.rst +++ b/README.rst @@ -134,7 +134,7 @@ Integrations Django ****** -You can use DjangoOpenAPIRequest a Django request factory: +For Django 2.2 you can use DjangoOpenAPIRequest a Django request factory: .. code-block:: python diff --git a/requirements_dev.txt b/requirements_dev.txt index b711d43..37815b2 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,4 +3,4 @@ pytest==3.5.0 pytest-flake8 pytest-cov==2.5.1 flask -django==2.2.6 +django==2.2.6; python_version>="3.0" diff --git a/setup.cfg b/setup.cfg index 967a29d..bd43a27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,7 +43,7 @@ exclude = tests [options.extras_require] -django = django +django = django>=2.2; python_version>="3.0" flask = werkzeug [tool:pytest] diff --git a/tests/integration/contrib/test_django.py b/tests/integration/contrib/test_django.py index 03672a5..fd57984 100644 --- a/tests/integration/contrib/test_django.py +++ b/tests/integration/contrib/test_django.py @@ -1,6 +1,5 @@ -from django.http import HttpResponse, HttpResponseRedirect -from django.test.client import RequestFactory -from django.urls import resolve +import sys + import pytest from six import b @@ -13,41 +12,58 @@ from openapi_core.validation.request.validators import RequestValidator from openapi_core.validation.response.validators import ResponseValidator -@pytest.fixture(autouse=True, scope='module') -def django_settings(): - import django - from django.conf import settings - from django.contrib import admin - from django.urls import path - settings.configure( - ALLOWED_HOSTS=[ - 'testserver', - ], - INSTALLED_APPS=[ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.messages', - 'django.contrib.sessions', - ], - MIDDLEWARE=[ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - ] - ) - django.setup() - settings.ROOT_URLCONF = ( - path('admin/', admin.site.urls), - ) +@pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python3") +class BaseTestDjango(object): + @pytest.fixture(autouse=True, scope='module') + def django_settings(self): + import django + from django.conf import settings + from django.contrib import admin + from django.urls import path -class TestDjangoOpenAPIRequest(object): + if settings.configured: + return + + settings.configure( + ALLOWED_HOSTS=[ + 'testserver', + ], + INSTALLED_APPS=[ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.messages', + 'django.contrib.sessions', + ], + MIDDLEWARE=[ + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ] + ) + django.setup() + settings.ROOT_URLCONF = ( + path('admin/', admin.site.urls), + ) @pytest.fixture def request_factory(self): + from django.test.client import RequestFactory return RequestFactory() + @pytest.fixture + def response_factory(self): + from django.http import HttpResponse + + def create(content=b(''), status_code=None): + return HttpResponse(content, status=status_code) + + return create + + +class TestDjangoOpenAPIRequest(BaseTestDjango): + def test_no_resolver(self, request_factory): request = request_factory.get('/admin/') @@ -73,6 +89,7 @@ class TestDjangoOpenAPIRequest(object): assert openapi_request.mimetype == request.content_type def test_simple(self, request_factory): + from django.urls import resolve request = request_factory.get('/admin/') request.resolver_match = resolve('/admin/') @@ -98,6 +115,7 @@ class TestDjangoOpenAPIRequest(object): assert openapi_request.mimetype == request.content_type def test_url_rule(self, request_factory): + from django.urls import resolve request = request_factory.get('/admin/auth/group/1/') request.resolver_match = resolve('/admin/auth/group/1/') @@ -126,10 +144,10 @@ class TestDjangoOpenAPIRequest(object): assert openapi_request.mimetype == request.content_type -class TestDjangoOpenAPIResponse: +class TestDjangoOpenAPIResponse(BaseTestDjango): - def test_stream_response(self): - response = HttpResponse() + def test_stream_response(self, response_factory): + response = response_factory() response.writelines(['foo\n', 'bar\n', 'baz\n']) openapi_response = DjangoOpenAPIResponse(response) @@ -138,8 +156,8 @@ class TestDjangoOpenAPIResponse: assert openapi_response.status_code == response.status_code assert openapi_response.mimetype == response["Content-Type"] - def test_redirect_response(self): - response = HttpResponseRedirect('/redirected/') + def test_redirect_response(self, response_factory): + response = response_factory('/redirected/', status_code=302) openapi_response = DjangoOpenAPIResponse(response) @@ -148,11 +166,7 @@ class TestDjangoOpenAPIResponse: assert openapi_response.mimetype == response["Content-Type"] -class TestDjangoOpenAPIValidation(object): - - @pytest.fixture - def request_factory(self): - return RequestFactory() +class TestDjangoOpenAPIValidation(BaseTestDjango): @pytest.fixture def django_spec(self, factory): @@ -160,18 +174,20 @@ class TestDjangoOpenAPIValidation(object): return create_spec(factory.spec_from_file(specfile)) def test_response_validator_path_pattern( - self, django_spec, request_factory): + self, django_spec, request_factory, response_factory): + from django.urls import resolve validator = ResponseValidator(django_spec) request = request_factory.get('/admin/auth/group/1/') request.resolver_match = resolve('/admin/auth/group/1/') openapi_request = DjangoOpenAPIRequest(request) - response = HttpResponse(b('Some item')) + response = response_factory(b('Some item')) openapi_response = DjangoOpenAPIResponse(response) result = validator.validate(openapi_request, openapi_response) assert not result.errors def test_request_validator_path_pattern( self, django_spec, request_factory): + from django.urls import resolve validator = RequestValidator(django_spec) request = request_factory.get('/admin/auth/group/1/') request.resolver_match = resolve('/admin/auth/group/1/')