diff --git a/.gitignore b/.gitignore index 694c04f8b..a2afe59d1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ htmlcov/ *.swp .tox/ .idea/ -venv/ \ No newline at end of file +.vscode/ +venv/ +src/ \ No newline at end of file diff --git a/connexion/apis/abstract.py b/connexion/apis/abstract.py index c21eff128..e367da1e0 100644 --- a/connexion/apis/abstract.py +++ b/connexion/apis/abstract.py @@ -2,16 +2,20 @@ import logging import pathlib import sys +from enum import Enum import six +from ..decorators.produces import NoContent from ..exceptions import ResolverError from ..http_facts import METHODS from ..jsonifier import Jsonifier -from ..operations import make_operation +from ..lifecycle import ConnexionResponse +from ..operations import make_operation, DEFAULT_MIMETYPE from ..options import ConnexionOptions from ..resolver import Resolver from ..spec import Specification +from ..utils import is_json_mimetype MODULE_PATH = pathlib.Path(__file__).absolute().parent.parent SWAGGER_UI_URL = 'ui' @@ -238,23 +242,221 @@ def get_request(self, *args, **kwargs): @abc.abstractmethod def get_response(self, response, mimetype=None, request=None): """ - This method converts the ConnexionResponse to a user framework response. - :param response: A response to cast. + This method converts a handler response to a framework response. + This method should just retrieve response from handler then call `cls._get_response`. + It is mainly here to handle AioHttp async handler. + :param response: A response to cast (tuple, framework response, etc). :param mimetype: The response mimetype. + :type mimetype: Union[None, str] :param request: The request associated with this response (the user framework request). + """ + + @classmethod + def _get_response(cls, response, mimetype=None, extra_context=None): + """ + This method converts a handler response to a framework response. + The response can be a ConnexionResponse, an operation handler, a framework response or a tuple. + Other type than ConnexionResponse are handled by `cls._response_from_handler` + :param response: A response to cast (tuple, framework response, etc). + :param mimetype: The response mimetype. + :type mimetype: Union[None, str] + :param extra_context: dict of extra details, like url, to include in logs + :type extra_context: Union[None, dict] + """ + if extra_context is None: + extra_context = {} + logger.debug('Getting data and status code', + extra={ + 'data': response, + 'data_type': type(response), + **extra_context + }) + + if isinstance(response, ConnexionResponse): + framework_response = cls._connexion_to_framework_response(response, mimetype, extra_context) + else: + framework_response = cls._response_from_handler(response, mimetype, extra_context) + + logger.debug('Got framework response', + extra={ + 'response': framework_response, + 'response_type': type(framework_response), + **extra_context + }) + return framework_response - :type response: ConnexionResponse + @classmethod + def _response_from_handler(cls, response, mimetype, extra_context=None): + """ + Create a framework response from the operation handler data. + An operation handler can return: + - a framework response + - a body (str / binary / dict / list), a response will be created + with a status code 200 by default and empty headers. + - a tuple of (body: str, status_code: int) + - a tuple of (body: str, status_code: int, headers: dict) + :param response: A response from an operation handler. + :type response Union[Response, str, Tuple[str,], Tuple[str, int], Tuple[str, int, dict]] + :param mimetype: The response mimetype. :type mimetype: str + :param extra_context: dict of extra details, like url, to include in logs + :type extra_context: Union[None, dict] + :return A framework response. + :rtype Response """ + if cls._is_framework_response(response): + return response + + if isinstance(response, tuple): + len_response = len(response) + if len_response == 1: + data, = response + return cls._build_response(mimetype=mimetype, data=data, extra_context=extra_context) + if len_response == 2: + if isinstance(response[1], (int, Enum)): + data, status_code = response + return cls._build_response(mimetype=mimetype, data=data, status_code=status_code, extra_context=extra_context) + else: + data, headers = response + return cls._build_response(mimetype=mimetype, data=data, headers=headers, extra_context=extra_context) + elif len_response == 3: + data, status_code, headers = response + return cls._build_response(mimetype=mimetype, data=data, status_code=status_code, headers=headers, extra_context=extra_context) + else: + raise TypeError( + 'The view function did not return a valid response tuple.' + ' The tuple must have the form (body), (body, status, headers),' + ' (body, status), or (body, headers).' + ) + else: + return cls._build_response(mimetype=mimetype, data=response, extra_context=extra_context) @classmethod - @abc.abstractmethod def get_connexion_response(cls, response, mimetype=None): + """ Cast framework dependent response to ConnexionResponse used for schema validation """ + if isinstance(response, ConnexionResponse): + # If body in ConnexionResponse is not byte, it may not pass schema validation. + # In this case, rebuild response with aiohttp to have consistency + if response.body is None or isinstance(response.body, bytes): + return response + else: + response = cls._build_response( + data=response.body, + mimetype=mimetype, + content_type=response.content_type, + headers=response.headers, + status_code=response.status_code + ) + + if not cls._is_framework_response(response): + response = cls._response_from_handler(response, mimetype) + return cls._framework_to_connexion_response(response=response, mimetype=mimetype) + + @classmethod + @abc.abstractmethod + def _is_framework_response(cls, response): + """ Return True if `response` is a framework response class """ + + @classmethod + @abc.abstractmethod + def _framework_to_connexion_response(cls, response, mimetype): + """ Cast framework response class to ConnexionResponse used for schema validation """ + + @classmethod + @abc.abstractmethod + def _connexion_to_framework_response(cls, response, mimetype, extra_context=None): + """ Cast ConnexionResponse to framework response class """ + + @classmethod + @abc.abstractmethod + def _build_response(cls, data, mimetype, content_type=None, status_code=None, headers=None, extra_context=None): """ - This method converts the user framework response to a ConnexionResponse. - :param response: A response to cast. + Create a framework response from the provided arguments. + :param data: Body data. + :param content_type: The response mimetype. + :type content_type: str + :param content_type: The response status code. + :type status_code: int + :param headers: The response status code. + :type headers: Union[Iterable[Tuple[str, str]], Dict[str, str]] + :param extra_context: dict of extra details, like url, to include in logs + :type extra_context: Union[None, dict] + :return A framework response. + :rtype Response """ + @classmethod + def _prepare_body_and_status_code(cls, data, mimetype, status_code=None, extra_context=None): + if data is NoContent: + data = None + + if status_code is None: + if data is None: + status_code = 204 + mimetype = None + else: + status_code = 200 + elif hasattr(status_code, "value"): + # If we got an enum instead of an int, extract the value. + status_code = status_code.value + + if data is not None: + body, mimetype = cls._serialize_data(data, mimetype) + else: + body = data + + if extra_context is None: + extra_context = {} + logger.debug('Prepared body and status code (%d)', + status_code, + extra={ + 'body': body, + **extra_context + }) + + return body, status_code, mimetype + + @classmethod + def _serialize_data(cls, data, mimetype): + """ Serialize data (aka: _jsonify_data or _cast_body). + + The old aiohttp_api._cast_body did not jsonify when mimetype was None. + The old flask_api._jsonify_data jsonified data, or raised a TypeError if the + data could not be jsonified, even with non-JSON or None mimetypes. + + This unifies the behavior so that only JSON or None mimetypes can trigger + jsonification, and only JSON mimetypes will raise a TypeError if the data + is not jsonifiable. Non-JSON mimetypes and non-jsonifiable data with None + mimetype are stringified. + + This means that the data can serialize itself when + - mimetype is None and data is not jsonifiable, or + - mimetype is not a JSON mimetype + + mimetype, in general, is only None when the spec did not define `produces`. + """ + if not isinstance(data, bytes): + if isinstance(mimetype, str) and is_json_mimetype(mimetype): + body = cls.jsonifier.dumps(data) + elif isinstance(data, str): + body = data + elif mimetype in [None, "text/plain"]: + try: + # try as json by default + body = cls.jsonifier.dumps(data) + except TypeError: + # or let objects self-serialize + body = str(data) + logger.debug('_serialize_data mimetype={} and jsonify failed and str()'.format(mimetype)) + else: + mimetype = DEFAULT_MIMETYPE + else: + logger.debug('_serialize_data mimetype={} and str()'.format(mimetype)) + body = str(data) + else: + body = data + return body, mimetype + def json_loads(self, data): return self.jsonifier.loads(data) diff --git a/connexion/apis/aiohttp_api.py b/connexion/apis/aiohttp_api.py index 949b35427..e32f6e8bc 100644 --- a/connexion/apis/aiohttp_api.py +++ b/connexion/apis/aiohttp_api.py @@ -17,7 +17,7 @@ from connexion.jsonifier import JSONEncoder, Jsonifier from connexion.lifecycle import ConnexionRequest, ConnexionResponse from connexion.problem import problem -from connexion.utils import is_json_mimetype, yamldumper +from connexion.utils import yamldumper from werkzeug.exceptions import HTTPException as werkzeug_HTTPException @@ -303,6 +303,7 @@ def get_response(cls, response, mimetype=None, request=None): """Get response. This method is used in the lifecycle decorators + :type response: aiohttp.web.StreamResponse | (Any,) | (Any, int) | (Any, dict) | (Any, int, dict) :rtype: aiohttp.web.Response """ while asyncio.iscoroutine(response): @@ -310,66 +311,52 @@ def get_response(cls, response, mimetype=None, request=None): url = str(request.url) if request else '' - logger.debug('Getting data and status code', - extra={ - 'data': response, - 'url': url - }) - - if isinstance(response, ConnexionResponse): - response = cls._get_aiohttp_response_from_connexion(response, mimetype) - - if isinstance(response, web.StreamResponse): - logger.debug('Got stream response with status code (%d)', - response.status, extra={'url': url}) - else: - logger.debug('Got data and status code (%d)', - response.status, extra={'data': response.body, 'url': url}) - - return response + return cls._get_response(response, mimetype=mimetype, extra_context={"url": url}) @classmethod - def get_connexion_response(cls, response, mimetype=None): - response.body = cls._cast_body(response.body, mimetype) - - if isinstance(response, ConnexionResponse): - return response + def _is_framework_response(cls, response): + """ Return True if `response` is a framework response class """ + return isinstance(response, web.StreamResponse) + @classmethod + def _framework_to_connexion_response(cls, response, mimetype): + """ Cast framework response class to ConnexionResponse used for schema validation """ return ConnexionResponse( status_code=response.status, - mimetype=response.content_type, + mimetype=mimetype, content_type=response.content_type, headers=response.headers, body=response.body ) @classmethod - def _get_aiohttp_response_from_connexion(cls, response, mimetype): - content_type = response.content_type if response.content_type else \ - response.mimetype if response.mimetype else mimetype - - body = cls._cast_body(response.body, content_type) - - return web.Response( - status=response.status_code, - content_type=content_type, + def _connexion_to_framework_response(cls, response, mimetype, extra_context=None): + """ Cast ConnexionResponse to framework response class """ + return cls._build_response( + mimetype=response.mimetype or mimetype, + status_code=response.status_code, + content_type=response.content_type, headers=response.headers, - body=body + data=response.body, + extra_context=extra_context, ) @classmethod - def _cast_body(cls, body, content_type=None): - if not isinstance(body, bytes): - if content_type and is_json_mimetype(content_type): - return cls.jsonifier.dumps(body).encode() + def _build_response(cls, data, mimetype, content_type=None, headers=None, status_code=None, extra_context=None): + if cls._is_framework_response(data): + raise TypeError("Cannot return web.StreamResponse in tuple. Only raw data can be returned in tuple.") - elif isinstance(body, str): - return body.encode() + data, status_code, serialized_mimetype = cls._prepare_body_and_status_code(data=data, mimetype=mimetype, status_code=status_code, extra_context=extra_context) - else: - return str(body).encode() + if isinstance(data, str): + text = data + body = None else: - return body + text = None + body = data + + content_type = content_type or mimetype or serialized_mimetype + return web.Response(body=body, text=text, headers=headers, status=status_code, content_type=content_type) @classmethod def _set_jsonifier(cls): diff --git a/connexion/apis/flask_api.py b/connexion/apis/flask_api.py index dc79062b7..8fb94f8b9 100644 --- a/connexion/apis/flask_api.py +++ b/connexion/apis/flask_api.py @@ -5,7 +5,6 @@ import werkzeug.exceptions from connexion.apis import flask_utils from connexion.apis.abstract import AbstractAPI -from connexion.decorators.produces import NoContent from connexion.handlers import AuthErrorHandler from connexion.jsonifier import Jsonifier from connexion.lifecycle import ConnexionRequest, ConnexionResponse @@ -54,9 +53,9 @@ def add_openapi_yaml(self): self.blueprint.add_url_rule( openapi_spec_path_yaml, endpoint_name, - lambda: FlaskApi._build_flask_response( + lambda: FlaskApi._build_response( status_code=200, - content_type="text/yaml", + mimetype="text/yaml", data=yamldumper(self.specification.raw) ) ) @@ -133,115 +132,57 @@ def get_response(cls, response, mimetype=None, request=None): If the returned object is a flask.Response then it will just pass the information needed to recreate it. - :type operation_handler_result: flask.Response | (flask.Response, int) | (flask.Response, int, dict) + :type response: flask.Response | (flask.Response,) | (flask.Response, int) | (flask.Response, dict) | (flask.Response, int, dict) :rtype: ConnexionResponse """ - logger.debug('Getting data and status code', - extra={ - 'data': response, - 'data_type': type(response), - 'url': flask.request.url - }) - - if isinstance(response, ConnexionResponse): - flask_response = cls._get_flask_response_from_connexion(response, mimetype) - else: - flask_response = cls._get_flask_response(response, mimetype) - - logger.debug('Got data and status code (%d)', - flask_response.status_code, - extra={ - 'data': response, - 'datatype': type(response), - 'url': flask.request.url - }) + return cls._get_response(response, mimetype=mimetype, extra_context={"url": flask.request.url}) - return flask_response + @classmethod + def _is_framework_response(cls, response): + """ Return True if provided response is a framework type """ + return flask_utils.is_flask_response(response) @classmethod - def _get_flask_response_from_connexion(cls, response, mimetype): - data = response.body - status_code = response.status_code - mimetype = response.mimetype or mimetype - content_type = response.content_type or mimetype - headers = response.headers + def _framework_to_connexion_response(cls, response, mimetype): + """ Cast framework response class to ConnexionResponse used for schema validation """ + return ConnexionResponse( + status_code=response.status_code, + mimetype=response.mimetype, + content_type=response.content_type, + headers=response.headers, + body=response.get_data(), + ) - flask_response = cls._build_flask_response(mimetype, content_type, - headers, status_code, data) + @classmethod + def _connexion_to_framework_response(cls, response, mimetype, extra_context=None): + """ Cast ConnexionResponse to framework response class """ + flask_response = cls._build_response( + mimetype=response.mimetype or mimetype, + content_type=response.content_type, + headers=response.headers, + status_code=response.status_code, + data=response.body, + extra_context=extra_context, + ) return flask_response @classmethod - def _build_flask_response(cls, mimetype=None, content_type=None, - headers=None, status_code=None, data=None): + def _build_response(cls, mimetype, content_type=None, headers=None, status_code=None, data=None, extra_context=None): + if cls._is_framework_response(data): + return flask.current_app.make_response((data, status_code, headers)) + + data, status_code, serialized_mimetype = cls._prepare_body_and_status_code(data=data, mimetype=mimetype, status_code=status_code, extra_context=extra_context) + kwargs = { - 'mimetype': mimetype, + 'mimetype': mimetype or serialized_mimetype, 'content_type': content_type, - 'headers': headers + 'headers': headers, + 'response': data, + 'status': status_code } kwargs = {k: v for k, v in six.iteritems(kwargs) if v is not None} - flask_response = flask.current_app.response_class(**kwargs) # type: flask.Response - - if status_code is not None: - # If we got an enum instead of an int, extract the value. - if hasattr(status_code, "value"): - status_code = status_code.value - - flask_response.status_code = status_code - - if data is not None and data is not NoContent: - data = cls._jsonify_data(data, mimetype) - flask_response.set_data(data) - - elif data is NoContent: - flask_response.set_data('') - - return flask_response - - @classmethod - def _jsonify_data(cls, data, mimetype): - if (isinstance(mimetype, six.string_types) and is_json_mimetype(mimetype)) \ - or not (isinstance(data, six.binary_type) or isinstance(data, six.text_type)): - return cls.jsonifier.dumps(data) - - return data - - @classmethod - def _get_flask_response(cls, response, mimetype): - if flask_utils.is_flask_response(response): - return response - - elif isinstance(response, tuple) and flask_utils.is_flask_response(response[0]): - return flask.current_app.make_response(response) - - elif isinstance(response, tuple) and len(response) == 3: - data, status_code, headers = response - return cls._build_flask_response(mimetype, None, - headers, status_code, data) - - elif isinstance(response, tuple) and len(response) == 2: - data, status_code = response - return cls._build_flask_response(mimetype, None, None, - status_code, data) - - else: - return cls._build_flask_response(mimetype=mimetype, data=response) - - @classmethod - def get_connexion_response(cls, response, mimetype=None): - if isinstance(response, ConnexionResponse): - return response - - if not isinstance(response, flask.current_app.response_class): - response = cls.get_response(response, mimetype) - - return ConnexionResponse( - status_code=response.status_code, - mimetype=response.mimetype, - content_type=response.content_type, - headers=response.headers, - body=response.get_data(), - ) + return flask.current_app.response_class(**kwargs) # type: flask.Response @classmethod def get_request(cls, *args, **params): diff --git a/connexion/decorators/produces.py b/connexion/decorators/produces.py index b105d889a..1878fffca 100644 --- a/connexion/decorators/produces.py +++ b/connexion/decorators/produces.py @@ -12,7 +12,8 @@ class BaseSerializer(BaseDecorator): - def __init__(self, mimetype='text/plain'): + #def __init__(self, mimetype='text/plain'): + def __init__(self, mimetype='application/json'): """ :type mimetype: str """ diff --git a/connexion/jsonifier.py b/connexion/jsonifier.py index 9bc5744a5..b656bea2b 100644 --- a/connexion/jsonifier.py +++ b/connexion/jsonifier.py @@ -49,11 +49,11 @@ def loads(self, data): """ Central point where JSON deserialization happens inside Connexion. """ - if isinstance(data, six.binary_type): + if isinstance(data, bytes): data = data.decode() try: return self.json.loads(data) except Exception: - if isinstance(data, six.string_types): + if isinstance(data, str): return data diff --git a/connexion/operations/__init__.py b/connexion/operations/__init__.py index 4c44b9f38..16c0998c3 100644 --- a/connexion/operations/__init__.py +++ b/connexion/operations/__init__.py @@ -2,6 +2,7 @@ from .openapi import OpenAPIOperation # noqa from .secure import SecureOperation # noqa from .swagger2 import Swagger2Operation # noqa +from .mimetype import DEFAULT_MIMETYPE # noqa def make_operation(spec, *args, **kwargs): diff --git a/connexion/operations/abstract.py b/connexion/operations/abstract.py index f009f5991..e44d00199 100644 --- a/connexion/operations/abstract.py +++ b/connexion/operations/abstract.py @@ -4,6 +4,7 @@ import six from connexion.operations.secure import SecureOperation +from .mimetype import DEFAULT_MIMETYPE from ..decorators.metrics import UWSGIMetricsCollector from ..decorators.parameter import parameter_to_arg from ..decorators.produces import BaseSerializer, Produces @@ -13,8 +14,6 @@ logger = logging.getLogger('connexion.operations.abstract') -DEFAULT_MIMETYPE = 'application/json' - VALIDATOR_MAP = { 'parameter': ParameterValidator, 'body': RequestBodyValidator, diff --git a/connexion/operations/mimetype.py b/connexion/operations/mimetype.py new file mode 100644 index 000000000..ad0bcf137 --- /dev/null +++ b/connexion/operations/mimetype.py @@ -0,0 +1 @@ +DEFAULT_MIMETYPE = 'application/json' diff --git a/connexion/operations/secure.py b/connexion/operations/secure.py index 4d9c617e7..51cba83c0 100644 --- a/connexion/operations/secure.py +++ b/connexion/operations/secure.py @@ -1,6 +1,7 @@ import functools import logging +from .mimetype import DEFAULT_MIMETYPE from ..decorators.decorator import RequestResponseDecorator from ..decorators.security import (get_apikeyinfo_func, get_basicinfo_func, get_bearerinfo_func, @@ -11,8 +12,6 @@ logger = logging.getLogger("connexion.operations.secure") -DEFAULT_MIMETYPE = 'application/json' - class SecureOperation(object): diff --git a/tests/aiohttp/test_aiohttp_simple_api.py b/tests/aiohttp/test_aiohttp_simple_api.py index 50b204a14..dc979ec6d 100644 --- a/tests/aiohttp/test_aiohttp_simple_api.py +++ b/tests/aiohttp/test_aiohttp_simple_api.py @@ -4,7 +4,6 @@ import pytest import yaml -import aiohttp.web from conftest import TEST_FOLDER from connexion import AioHttpApp @@ -266,7 +265,9 @@ def test_response_with_non_str_and_non_json_body(aiohttp_app, aiohttp_client): '/v1.0/aiohttp_non_str_non_json_response' ) assert get_bye.status == 200 - assert (yield from get_bye.read()) == b'1234' + # \n comes from jsonifier.dumps. text/plain gets serialized as json if possible + # as that json representation should generally be more useful then python literals. + assert (yield from get_bye.read()) == b'1234\n' @asyncio.coroutine @@ -283,7 +284,7 @@ def test_validate_responses(aiohttp_app, aiohttp_client): app_client = yield from aiohttp_client(aiohttp_app.app) get_bye = yield from app_client.get('/v1.0/aiohttp_validate_responses') assert get_bye.status == 200 - assert (yield from get_bye.read()) == b'{"validate": true}' + assert (yield from get_bye.json()) == {"validate": True} @asyncio.coroutine diff --git a/tests/aiohttp/test_get_response.py b/tests/aiohttp/test_get_response.py new file mode 100644 index 000000000..34402436e --- /dev/null +++ b/tests/aiohttp/test_get_response.py @@ -0,0 +1,171 @@ +import asyncio +import json + +import pytest + +from aiohttp import web +from connexion.apis.aiohttp_api import AioHttpApi +from connexion.lifecycle import ConnexionResponse + + +@pytest.fixture(scope='module') +def api(aiohttp_api_spec_dir): + yield AioHttpApi(specification=aiohttp_api_spec_dir / 'swagger_secure.yaml') + + +@asyncio.coroutine +def test_get_response_from_aiohttp_response(api): + response = yield from api.get_response(web.Response(text='foo', status=201, headers={'X-header': 'value'})) + assert isinstance(response, web.Response) + assert response.status == 201 + assert response.body == b'foo' + assert response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value'} + + +@asyncio.coroutine +def test_get_response_from_connexion_response(api): + response = yield from api.get_response(ConnexionResponse(status_code=201, mimetype='text/plain', body='foo', headers={'X-header': 'value'})) + assert isinstance(response, web.Response) + assert response.status == 201 + assert response.body == b'foo' + assert response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value'} + + +@asyncio.coroutine +def test_get_response_from_string(api): + response = yield from api.get_response('foo') + assert isinstance(response, web.Response) + assert response.status == 200 + assert response.body == b'foo' + assert response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8'} + + +@asyncio.coroutine +def test_get_response_from_string_tuple(api): + response = yield from api.get_response(('foo',)) + assert isinstance(response, web.Response) + assert response.status == 200 + assert response.body == b'foo' + assert response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8'} + + +@asyncio.coroutine +def test_get_response_from_string_status(api): + response = yield from api.get_response(('foo', 201)) + assert isinstance(response, web.Response) + assert response.status == 201 + assert response.body == b'foo' + assert response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8'} + + +@asyncio.coroutine +def test_get_response_from_string_headers(api): + response = yield from api.get_response(('foo', {'X-header': 'value'})) + assert isinstance(response, web.Response) + assert response.status == 200 + assert response.body == b'foo' + assert response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value'} + + +@asyncio.coroutine +def test_get_response_from_string_status_headers(api): + response = yield from api.get_response(('foo', 201, {'X-header': 'value'})) + assert isinstance(response, web.Response) + assert response.status == 201 + assert response.body == b'foo' + assert response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value'} + + +@asyncio.coroutine +def test_get_response_from_tuple_error(api): + with pytest.raises(TypeError) as e: + yield from api.get_response((web.Response(text='foo', status=201, headers={'X-header': 'value'}), 200)) + assert str(e.value) == "Cannot return web.StreamResponse in tuple. Only raw data can be returned in tuple." + + +@asyncio.coroutine +def test_get_response_from_dict(api): + # mimetype=None => assume json + response = yield from api.get_response({'foo': 'bar'}) + assert isinstance(response, web.Response) + assert response.status == 200 + assert json.loads(response.body.decode()) == {"foo": "bar"} + assert response.content_type == 'application/json' + assert dict(response.headers) == {'Content-Type': 'application/json; charset=utf-8'} + + +@asyncio.coroutine +def test_get_response_from_dict_json(api): + response = yield from api.get_response({'foo': 'bar'}, mimetype='application/json') + assert isinstance(response, web.Response) + assert response.status == 200 + assert json.loads(response.body.decode()) == {"foo": "bar"} + assert response.content_type == 'application/json' + assert dict(response.headers) == {'Content-Type': 'application/json; charset=utf-8'} + + +@asyncio.coroutine +def test_get_response_no_data(api): + response = yield from api.get_response(None, mimetype='application/json') + assert isinstance(response, web.Response) + assert response.status == 204 + assert response.body is None + assert response.content_type == 'application/json' + assert dict(response.headers) == {'Content-Type': 'application/json'} + + +@asyncio.coroutine +def test_get_response_binary_json(api): + response = yield from api.get_response(b'{"foo":"bar"}', mimetype='application/json') + assert isinstance(response, web.Response) + assert response.status == 200 + assert json.loads(response.body.decode()) == {"foo": "bar"} + assert response.content_type == 'application/json' + assert dict(response.headers) == {'Content-Type': 'application/json'} + + +@asyncio.coroutine +def test_get_response_binary_no_mimetype(api): + response = yield from api.get_response(b'{"foo":"bar"}') + assert isinstance(response, web.Response) + assert response.status == 200 + assert response.body == b'{"foo":"bar"}' + assert response.content_type == 'application/octet-stream' + assert dict(response.headers) == {} + + +@asyncio.coroutine +def test_get_connexion_response_from_aiohttp_response(api): + response = api.get_connexion_response(web.Response(text='foo', status=201, headers={'X-header': 'value'})) + assert isinstance(response, ConnexionResponse) + assert response.status_code == 201 + assert response.body == b'foo' + assert response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value'} + + +@asyncio.coroutine +def test_get_connexion_response_from_connexion_response(api): + response = api.get_connexion_response(ConnexionResponse(status_code=201, content_type='text/plain', body='foo', headers={'X-header': 'value'})) + assert isinstance(response, ConnexionResponse) + assert response.status_code == 201 + assert response.body == b'foo' + assert response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value'} + + +@asyncio.coroutine +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 response.content_type == 'text/plain' + assert dict(response.headers) == {'Content-Type': 'text/plain; charset=utf-8', 'X-header': 'value'} diff --git a/tests/api/test_responses.py b/tests/api/test_responses.py index f9c43f8a2..e5a787a4c 100644 --- a/tests/api/test_responses.py +++ b/tests/api/test_responses.py @@ -90,7 +90,7 @@ def test_not_content_response(simple_app): get_no_content_response = app_client.get('/v1.0/test_no_content_response') assert get_no_content_response.status_code == 204 - assert get_no_content_response.content_length in [0, None] + assert get_no_content_response.content_length is None def test_pass_through(simple_app): @@ -311,6 +311,7 @@ def test_get_enum_response(simple_app): resp = app_client.get('/v1.0/get_enum_response') assert resp.status_code == 200 + def test_get_httpstatus_response(simple_app): app_client = simple_app.app.test_client() resp = app_client.get('/v1.0/get_httpstatus_response') diff --git a/tests/conftest.py b/tests/conftest.py index 450fa3cd2..a92131851 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -72,7 +72,7 @@ def simple_api_spec_dir(): return FIXTURES_FOLDER / 'simple' -@pytest.fixture +@pytest.fixture(scope='session') def aiohttp_api_spec_dir(): return FIXTURES_FOLDER / 'aiohttp' diff --git a/tests/fakeapi/aiohttp_handlers.py b/tests/fakeapi/aiohttp_handlers.py index 98b601431..607833032 100755 --- a/tests/fakeapi/aiohttp_handlers.py +++ b/tests/fakeapi/aiohttp_handlers.py @@ -16,50 +16,50 @@ def get_bye(name): @asyncio.coroutine def aiohttp_str_response(): - return ConnexionResponse(body='str response') + return 'str response' @asyncio.coroutine def aiohttp_non_str_non_json_response(): - return ConnexionResponse(body=1234) + return 1234 @asyncio.coroutine def aiohttp_bytes_response(): - return ConnexionResponse(body=b'bytes response') + return b'bytes response' @asyncio.coroutine def aiohttp_validate_responses(): - return ConnexionResponse(body=b'{"validate": true}') + return {"validate": True} @asyncio.coroutine def aiohttp_post_greeting(name, **kwargs): data = {'greeting': 'Hello {name}'.format(name=name)} - return ConnexionResponse(body=data) + return data @asyncio.coroutine def aiohttp_access_request_context(request_ctx): assert request_ctx is not None assert isinstance(request_ctx, aiohttp.web.Request) - return ConnexionResponse(status_code=204) + return None @asyncio.coroutine def aiohttp_query_parsing_str(query): - return ConnexionResponse(body={'query': query}) + return {'query': query} @asyncio.coroutine def aiohttp_query_parsing_array(query): - return ConnexionResponse(body={'query': query}) + return {'query': query} @asyncio.coroutine def aiohttp_query_parsing_array_multi(query): - return ConnexionResponse(body={'query': query}) + return {'query': query} USERS = [