diff --git a/django_connexion/apis/django_api.py b/django_connexion/apis/django_api.py index ec07ed4..b1766cb 100644 --- a/django_connexion/apis/django_api.py +++ b/django_connexion/apis/django_api.py @@ -7,7 +7,7 @@ import logging from connexion.apis.abstract import AbstractAPI -from connexion.lifecycle import ConnexionRequest +from connexion.lifecycle import ConnexionRequest, ConnexionResponse from connexion.utils import yamldumper from django.http import HttpResponse, JsonResponse from django.urls import path as django_path @@ -67,7 +67,6 @@ def _add_operation_internal(self, method, path, operation): Adds the operation according to the user framework in use. It will be used to register the operation on the user framework router. """ - # print('_add_operation_internal', method, path, operation) operation_id = operation.operation_id logger.debug('... Adding %s -> %s', method.upper(), operation_id, extra=vars(operation)) @@ -131,10 +130,43 @@ def _is_framework_response(cls, response): @classmethod def _framework_to_connexion_response(cls, response, mimetype): """ Cast framework response class to ConnexionResponse used for schema validation """ + content_type = response.headers['Content-Type'] + + if response.streaming: + body = response.streaming_content + if not content_type: + content_type = 'application/octet-stream' + else: + body = response.content + + if not mimetype: + try: + mimetype, _ = content_type.split(';', 1) + except ValueError: + mimetype = content_type + + return ConnexionResponse( + status_code=response.status_code, + mimetype=mimetype, + content_type=content_type, + headers=response.headers, + body=body, + ) @classmethod def _connexion_to_framework_response(cls, response, mimetype, extra_context=None): """ Cast ConnexionResponse to framework response class """ + content_type = response.content_type + if not content_type: + content_type = f'{mimetype or response.mimetype}; charset=utf-8' + + django_response = HttpResponse( + status=response.status_code, + content_type=content_type, + content=response.body, + headers=response.headers, + ) + return django_response @classmethod def _build_response(cls, data, mimetype, content_type=None, status_code=None, headers=None, @@ -153,7 +185,34 @@ def _build_response(cls, data, mimetype, content_type=None, status_code=None, he :return A framework response. :rtype Response """ - breakpoint() + if cls._is_framework_response(data): + return HttpResponse(data, status_code=status_code, headers=headers) + + data, status_code, serialized_mimetype = cls._prepare_body_and_status_code( + data=data, mimetype=mimetype, status_code=status_code, extra_context=extra_context) + + if data is None: + data = b'' + + mimetype = mimetype or serialized_mimetype + if content_type is None: + if mimetype: + content_type = mimetype + elif isinstance(data, bytes): + content_type = 'application/octet-stream' + else: + content_type = 'text/plain' + + if isinstance(data, (str, dict)): + content_type += '; charset=utf-8' + + kwargs = { + 'content_type': content_type, + 'headers': headers, + 'status': status_code + } + kwargs = {k: v for k, v in kwargs.items() if v is not None} + return HttpResponse(data, **kwargs) @property def urls(self): diff --git a/django_connexion/apis/django_utils.py b/django_connexion/apis/django_utils.py index 9261a50..c884ddb 100644 --- a/django_connexion/apis/django_utils.py +++ b/django_connexion/apis/django_utils.py @@ -3,7 +3,7 @@ import re import string -from django.http import HttpResponse +from django.http.response import HttpResponseBase PATH_PARAMETER = re.compile(r'\{([^}]*)\}') @@ -75,4 +75,4 @@ def is_django_response(obj: object) -> bool: >>> is_django_response(flask.Response()) True """ - return isinstance(obj, HttpResponse) + return isinstance(obj, HttpResponseBase) diff --git a/django_connexion/tests/api/__init__.py b/django_connexion/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_connexion/tests/api/test_get_response.py b/django_connexion/tests/api/test_get_response.py new file mode 100644 index 0000000..8fece78 --- /dev/null +++ b/django_connexion/tests/api/test_get_response.py @@ -0,0 +1,185 @@ +import json + +import pytest +from django_connexion.apis.django_api import DjangoApi +from connexion.lifecycle import ConnexionResponse + +from django.http import HttpResponse, StreamingHttpResponse + + +@pytest.fixture(scope='module') +def api(django_api_spec_dir): + yield DjangoApi(specification=django_api_spec_dir / 'swagger_secure.yaml') + + +def test_get_response_from_django_response(api): + django_response = HttpResponse( + 'foo', status=201, headers={'X-header': 'value'}, content_type='text/plain; charset=utf-8') + response = api.get_response(django_response) + assert isinstance(response, HttpResponse) + assert response.status_code == 201 + assert response.content == b'foo' + assert dict(response.headers) == { + 'Content-Type': 'text/plain; charset=utf-8', + 'X-header': 'value' + } + + +def test_get_response_from_django_stream_response(api): + django_stream_response = StreamingHttpResponse( + status=201, content_type='application/octet-stream', headers={'X-header': 'value'} + ) + response = api.get_response(django_stream_response) + assert isinstance(response, StreamingHttpResponse) + assert response.status_code == 201 + assert dict(response.headers) == { + 'Content-Type': 'application/octet-stream', 'X-header': 'value' + } + + +def test_get_response_from_connexion_response(api): + connexion_response = ConnexionResponse( + status_code=201, mimetype='text/plain', body='foo', headers={'X-header': 'value'}) + response = api.get_response(connexion_response) + assert isinstance(response, HttpResponse) + assert response.status_code == 201 + assert response.content == b'foo' + assert dict(response.headers) == { + 'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value' + } + + +def test_get_response_from_string(api): + response = api.get_response('foo') + assert isinstance(response, HttpResponse) + assert response.status_code == 200 + assert response.content == b'foo' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8'} + + +def test_get_response_from_string_tuple(api): + response = api.get_response(('foo',)) + assert isinstance(response, HttpResponse) + assert response.status_code == 200 + assert response.content == b'foo' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8'} + + +def test_get_response_from_string_status(api): + response = api.get_response(('foo', 201)) + assert isinstance(response, HttpResponse) + assert response.status_code == 201 + assert response.content == b'foo' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8'} + + +def test_get_response_from_string_headers(api): + response = api.get_response(('foo', {'X-header': 'value'})) + assert isinstance(response, HttpResponse) + assert response.status_code == 200 + assert response.content == b'foo' + assert dict(response.headers) == { + 'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value' + } + + +def test_get_response_from_string_status_headers(api): + response = api.get_response(('foo', 201, {'X-header': 'value'})) + assert isinstance(response, HttpResponse) + assert response.status_code == 201 + assert response.content == b'foo' + assert dict(response.headers) == { + 'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value' + } + + +def test_get_response_from_dict(api): + response = api.get_response({'foo': 'bar'}) + assert isinstance(response, HttpResponse) + assert response.status_code == 200 + # odd, yes. but backwards compatible. see test_response_with_non_str_and_non_json_body in + # tests/aiohttp/test_aiohttp_simple_api.py + # TODO: This should be made into JSON when aiohttp and flask serialization can be harmonized. + assert response.content == b"{'foo': 'bar'}" + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8'} + + +def test_get_response_from_dict_json(api): + response = api.get_response({'foo': 'bar'}, mimetype='application/json') + assert isinstance(response, HttpResponse) + assert response.status_code == 200 + assert json.loads(response.content.decode()) == {"foo": "bar"} + assert dict(response.headers) == {'Content-Type': 'application/json; charset=utf-8'} + + +def test_get_response_no_data(api): + response = api.get_response(None, mimetype='application/json') + assert isinstance(response, HttpResponse) + assert response.status_code == 204 + assert response.content == b'' + assert dict(response.headers) == {'Content-Type': 'application/json'} + + +def test_get_response_binary_json(api): + response = api.get_response(b'{"foo":"bar"}', mimetype='application/json') + assert isinstance(response, HttpResponse) + assert response.status_code == 200 + assert json.loads(response.content.decode()) == {"foo": "bar"} + assert dict(response.headers) == {'Content-Type': 'application/json'} + + +def test_get_response_binary_no_mimetype(api): + response = api.get_response(b'{"foo":"bar"}') + assert isinstance(response, HttpResponse) + assert response.status_code == 200 + assert response.content == b'{"foo":"bar"}' + assert dict(response.headers) == {'Content-Type': 'application/octet-stream'} + + +def test_get_connexion_response_from_django_response(api): + django_response = HttpResponse( + 'foo', status=201, content_type='text/plain; charset=utf-8', headers={'X-header': 'value'} + ) + response = api.get_connexion_response(django_response) + assert isinstance(response, ConnexionResponse) + assert response.status_code == 201 + assert response.body == b'foo' + assert dict(response.headers) == { + 'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value' + } + + +def test_get_connexion_response_from_connexion_response(api): + connexion_response = ConnexionResponse( + status_code=201, content_type='text/plain', body='foo', headers={'X-header': 'value'} + ) + response = api.get_connexion_response(connexion_response) + assert isinstance(response, ConnexionResponse) + assert response.status_code == 201 + assert response.body == b'foo' + assert dict(response.headers) == { + 'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value' + } + + +def test_get_connexion_response_from_tuple(api): + response = api.get_connexion_response(('foo', 201, {'X-header': 'value'})) + assert isinstance(response, ConnexionResponse) + assert response.status_code == 201 + assert response.body == b'foo' + assert dict(response.headers) == { + 'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value' + } + + +def test_get_connexion_response_from_django_stream_response(api): + django_stream_response = StreamingHttpResponse( + status=201, content_type='application/octet-stream', headers={'X-header': 'value'} + ) + response = api.get_connexion_response(django_stream_response) + assert isinstance(response, ConnexionResponse) + assert response.status_code == 201 + assert bytes(response.body) == b'' + assert dict(response.headers) == { + 'Content-Type': 'application/octet-stream', 'X-header': 'value' + } diff --git a/django_connexion/tests/conftest.py b/django_connexion/tests/conftest.py new file mode 100644 index 0000000..6640890 --- /dev/null +++ b/django_connexion/tests/conftest.py @@ -0,0 +1,11 @@ +import pathlib + +import pytest + +TEST_FOLDER = pathlib.Path(__file__).parent +FIXTURES_FOLDER = TEST_FOLDER / 'fixtures' + + +@pytest.fixture(scope='session') +def django_api_spec_dir(): + return FIXTURES_FOLDER / 'django' diff --git a/django_connexion/tests/fakeapi/__init__.py b/django_connexion/tests/fakeapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_connexion/tests/fakeapi/auth.py b/django_connexion/tests/fakeapi/auth.py new file mode 100644 index 0000000..4c91371 --- /dev/null +++ b/django_connexion/tests/fakeapi/auth.py @@ -0,0 +1,14 @@ +import json + + +def fake_basic_auth(username, password, required_scopes=None): + if username == password: + return {'uid': username} + return None + + +def fake_json_auth(token, required_scopes=None): + try: + return json.loads(token) + except ValueError: + return None diff --git a/django_connexion/tests/fakeapi/django_handlers.py b/django_connexion/tests/fakeapi/django_handlers.py new file mode 100644 index 0000000..7bf79de --- /dev/null +++ b/django_connexion/tests/fakeapi/django_handlers.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +import datetime +import uuid + +from connexion.lifecycle import ConnexionResponse + +from django.http import HttpRequest, HttpResponse + + +def get_bye(name): + return HttpResponse(text=f'Goodbye {name}') + + +def django_str_response(): + return 'str response' + + +def django_non_str_non_json_response(): + return 1234 + + +def django_bytes_response(): + return b'bytes response' + + +def django_validate_responses(): + return {"validate": True} + + +def django_post_greeting(name, **kwargs): + data = {'greeting': f'Hello {name}'} + return data + + +def django_echo(**kwargs): + return django.web.json_response(data=kwargs, status=200) + + +def django_access_request_context(request_ctx): + assert request_ctx is not None + assert isinstance(request_ctx, HttpRequest) + return None + + +def django_query_parsing_str(query): + return {'query': query} + + +def django_query_parsing_array(query): + return {'query': query} + + +def django_query_parsing_array_multi(query): + return {'query': query} + + +USERS = [ + {"id": 1, "name": "John Doe"}, + {"id": 2, "name": "Nick Carlson"} +] + + +def django_users_get(*args): + return django.web.json_response(data=USERS, status=200) + + +def django_users_post(user): + if "name" not in user: + return ConnexionResponse(body={"error": "name is undefined"}, + status_code=400, + content_type='application/json') + user['id'] = len(USERS) + 1 + USERS.append(user) + return django.web.json_response(data=USERS[-1], status=201) + + +def django_token_info(token_info): + return django.web.json_response(data=token_info) + + +def django_all_auth(token_info): + return django_token_info(token_info) + + +def django_async_auth(token_info): + return django_token_info(token_info) + + +def django_bearer_auth(token_info): + return django_token_info(token_info) + + +def django_async_bearer_auth(token_info): + return django_token_info(token_info) + + +def get_datetime(): + return ConnexionResponse(body={'value': datetime.datetime(2000, 1, 2, 3, 4, 5, 6)}) + + +def get_date(): + return ConnexionResponse(body={'value': datetime.date(2000, 1, 2)}) + + +def get_uuid(): + return ConnexionResponse(body={'value': uuid.UUID(hex='e7ff66d0-3ec2-4c4e-bed0-6e4723c24c51')}) diff --git a/django_connexion/tests/fixtures/django/swagger_secure.yaml b/django_connexion/tests/fixtures/django/swagger_secure.yaml new file mode 100644 index 0000000..a977d7b --- /dev/null +++ b/django_connexion/tests/fixtures/django/swagger_secure.yaml @@ -0,0 +1,40 @@ +swagger: "2.0" + +info: + title: "{{title}}" + version: "1.0" + +basePath: /v1.0 + +securityDefinitions: + oauth: + type: oauth2 + flow: password + tokenUrl: https://oauth.example/token + x-tokenInfoUrl: https://oauth.example/token_info + scopes: + myscope: can do stuff + basic: + type: basic + x-basicInfoFunc: django_connexion.tests.fakeapi.auth.fake_basic_auth + api_key: + type: apiKey + in: header + name: X-API-Key + x-apikeyInfoFunc: django_connexion.tests.fakeapi.auth.fake_json_auth + +security: + - oauth: + - myscope + - basic: [] + - api_key: [] +paths: + /all_auth: + get: + summary: Test different authentication + operationId: django_connexion.tests.fakeapi.django_handlers.django_token_info + responses: + 200: + description: greeting response + schema: + type: object