From c27bee4464f3cb0b444c6a463b6a38f1c0543b36 Mon Sep 17 00:00:00 2001 From: panpann Date: Sun, 16 Dec 2018 21:38:00 +0100 Subject: [PATCH 01/15] generalize ConnexionResponse to be used as app.test_client response --- connexion/lifecycle.py | 25 +++++++++++++++++++++++++ connexion/utils.py | 30 ++++++++++++++++++++++++++++++ tests/conftest.py | 7 +++++++ tests/test_lifecycle.py | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_utils.py | 13 +++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 tests/test_lifecycle.py diff --git a/connexion/lifecycle.py b/connexion/lifecycle.py index 32fb0f26f..f7adc003c 100644 --- a/connexion/lifecycle.py +++ b/connexion/lifecycle.py @@ -1,3 +1,7 @@ +import json + +from .utils import decode, encode + class ConnexionRequest(object): def __init__(self, @@ -34,8 +38,29 @@ def __init__(self, content_type=None, body=None, headers=None): + if not isinstance(status_code, int) or not (100 <= status_code <= 505): + raise ValueError("{} is not a valid status code".format(status_code)) self.status_code = status_code self.mimetype = mimetype self.content_type = content_type self.body = body self.headers = headers or {} + + @property + def text(self): + """return a decoded version of body.""" + return decode(self.body) + + @property + def json(self): + """Return JSON decoded body. + + This method is naive, it will try to load JSON even + if the content_type is not JSON. + """ + return json.loads(self.text) + + @property + def data(self): + """return the encoded body.""" + return encode(self.body) diff --git a/connexion/utils.py b/connexion/utils.py index c1f086685..f937eacd6 100644 --- a/connexion/utils.py +++ b/connexion/utils.py @@ -39,6 +39,7 @@ def boolean(s): 'boolean': boolean, 'array': list, 'object': dict} # map of swagger types to python types +ENCODINGS = ["utf-8", "latin1", "cp1250", "ascii"] def make_type(value, _type): @@ -205,3 +206,32 @@ def iscorofunc(func): else: # pragma: 3 no cover # there's no asyncio in python 2 return False + + +def decode(_string): + """decode a string, as a fallback will ignore decoding errors. + + inspired from https://github.com/sdispater/poetry/blob/master/poetry/utils/_compat.py#L46. + """ + if isinstance(_string, six.text_type): + return _string + + for encoding in ENCODINGS: + try: + return _string.decode(encoding) + except (UnicodeEncodeError, UnicodeDecodeError): + pass + return _string.decode(ENCODINGS[0], errors="ignore") + + +def encode(_string): + """Encode a string, as a fallback will ignore encoding errors.""" + if isinstance(_string, six.binary_type): + return _string + + for encoding in ENCODINGS: + try: + return _string.encode(encoding) + except (UnicodeEncodeError, UnicodeDecodeError): + pass + return _string.encode(ENCODINGS[0], errors="ignore") diff --git a/tests/conftest.py b/tests/conftest.py index c45cb1573..d3baaccea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import sys import pytest +import six from connexion import App logging.basicConfig(level=logging.DEBUG) @@ -12,6 +13,12 @@ FIXTURES_FOLDER = TEST_FOLDER / 'fixtures' SPEC_FOLDER = TEST_FOLDER / "fakeapi" SPECS = ["swagger.yaml", "openapi.yaml"] +ENCODING_STRINGS = [ + six.b("test"), + six.u("test"), + "ą".encode("cp1250"), + "£".encode("latin1") +] class FakeResponse(object): diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py new file mode 100644 index 000000000..808a9aa92 --- /dev/null +++ b/tests/test_lifecycle.py @@ -0,0 +1,38 @@ +import json + +import pytest +import six + +from connexion.lifecycle import ConnexionResponse + + +class TestConnexionResponse(object): + + @pytest.mark.parametrize("status", [ + 200, + 302, + 403, + -1, + "test", + False, + 506 + ]) + @pytest.mark.xfail(raises=ValueError) + def test_status_code(self, status): + response = ConnexionResponse(status) + assert response.status_code == status + + @pytest.mark.parametrize("body", [ + b"test", + u"test", + ]) + def test_text_and_data(self, body): + response = ConnexionResponse(body=body) + assert isinstance(response.text, six.text_type) + assert isinstance(response.data, six.binary_type) + + @pytest.mark.parametrize("body", [ + '{"test": true}' + ]) + def test_json(self, body): + assert ConnexionResponse(body=body).json == json.loads(body) diff --git a/tests/test_utils.py b/tests/test_utils.py index 864c56f8b..191c891b1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,9 +2,12 @@ import connexion.apps import pytest +import six from connexion import utils from mock import MagicMock +from .conftest import ENCODING_STRINGS + def test_get_function_from_name(): function = utils.get_function_from_name('math.ceil') @@ -50,3 +53,13 @@ def test_boolean(): with pytest.raises(ValueError): utils.boolean(None) + + +@pytest.mark.parametrize("data", ENCODING_STRINGS) +def test_decode(data): + assert isinstance(utils.decode(data), six.text_type) + + +@pytest.mark.parametrize("data", ENCODING_STRINGS) +def test_encode(data): + assert isinstance(utils.encode(data), six.binary_type) From 7c0cdd3472406c4979f3a242ebfe3a997cf3ec99 Mon Sep 17 00:00:00 2001 From: panpann Date: Sun, 16 Dec 2018 21:41:34 +0100 Subject: [PATCH 02/15] chore: use conftest global variables instead of copied local variables --- tests/api/test_bootstrap.py | 4 +--- tests/test_json_validation.py | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/api/test_bootstrap.py b/tests/api/test_bootstrap.py index c5685f777..82a34657c 100644 --- a/tests/api/test_bootstrap.py +++ b/tests/api/test_bootstrap.py @@ -6,12 +6,10 @@ import mock import pytest -from conftest import TEST_FOLDER, build_app_from_fixture +from conftest import TEST_FOLDER, build_app_from_fixture, SPECS from connexion import App from connexion.exceptions import InvalidSpecification -SPECS = ["swagger.yaml", "openapi.yaml"] - @pytest.mark.parametrize("spec", SPECS) def test_app_with_relative_path(simple_api_spec_dir, spec): diff --git a/tests/test_json_validation.py b/tests/test_json_validation.py index 18b26149f..5b25aec43 100644 --- a/tests/test_json_validation.py +++ b/tests/test_json_validation.py @@ -3,12 +3,11 @@ from jsonschema.validators import _utils, extend import pytest -from conftest import build_app_from_fixture +from conftest import build_app_from_fixture, SPECS from connexion import App from connexion.decorators.validation import RequestBodyValidator from connexion.json_schema import Draft4RequestValidator -SPECS = ["swagger.yaml", "openapi.yaml"] @pytest.mark.parametrize("spec", SPECS) def test_validator_map(json_validation_spec_dir, spec): @@ -35,10 +34,12 @@ def __init__(self, *args, **kwargs): app.add_api(spec, validate_responses=True, validator_map=validator_map) app_client = app.app.test_client() - res = app_client.post('/v1.0/minlength', data=json.dumps({'foo': 'bar'}), content_type='application/json') # type: flask.Response + # type: flask.Response + res = app_client.post('/v1.0/minlength', data=json.dumps({'foo': 'bar'}), content_type='application/json') assert res.status_code == 200 - res = app_client.post('/v1.0/minlength', data=json.dumps({'foo': ''}), content_type='application/json') # type: flask.Response + res = app_client.post('/v1.0/minlength', data=json.dumps({'foo': ''}), + content_type='application/json') # type: flask.Response assert res.status_code == 400 @@ -47,15 +48,18 @@ def test_readonly(json_validation_spec_dir, spec): app = build_app_from_fixture(json_validation_spec_dir, spec, validate_responses=True) app_client = app.app.test_client() - res = app_client.get('/v1.0/user') # type: flask.Response + res = app_client.get('/v1.0/user') # type: flask.Response assert res.status_code == 200 assert json.loads(res.data.decode()).get('user_id') == 7 - res = app_client.post('/v1.0/user', data=json.dumps({'name': 'max', 'password': '1234'}), content_type='application/json') # type: flask.Response + # type: flask.Response + res = app_client.post( + '/v1.0/user', data=json.dumps({'name': 'max', 'password': '1234'}), content_type='application/json') assert res.status_code == 200 assert json.loads(res.data.decode()).get('user_id') == 8 - res = app_client.post('/v1.0/user', data=json.dumps({'user_id': 9, 'name': 'max'}), content_type='application/json') # type: flask.Response + # type: flask.Response + res = app_client.post('/v1.0/user', data=json.dumps({'user_id': 9, 'name': 'max'}), content_type='application/json') assert res.status_code == 400 @@ -64,14 +68,16 @@ def test_writeonly(json_validation_spec_dir, spec): app = build_app_from_fixture(json_validation_spec_dir, spec, validate_responses=True) app_client = app.app.test_client() - res = app_client.post('/v1.0/user', data=json.dumps({'name': 'max', 'password': '1234'}), content_type='application/json') # type: flask.Response + # type: flask.Response + res = app_client.post( + '/v1.0/user', data=json.dumps({'name': 'max', 'password': '1234'}), content_type='application/json') assert res.status_code == 200 assert 'password' not in json.loads(res.data.decode()) - res = app_client.get('/v1.0/user') # type: flask.Response + res = app_client.get('/v1.0/user') # type: flask.Response assert res.status_code == 200 assert 'password' not in json.loads(res.data.decode()) - res = app_client.get('/v1.0/user_with_password') # type: flask.Response + res = app_client.get('/v1.0/user_with_password') # type: flask.Response assert res.status_code == 500 assert json.loads(res.data.decode())['title'] == 'Response body does not conform to specification' From 9eaadafaeb829b86a912d348d36862270274d931 Mon Sep 17 00:00:00 2001 From: panpann Date: Sun, 16 Dec 2018 23:07:47 +0100 Subject: [PATCH 03/15] add an app generic test_client to run tests on every implemented Apps --- connexion/apps/abstract.py | 7 ++++++ connexion/apps/flask_app.py | 20 ++++++++++++++- tests/api/test_bootstrap.py | 24 ++++++++++++++++++ tests/fakeapi/hello.py | 39 +++++++++++++++++++++++++++++- tests/fixtures/simple/openapi.yaml | 26 ++++++++++++++++++++ tests/fixtures/simple/swagger.yaml | 26 ++++++++++++++++++++ 6 files changed, 140 insertions(+), 2 deletions(-) diff --git a/connexion/apps/abstract.py b/connexion/apps/abstract.py index 36d8d23b7..c701e0b0c 100644 --- a/connexion/apps/abstract.py +++ b/connexion/apps/abstract.py @@ -246,3 +246,10 @@ def __call__(self, environ, start_response): # pragma: no cover class and protect it from unwanted modification. """ return self.app(environ, start_response) + + @abc.abstractmethod + def test_client(self): + """Return a synchronous client, compatible wit flask test_client. + + An abstract is implemented in connexion.tests.AbstractClient. + """ diff --git a/connexion/apps/flask_app.py b/connexion/apps/flask_app.py index 0c27d37aa..ce5b8900e 100644 --- a/connexion/apps/flask_app.py +++ b/connexion/apps/flask_app.py @@ -11,14 +11,18 @@ from ..apis.flask_api import FlaskApi from ..exceptions import ProblemException from ..problem import problem +from ..tests import AbstractClient from .abstract import AbstractApp logger = logging.getLogger('connexion.app') class FlaskApp(AbstractApp): + + api_cls = FlaskApi + def __init__(self, import_name, server='flask', **kwargs): - super(FlaskApp, self).__init__(import_name, FlaskApi, server=server, **kwargs) + super(FlaskApp, self).__init__(import_name, self.api_cls, server=server, **kwargs) def create_app(self): app = flask.Flask(self.import_name) @@ -114,6 +118,10 @@ def run(self, port=None, server=None, debug=None, host=None, **options): # prag else: raise Exception('Server {} not recognized'.format(self.server)) + def test_client(self): + """Return a test_client which use flask's test_client.""" + return FlaskTestClient.from_app(self) + class FlaskJSONEncoder(json.JSONEncoder): def default(self, o): @@ -133,3 +141,13 @@ def default(self, o): return float(o) return json.JSONEncoder.default(self, o) + + +class FlaskTestClient(AbstractClient): + """A specific test client for Flask framework.""" + + def _request(self, method, url, **kw): + kw["method"] = method.upper() + with self.app.app.app_context(): + response = self.app.app.test_client().open(url, **kw) + return self.app.api_cls.get_connexion_response(response) diff --git a/tests/api/test_bootstrap.py b/tests/api/test_bootstrap.py index 82a34657c..8b6c41f31 100644 --- a/tests/api/test_bootstrap.py +++ b/tests/api/test_bootstrap.py @@ -9,6 +9,8 @@ from conftest import TEST_FOLDER, build_app_from_fixture, SPECS from connexion import App from connexion.exceptions import InvalidSpecification +from connexion.lifecycle import ConnexionResponse +from connexion.tests import AbstractClient @pytest.mark.parametrize("spec", SPECS) @@ -234,3 +236,25 @@ def test_handle_add_operation_error(simple_api_spec_dir): app.api_cls.add_operation = mock.MagicMock(side_effect=Exception('operation error!')) with pytest.raises(Exception): app.add_api('swagger.yaml', resolver=lambda oid: (lambda foo: 'bar')) + + +def test_test_client_type(simple_app): + client = simple_app.test_client() + assert isinstance(client, AbstractClient) + + +@pytest.mark.parametrize("method", [ + "get", + "delete", + "patch", + "post", + "put", +]) +def test_test_client_support_all_http_methods(simple_app, method): + url = "/v1.0/test_test_client_support_all_http_methods" + client = simple_app.test_client() + response = getattr(client, method)(url) + + assert isinstance(response, ConnexionResponse) + assert response.status_code == 200 + assert response.json["method"] == method diff --git a/tests/fakeapi/hello.py b/tests/fakeapi/hello.py index 2f1b318d3..c7369a8c6 100755 --- a/tests/fakeapi/hello.py +++ b/tests/fakeapi/hello.py @@ -13,6 +13,7 @@ def test_classmethod(cls): def test_method(self): return self.__class__.__name__ + class_instance = DummyClass() # noqa @@ -36,14 +37,17 @@ def post_greeting(name, **kwargs): data = {'greeting': 'Hello {name}'.format(name=name)} return data + def post_greeting3(body, **kwargs): data = {'greeting': 'Hello {name}'.format(name=body["name"])} return data + def post_greeting_url(name, remainder, **kwargs): - data = {'greeting': 'Hello {name} thanks for {remainder}'.format(name=name,remainder=remainder)} + data = {'greeting': 'Hello {name} thanks for {remainder}'.format(name=name, remainder=remainder)} return data + def post_goodday(name): data = {'greeting': 'Hello {name}'.format(name=name)} headers = {"Location": "/my/uri"} @@ -88,9 +92,11 @@ def get_bye_secure_from_connexion(req_context): def get_bye_secure_ignoring_context(name): return 'Goodbye {name} (Secure!)'.format(name=name) + def get_bye_secure_jwt(name, user, token_info): return 'Goodbye {name} (Secure: {user})'.format(name=name, user=user) + def with_problem(): return problem(type='http://www.example.com/error', title='Some Error', @@ -278,6 +284,7 @@ def test_default_integer_body(stack_version): def test_falsy_param(falsy): return falsy + def test_formdata_param3(body): return body["formData"] @@ -445,15 +452,19 @@ def test_param_sanitization3(query=None, body=None): def test_body_sanitization(body=None): return body + def test_body_sanitization_additional_properties(body): return body + def test_body_sanitization_additional_properties_defined(body): return body + def test_body_not_allowed_additional_properties(body): return body + def post_wrong_content_type(): return "NOT OK" @@ -470,6 +481,7 @@ def get_unicode_data(): def get_enum_response(): try: from enum import Enum + class HTTPStatus(Enum): OK = 200 except ImportError: @@ -510,7 +522,32 @@ def apikey_info(apikey, required_scopes=None): return {'sub': 'admin'} return None + def jwt_info(token): if token == '100': return {'sub': '100'} return None + + +def _200_method(method): + return {"method": method}, 200 + + +def get_200(): + return _200_method("get") + + +def delete_200(): + return _200_method("delete") + + +def patch_200(): + return _200_method("patch") + + +def post_200(): + return _200_method("post") + + +def put_200(): + return _200_method("put") diff --git a/tests/fixtures/simple/openapi.yaml b/tests/fixtures/simple/openapi.yaml index 907db8465..e1b05a99d 100644 --- a/tests/fixtures/simple/openapi.yaml +++ b/tests/fixtures/simple/openapi.yaml @@ -931,6 +931,32 @@ paths: schema: type: array items: {} + /test_test_client_support_all_http_methods: + get: + operationId: fakeapi.hello.get_200 + responses: + "200": + description: OK + delete: + operationId: fakeapi.hello.delete_200 + responses: + "200": + description: OK + patch: + operationId: fakeapi.hello.patch_200 + responses: + "200": + description: OK + post: + operationId: fakeapi.hello.post_200 + responses: + "200": + description: OK + put: + operationId: fakeapi.hello.put_200 + responses: + "200": + description: OK servers: - url: http://localhost:{port}/{basePath} variables: diff --git a/tests/fixtures/simple/swagger.yaml b/tests/fixtures/simple/swagger.yaml index 6591e3f12..c8067f1a0 100644 --- a/tests/fixtures/simple/swagger.yaml +++ b/tests/fixtures/simple/swagger.yaml @@ -950,6 +950,32 @@ paths: type: array items: type: integer + /test_test_client_support_all_http_methods: + get: + operationId: fakeapi.hello.get_200 + responses: + "200": + description: OK + delete: + operationId: fakeapi.hello.delete_200 + responses: + "200": + description: OK + patch: + operationId: fakeapi.hello.patch_200 + responses: + "200": + description: OK + post: + operationId: fakeapi.hello.post_200 + responses: + "200": + description: OK + put: + operationId: fakeapi.hello.put_200 + responses: + "200": + description: OK definitions: new_stack: From 10cc011dae6ea03ac31b1aea277c755e996837d8 Mon Sep 17 00:00:00 2001 From: panpann Date: Tue, 18 Dec 2018 20:58:01 +0100 Subject: [PATCH 04/15] use the connexion test_client in tests --- connexion/lifecycle.py | 12 ++++- connexion/tests.py | 56 ++++++++++++++++++++++ tests/api/test_bootstrap.py | 30 ++++++------ tests/api/test_errors.py | 2 +- tests/api/test_headers.py | 10 ++-- tests/api/test_parameters.py | 64 ++++++++++++------------- tests/api/test_responses.py | 65 ++++++++++++++------------ tests/api/test_schema.py | 20 ++++---- tests/api/test_secure_api.py | 9 ++-- tests/api/test_unordered_definition.py | 2 +- tests/test_json_validation.py | 6 +-- tests/test_lifecycle.py | 15 ++++++ 12 files changed, 188 insertions(+), 103 deletions(-) create mode 100644 connexion/tests.py diff --git a/connexion/lifecycle.py b/connexion/lifecycle.py index f7adc003c..3c262ec0d 100644 --- a/connexion/lifecycle.py +++ b/connexion/lifecycle.py @@ -42,9 +42,9 @@ def __init__(self, raise ValueError("{} is not a valid status code".format(status_code)) self.status_code = status_code self.mimetype = mimetype - self.content_type = content_type self.body = body self.headers = headers or {} + self.content_type = content_type or self.headers.get("Content-Type") @property def text(self): @@ -57,6 +57,7 @@ def json(self): This method is naive, it will try to load JSON even if the content_type is not JSON. + It will raise in case of a non JSON string """ return json.loads(self.text) @@ -64,3 +65,12 @@ def json(self): def data(self): """return the encoded body.""" return encode(self.body) + + @property + def content_length(self): + """return the content length. + + If Content-Length is not present in headers, + get the size of encoded body. + """ + return int(self.headers.get("Content-Length", len(self.data))) diff --git a/connexion/tests.py b/connexion/tests.py new file mode 100644 index 000000000..11cc03652 --- /dev/null +++ b/connexion/tests.py @@ -0,0 +1,56 @@ +import abc +import functools + +import six + + +class AbstractClientMeta(abc.ABCMeta): + pass + + +@six.add_metaclass(AbstractClientMeta) +class AbstractClient(object): + """A test client to run tests on top of different web frameworks.""" + http_verbs = [ + "head", + "get", + "delete", + "options", + "patch", + "post", + "put", + "trace", + ] + + def __init__(self, app): + self.app = app + + @classmethod + def from_app(cls, app): + return cls(app) + + @abc.abstractmethod + def _request( + self, + method, + url, + headers=None, + data=None, + json=None, + content_type=None, + ): + # type: (...) -> connexion.lifecycle.ConnexionResponse + """Make request and return a ConnexionResponse instance. + + For now, the expected client has to be compatible with flask's test_client. + TODO: use the arguments from ConnexionRequest if one day Apis have a method + to translate ConnexionRequest to web framework request. + see https://github.com/pallets/werkzeug/blob/master/werkzeug/test.py#L222 + for the request client. + """ + + def __getattr__(self, key): + """Call _request with method bound if key is an HTTP method.""" + if key in self.http_verbs: + return functools.partial(self._request, key) + raise AttributeError(key) diff --git a/tests/api/test_bootstrap.py b/tests/api/test_bootstrap.py index 8b6c41f31..3334d0f2a 100644 --- a/tests/api/test_bootstrap.py +++ b/tests/api/test_bootstrap.py @@ -21,7 +21,7 @@ def test_app_with_relative_path(simple_api_spec_dir, spec): debug=True) app.add_api(spec) - app_client = app.app.test_client() + app_client = app.test_client() get_bye = app_client.get('/v1.0/bye/jsantos') # type: flask.Response assert get_bye.status_code == 200 assert get_bye.data == b'Goodbye jsantos' @@ -47,7 +47,7 @@ def test_app_with_different_server_option(simple_api_spec_dir, spec): debug=True) app.add_api(spec) - app_client = app.app.test_client() + app_client = app.test_client() get_bye = app_client.get('/v1.0/bye/jsantos') # type: flask.Response assert get_bye.status_code == 200 assert get_bye.data == b'Goodbye jsantos' @@ -61,12 +61,12 @@ def test_app_with_different_uri_parser(simple_api_spec_dir): debug=True) app.add_api('swagger.yaml') - app_client = app.app.test_client() + app_client = app.test_client() resp = app_client.get( '/v1.0/test_array_csv_query_param?items=a,b,c&items=d,e,f' ) # type: flask.Response assert resp.status_code == 200 - j = json.loads(resp.get_data(as_text=True)) + j = resp.json assert j == ['a', 'b', 'c'] @@ -77,13 +77,13 @@ def test_no_swagger_ui(simple_api_spec_dir, spec): options=options, debug=True) app.add_api(spec) - app_client = app.app.test_client() + app_client = app.test_client() swagger_ui = app_client.get('/v1.0/ui/') # type: flask.Response assert swagger_ui.status_code == 404 app2 = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True) app2.add_api(spec, options={"swagger_ui": False}) - app2_client = app2.app.test_client() + app2_client = app2.test_client() swagger_ui2 = app2_client.get('/v1.0/ui/') # type: flask.Response assert swagger_ui2.status_code == 404 @@ -93,7 +93,7 @@ def test_swagger_json_app(simple_api_spec_dir, spec): """ Verify the spec json file is returned for default setting passed to app. """ app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True) app.add_api(spec) - app_client = app.app.test_client() + app_client = app.test_client() url = '/v1.0/{spec}' url = url.format(spec=spec.replace("yaml", "json")) spec_json = app_client.get(url) # type: flask.Response @@ -108,7 +108,7 @@ def test_no_swagger_json_app(simple_api_spec_dir, spec): options=options, debug=True) app.add_api(spec) - app_client = app.app.test_client() + app_client = app.test_client() url = '/v1.0/{spec}' url = url.format(spec=spec.replace("yaml", "json")) spec_json = app_client.get(url) # type: flask.Response @@ -132,7 +132,7 @@ def test_dict_as_yaml_path(simple_api_spec_dir, spec): app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True) app.add_api(specification) - app_client = app.app.test_client() + app_client = app.test_client() url = '/v1.0/{spec}'.format(spec=spec.replace("yaml", "json")) swagger_json = app_client.get(url) # type: flask.Response assert swagger_json.status_code == 200 @@ -144,7 +144,7 @@ def test_swagger_json_api(simple_api_spec_dir, spec): app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True) app.add_api(spec) - app_client = app.app.test_client() + app_client = app.test_client() url = '/v1.0/{spec}'.format(spec=spec.replace("yaml", "json")) swagger_json = app_client.get(url) # type: flask.Response assert swagger_json.status_code == 200 @@ -156,14 +156,14 @@ def test_no_swagger_json_api(simple_api_spec_dir, spec): app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True) app.add_api(spec, options={"serve_spec": False}) - app_client = app.app.test_client() + app_client = app.test_client() url = '/v1.0/{spec}'.format(spec=spec.replace("yaml", "json")) swagger_json = app_client.get(url) # type: flask.Response assert swagger_json.status_code == 404 def test_swagger_json_content_type(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() spec = simple_app._spec_file url = '/v1.0/{spec}'.format(spec=spec.replace("yaml", "json")) response = app_client.get(url) # type: flask.Response @@ -179,7 +179,7 @@ def route1(): def route2(): return 'single 2' - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() simple_app.add_url_rule('/single1', 'single1', route1, methods=['GET']) @@ -197,13 +197,13 @@ def route2(): def test_resolve_method(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/resolver-test/method') # type: flask.Response assert resp.data == b'"DummyClass"\n' def test_resolve_classmethod(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/resolver-test/classmethod') # type: flask.Response assert resp.data.decode('utf-8', 'replace') == '"DummyClass"\n' diff --git a/tests/api/test_errors.py b/tests/api/test_errors.py index d635c2da3..a9f32cc71 100644 --- a/tests/api/test_errors.py +++ b/tests/api/test_errors.py @@ -8,7 +8,7 @@ def fix_data(data): def test_errors(problem_app): - app_client = problem_app.app.test_client() + app_client = problem_app.test_client() greeting404 = app_client.get('/v1.0/greeting') # type: flask.Response assert greeting404.content_type == 'application/problem+json' diff --git a/tests/api/test_headers.py b/tests/api/test_headers.py index 0b6c88c75..c66e24d37 100644 --- a/tests/api/test_headers.py +++ b/tests/api/test_headers.py @@ -2,7 +2,7 @@ def test_headers_jsonifier(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() response = app_client.post('/v1.0/goodday/dan', data={}) # type: flask.Response assert response.status_code == 201 @@ -10,7 +10,7 @@ def test_headers_jsonifier(simple_app): def test_headers_produces(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() response = app_client.post('/v1.0/goodevening/dan', data={}) # type: flask.Response assert response.status_code == 201 @@ -18,7 +18,7 @@ def test_headers_produces(simple_app): def test_header_not_returned(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() response = app_client.post('/v1.0/goodday/noheader', data={}) # type: flask.Response assert response.status_code == 500 # view_func has not returned what was promised in spec @@ -31,14 +31,14 @@ def test_header_not_returned(simple_app): def test_no_content_response_have_headers(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-204-with-headers') assert resp.status_code == 204 assert 'X-Something' in resp.headers def test_no_content_object_and_have_headers(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-204-with-headers-nocontent-obj') assert resp.status_code == 204 assert 'X-Something' in resp.headers diff --git a/tests/api/test_parameters.py b/tests/api/test_parameters.py index acbc157a8..d88f2def6 100644 --- a/tests/api/test_parameters.py +++ b/tests/api/test_parameters.py @@ -6,7 +6,7 @@ def test_parameter_validation(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() url = '/v1.0/test_parameter_validation' @@ -29,7 +29,7 @@ def test_parameter_validation(simple_app): def test_required_query_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() url = '/v1.0/test_required_query_param' response = app_client.get(url) @@ -40,7 +40,7 @@ def test_required_query_param(simple_app): def test_array_query_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() headers = {'Content-type': 'application/json'} url = '/v1.0/test_array_csv_query_param' response = app_client.get(url, headers=headers) @@ -73,7 +73,7 @@ def test_array_query_param(simple_app): def test_array_form_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() headers = {'Content-type': 'application/x-www-form-urlencoded'} url = '/v1.0/test_array_csv_form_param' response = app_client.post(url, headers=headers) @@ -100,7 +100,7 @@ def test_array_form_param(simple_app): def test_extra_query_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() headers = {'Content-type': 'application/json'} url = '/v1.0/test_parameter_validation?extra_parameter=true' resp = app_client.get(url, headers=headers) @@ -108,7 +108,7 @@ def test_extra_query_param(simple_app): def test_strict_extra_query_param(strict_app): - app_client = strict_app.app.test_client() + app_client = strict_app.test_client() headers = {'Content-type': 'application/json'} url = '/v1.0/test_parameter_validation?extra_parameter=true' resp = app_client.get(url, headers=headers) @@ -118,7 +118,7 @@ def test_strict_extra_query_param(strict_app): def test_path_parameter_someint(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-int-path/123') # type: flask.Response assert resp.data.decode('utf-8', 'replace') == '"int"\n' @@ -128,7 +128,7 @@ def test_path_parameter_someint(simple_app): def test_path_parameter_somefloat(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-float-path/123.45') # type: flask.Response assert resp.data.decode('utf-8' , 'replace') == '"float"\n' @@ -138,7 +138,7 @@ def test_path_parameter_somefloat(simple_app): def test_default_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-default-query-parameter') assert resp.status_code == 200 response = json.loads(resp.data.decode('utf-8', 'replace')) @@ -146,7 +146,7 @@ def test_default_param(simple_app): def test_falsy_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-falsy-param', query_string={'falsy': 0}) assert resp.status_code == 200 response = json.loads(resp.data.decode('utf-8', 'replace')) @@ -159,7 +159,7 @@ def test_falsy_param(simple_app): def test_formdata_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/test-formData-param', data={'formData': 'test'}) assert resp.status_code == 200 @@ -168,7 +168,7 @@ def test_formdata_param(simple_app): def test_formdata_bad_request(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/test-formData-param') assert resp.status_code == 400 response = json.loads(resp.data.decode('utf-8', 'replace')) @@ -179,14 +179,14 @@ def test_formdata_bad_request(simple_app): def test_formdata_missing_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/test-formData-missing-param', data={'missing_formData': 'test'}) assert resp.status_code == 200 def test_formdata_extra_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/test-formData-param', data={'formData': 'test', 'extra_formData': 'test'}) @@ -194,7 +194,7 @@ def test_formdata_extra_param(simple_app): def test_strict_formdata_extra_param(strict_app): - app_client = strict_app.app.test_client() + app_client = strict_app.test_client() resp = app_client.post('/v1.0/test-formData-param', data={'formData': 'test', 'extra_formData': 'test'}) @@ -204,7 +204,7 @@ def test_strict_formdata_extra_param(strict_app): def test_formdata_file_upload(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/test-formData-file-upload', data={'formData': (BytesIO(b'file contents'), 'filename.txt')}) assert resp.status_code == 200 @@ -213,7 +213,7 @@ def test_formdata_file_upload(simple_app): def test_formdata_file_upload_bad_request(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/test-formData-file-upload') assert resp.status_code == 400 response = json.loads(resp.data.decode('utf-8', 'replace')) @@ -224,14 +224,14 @@ def test_formdata_file_upload_bad_request(simple_app): def test_formdata_file_upload_missing_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/test-formData-file-upload-missing-param', data={'missing_formData': (BytesIO(b'file contents'), 'example.txt')}) assert resp.status_code == 200 def test_body_not_allowed_additional_properties(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() body = { 'body1': 'bodyString', 'additional_property': 'test1'} resp = app_client.post( '/v1.0/body-not-allowed-additional-properties', @@ -243,7 +243,7 @@ def test_body_not_allowed_additional_properties(simple_app): assert 'Additional properties are not allowed' in response['detail'] def test_bool_as_default_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-bool-param') assert resp.status_code == 200 @@ -254,7 +254,7 @@ def test_bool_as_default_param(simple_app): def test_bool_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-bool-param', query_string={'thruthiness': True}) assert resp.status_code == 200 response = json.loads(resp.data.decode('utf-8', 'replace')) @@ -267,25 +267,25 @@ def test_bool_param(simple_app): def test_bool_array_param(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-bool-array-param?thruthiness=true,true,true') assert resp.status_code == 200 response = json.loads(resp.data.decode('utf-8', 'replace')) assert response is True - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-bool-array-param?thruthiness=true,true,false') assert resp.status_code == 200 response = json.loads(resp.data.decode('utf-8', 'replace')) assert response is False - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-bool-array-param') assert resp.status_code == 200 def test_required_param_miss_config(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-required-param') assert resp.status_code == 400 @@ -298,7 +298,7 @@ def test_required_param_miss_config(simple_app): def test_parameters_defined_in_path_level(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/parameters-in-root-path?title=nice-get') assert resp.status_code == 200 assert json.loads(resp.data.decode('utf-8', 'replace')) == ["nice-get"] @@ -308,7 +308,7 @@ def test_parameters_defined_in_path_level(simple_app): def test_array_in_path(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-array-in-path/one_item') assert json.loads(resp.data.decode('utf-8', 'replace')) == ["one_item"] @@ -317,7 +317,7 @@ def test_array_in_path(simple_app): def test_nullable_parameter(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/nullable-parameters?time_start=null') assert json.loads(resp.data.decode('utf-8', 'replace')) == 'it was None' @@ -344,7 +344,7 @@ def test_nullable_parameter(simple_app): def test_args_kwargs(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/query-params-as-kwargs') assert resp.status_code == 200 assert json.loads(resp.data.decode('utf-8', 'replace')) == {} @@ -355,7 +355,7 @@ def test_args_kwargs(simple_app): def test_param_sanitization(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/param-sanitization') assert resp.status_code == 200 assert json.loads(resp.data.decode('utf-8', 'replace')) == {} @@ -393,7 +393,7 @@ def test_param_sanitization(simple_app): assert json.loads(resp.data.decode('utf-8', 'replace')) == body def test_parameters_snake_case(snake_case_app): - app_client = snake_case_app.app.test_client() + app_client = snake_case_app.test_client() headers = {'Content-type': 'application/json'} resp = app_client.post('/v1.0/test-post-path-snake/123', headers=headers, data=json.dumps({"a": "test"})) assert resp.status_code == 200 @@ -415,7 +415,7 @@ def test_parameters_snake_case(snake_case_app): def test_get_unicode_request(simple_app): """Regression test for Python 2 UnicodeEncodeError bug during parameter parsing.""" - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/get_unicode_request?price=%C2%A319.99') # £19.99 assert resp.status_code == 200 assert json.loads(resp.data.decode('utf-8'))['price'] == '£19.99' diff --git a/tests/api/test_responses.py b/tests/api/test_responses.py index 093e0d3af..5d4faa9d0 100644 --- a/tests/api/test_responses.py +++ b/tests/api/test_responses.py @@ -9,7 +9,7 @@ def test_app(simple_app): assert simple_app.port == 5001 - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() # by default the Swagger UI is enabled swagger_ui = app_client.get('/v1.0/ui/') # type: flask.Response @@ -44,14 +44,14 @@ def test_app(simple_app): def test_produce_decorator(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() get_bye = app_client.get('/v1.0/bye/jsantos') # type: flask.Response assert get_bye.content_type == 'text/plain; charset=utf-8' def test_returning_flask_response_tuple(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() result = app_client.get('/v1.0/flask_response_tuple') # type: flask.Response assert result.status_code == 201 @@ -61,7 +61,7 @@ def test_returning_flask_response_tuple(simple_app): def test_jsonifier(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() post_greeting = app_client.post('/v1.0/greeting/jsantos', data={}) # type: flask.Response assert post_greeting.status_code == 200 @@ -86,22 +86,23 @@ def test_jsonifier(simple_app): def test_not_content_response(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() get_no_content_response = app_client.get('/v1.0/test_no_content_response') assert get_no_content_response.status_code == 204 + print("popo", get_no_content_response.headers) assert get_no_content_response.content_length == 0 def test_pass_through(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() response = app_client.get('/v1.0/multimime', data={}) # type: flask.Response assert response.status_code == 200 def test_empty(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() response = app_client.get('/v1.0/empty') # type: flask.Response assert response.status_code == 204 @@ -109,19 +110,19 @@ def test_empty(simple_app): def test_redirect_endpoint(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-redirect-endpoint') assert resp.status_code == 302 def test_redirect_response_endpoint(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/test-redirect-response-endpoint') assert resp.status_code == 302 def test_default_object_body(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/test-default-object-body') assert resp.status_code == 200 response = json.loads(resp.data.decode('utf-8', 'replace')) @@ -152,7 +153,7 @@ def default(self, o): def test_content_type_not_json(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/blob-response') assert resp.status_code == 200 @@ -164,7 +165,7 @@ def test_content_type_not_json(simple_app): def test_maybe_blob_or_json(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/binary-response') assert resp.status_code == 200 @@ -177,7 +178,7 @@ def test_maybe_blob_or_json(simple_app): def test_bad_operations(bad_operations_app): # Bad operationIds in bad_operations_app should result in 501 - app_client = bad_operations_app.app.test_client() + app_client = bad_operations_app.test_client() resp = app_client.get('/v1.0/welcome') assert resp.status_code == 501 @@ -190,20 +191,20 @@ def test_bad_operations(bad_operations_app): def test_text_request(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/text-request', data='text') assert resp.status_code == 200 def test_operation_handler_returns_flask_object(invalid_resp_allowed_app): - app_client = invalid_resp_allowed_app.app.test_client() + app_client = invalid_resp_allowed_app.test_client() resp = app_client.get('/v1.0/get_non_conforming_response') assert resp.status_code == 200 def test_post_wrong_content_type(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.post('/v1.0/post_wrong_content_type', content_type="application/xml", data=json.dumps({"some": "data"}) @@ -227,19 +228,22 @@ def test_post_wrong_content_type(simple_app): # (https://github.com/pallets/werkzeug/issues/1159) # so that content-type is added to every request, we remove it here manually for our test # this test can be removed once the werkzeug issue is addressed - builder = EnvironBuilder(path='/v1.0/post_wrong_content_type', method='POST', - data=json.dumps({"some": "data"})) - try: - environ = builder.get_environ() - finally: - builder.close() - environ.pop('CONTENT_TYPE') + # builder = EnvironBuilder(path='/v1.0/post_wrong_content_type', method='POST', + # data=json.dumps({"some": "data"})) + # try: + # environ = builder.get_environ() + # finally: + # builder.close() + # environ.pop('CONTENT_TYPE') # we cannot just call app_client.open() since app_client is a flask.testing.FlaskClient # which overrides werkzeug.test.Client.open() but does not allow passing an environment # directly - resp = Client.open(app_client, environ) - assert resp.status_code == 415 + # resp = Client.open(app_client, environ) + + # building then environ is not necessary anymore, seems to work without it. + resp = app_client.post('/v1.0/post_wrong_content_type', data=json.dumps({"some": "data"})) + assert resp.status_code == 415 resp = app_client.post('/v1.0/post_wrong_content_type', content_type="application/json", @@ -250,25 +254,26 @@ def test_post_wrong_content_type(simple_app): def test_get_unicode_response(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/get_unicode_response') actualJson = {u'currency': u'\xa3', u'key': u'leena'} - assert json.loads(resp.data.decode('utf-8','replace')) == actualJson + assert json.loads(resp.data.decode('utf-8', 'replace')) == actualJson def test_get_enum_response(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() 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() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/get_httpstatus_response') assert resp.status_code == 200 def test_get_bad_default_response(simple_app): - app_client = simple_app.app.test_client() + app_client = simple_app.test_client() resp = app_client.get('/v1.0/get_bad_default_response/200') assert resp.status_code == 200 diff --git a/tests/api/test_schema.py b/tests/api/test_schema.py index 272ad0b1a..b1d3a4ea5 100644 --- a/tests/api/test_schema.py +++ b/tests/api/test_schema.py @@ -2,7 +2,7 @@ def test_schema(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() headers = {'Content-type': 'application/json'} empty_request = app_client.post('/v1.0/test_schema', headers=headers, data=json.dumps({})) # type: flask.Response @@ -42,7 +42,7 @@ def test_schema(schema_app): def test_schema_response(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() request = app_client.get('/v1.0/test_schema/response/object/valid', headers={}, data=None) # type: flask.Response assert request.status_code == 200 @@ -81,7 +81,7 @@ def test_schema_response(schema_app): def test_schema_in_query(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() headers = {'Content-type': 'application/json'} good_request = app_client.post('/v1.0/test_schema_in_query', headers=headers, @@ -93,7 +93,7 @@ def test_schema_in_query(schema_app): def test_schema_list(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() headers = {'Content-type': 'application/json'} wrong_type = app_client.post('/v1.0/test_schema_list', headers=headers, data=json.dumps(42)) # type: flask.Response @@ -113,7 +113,7 @@ def test_schema_list(schema_app): def test_schema_map(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() headers = {'Content-type': 'application/json'} valid_object = { @@ -150,7 +150,7 @@ def test_schema_map(schema_app): def test_schema_recursive(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() headers = {'Content-type': 'application/json'} valid_object = { @@ -189,7 +189,7 @@ def test_schema_recursive(schema_app): def test_schema_format(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() headers = {'Content-type': 'application/json'} wrong_type = app_client.post('/v1.0/test_schema_format', headers=headers, @@ -202,7 +202,7 @@ def test_schema_format(schema_app): def test_schema_array(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() headers = {'Content-type': 'application/json'} array_request = app_client.post('/v1.0/schema_array', headers=headers, @@ -214,7 +214,7 @@ def test_schema_array(schema_app): def test_schema_int(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() headers = {'Content-type': 'application/json'} array_request = app_client.post('/v1.0/schema_int', headers=headers, @@ -226,6 +226,6 @@ def test_schema_int(schema_app): def test_global_response_definitions(schema_app): - app_client = schema_app.app.test_client() + app_client = schema_app.test_client() resp = app_client.get('/v1.0/define_global_response') assert json.loads(resp.data.decode('utf-8', 'replace')) == ['general', 'list'] diff --git a/tests/api/test_secure_api.py b/tests/api/test_secure_api.py index e21501cfc..408a4e5db 100644 --- a/tests/api/test_secure_api.py +++ b/tests/api/test_secure_api.py @@ -1,10 +1,8 @@ import json -from connexion import FlaskApp - def test_security_over_nonexistent_endpoints(oauth_requests, secure_api_app): - app_client = secure_api_app.app.test_client() + app_client = secure_api_app.test_client() headers = {"Authorization": "Bearer 300"} get_inexistent_endpoint = app_client.get('/v1.0/does-not-exist-invalid-token', headers=headers) # type: flask.Response @@ -32,7 +30,7 @@ def test_security_over_nonexistent_endpoints(oauth_requests, secure_api_app): def test_security(oauth_requests, secure_endpoint_app): - app_client = secure_endpoint_app.app.test_client() + app_client = secure_endpoint_app.test_client() get_bye_no_auth = app_client.get('/v1.0/byesecure/jsantos') # type: flask.Response assert get_bye_no_auth.status_code == 401 @@ -88,9 +86,10 @@ def test_security(oauth_requests, secure_endpoint_app): get_bye_from_connexion = app_client.get('/v1.0/byesecure-jwt/test-user', headers=headers) # type: flask.Response assert get_bye_from_connexion.data == b'Goodbye test-user (Secure: 100)' + def test_checking_that_client_token_has_all_necessary_scopes( oauth_requests, secure_endpoint_app): - app_client = secure_endpoint_app.app.test_client() + app_client = secure_endpoint_app.test_client() # has only one of the required scopes headers = {"Authorization": "Bearer has_myscope"} diff --git a/tests/api/test_unordered_definition.py b/tests/api/test_unordered_definition.py index fbe93eab9..4b35ede4f 100644 --- a/tests/api/test_unordered_definition.py +++ b/tests/api/test_unordered_definition.py @@ -2,7 +2,7 @@ def test_app(unordered_definition_app): - app_client = unordered_definition_app.app.test_client() + app_client = unordered_definition_app.test_client() response = app_client.get('/v1.0/unordered-params/1?first=first&second=2') # type: flask.Response assert response.status_code == 400 response_data = json.loads(response.data.decode('utf-8', 'replace')) diff --git a/tests/test_json_validation.py b/tests/test_json_validation.py index 5b25aec43..16f56d61f 100644 --- a/tests/test_json_validation.py +++ b/tests/test_json_validation.py @@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs): app = App(__name__, specification_dir=json_validation_spec_dir) app.add_api(spec, validate_responses=True, validator_map=validator_map) - app_client = app.app.test_client() + app_client = app.test_client() # type: flask.Response res = app_client.post('/v1.0/minlength', data=json.dumps({'foo': 'bar'}), content_type='application/json') @@ -46,7 +46,7 @@ def __init__(self, *args, **kwargs): @pytest.mark.parametrize("spec", SPECS) def test_readonly(json_validation_spec_dir, spec): app = build_app_from_fixture(json_validation_spec_dir, spec, validate_responses=True) - app_client = app.app.test_client() + app_client = app.test_client() res = app_client.get('/v1.0/user') # type: flask.Response assert res.status_code == 200 @@ -66,7 +66,7 @@ def test_readonly(json_validation_spec_dir, spec): @pytest.mark.parametrize("spec", SPECS) def test_writeonly(json_validation_spec_dir, spec): app = build_app_from_fixture(json_validation_spec_dir, spec, validate_responses=True) - app_client = app.app.test_client() + app_client = app.test_client() # type: flask.Response res = app_client.post( diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 808a9aa92..80d327099 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -36,3 +36,18 @@ def test_text_and_data(self, body): ]) def test_json(self, body): assert ConnexionResponse(body=body).json == json.loads(body) + + @pytest.mark.parametrize("body,expected", [ + ("test", 4), + ("", 0), + # Always count body as bytes, some unicode characters representation + # is composed of multiple bytes + ("€", 3) + ]) + def test_content_length(self, body, expected): + assert ConnexionResponse(body=body).content_length == expected + response_with_headers = ConnexionResponse( + body=body, + headers={"Content-Length": str(len(body.encode("UTF-8")))}, + ) + assert response_with_headers.content_length == expected From ef7c9ca722046e02308bdf256bdce2277e5da6c4 Mon Sep 17 00:00:00 2001 From: panpann Date: Wed, 19 Dec 2018 20:56:31 +0100 Subject: [PATCH 05/15] feat: test_client() for aiohttp_app --- connexion/apps/aiohttp_app.py | 56 ++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/connexion/apps/aiohttp_app.py b/connexion/apps/aiohttp_app.py index b5acffbd4..da4cf4967 100644 --- a/connexion/apps/aiohttp_app.py +++ b/connexion/apps/aiohttp_app.py @@ -1,12 +1,17 @@ +import asyncio import logging import os.path import pkgutil import sys from aiohttp import web +from aiohttp.test_utils import TestClient, TestServer from ..apis.aiohttp_api import AioHttpApi from ..exceptions import ConnexionException +from ..lifecycle import ConnexionResponse +from ..tests import AbstractClient +from ..utils import is_json_mimetype from .abstract import AbstractApp logger = logging.getLogger('connexion.aiohttp_app') @@ -14,8 +19,10 @@ class AioHttpApp(AbstractApp): + api_cls = AioHttpApi + def __init__(self, import_name, only_one_api=False, **kwargs): - super(AioHttpApp, self).__init__(import_name, AioHttpApi, server='aiohttp', **kwargs) + super(AioHttpApp, self).__init__(import_name, self.api_cls, server='aiohttp', **kwargs) self._only_one_api = only_one_api self._api_added = False @@ -96,3 +103,50 @@ def run(self, port=None, server=None, debug=None, host=None, **options): web.run_app(self.app, port=self.port, host=self.host, access_log=access_log) else: raise Exception('Server {} not recognized'.format(self.server)) + + def test_client(self): + """Return a flask's test_client compatible.""" + return AioHttpClient.from_app(self) + + +class AioHttpClient(AbstractClient): + """ A specific test client for aiohttp framework.""" + + def _request( + self, + method, + url, + **kwargs + ): + # code inspired from https://github.com/aio-libs/aiohttp/blob/v3.4.4/aiohttp/pytest_plugin.py#L286 + # set the loop in the app, + # and use only this one to avoid loop conflicts + self.app.app._set_loop(None) + loop = self.app.app.loop + client = TestClient(TestServer(self.app.app, loop=loop), loop=loop) + loop.run_until_complete(client.start_server()) + + @asyncio.coroutine + def _async_request(): + nonlocal client + content_type = kwargs.get("content_type") + if content_type: + headers = kwargs.setdefault("headers", {}) + if "Content-Type" not in headers: + headers["Content-Type"] = content_type + kwargs["params"] = kwargs.get("query_string") + res = yield from client.request(method.upper(), url, **kwargs) + body = yield from res.read() + print(res.content_type) + if is_json_mimetype(res.content_type): + body = body + b"\n" + print("test_client", body) + return ConnexionResponse( + status_code=res.status, + headers=res.headers, + body=body + ) + + response = loop.run_until_complete(_async_request()) + loop.run_until_complete(client.close()) + return response From 58e3fbb8e3d74b7b48567e2861f8f94e6c4ddc56 Mon Sep 17 00:00:00 2001 From: panpann Date: Wed, 19 Dec 2018 23:12:20 +0100 Subject: [PATCH 06/15] common handler to validate operation handler response Documented in AbstractApi._response_from_handler. This method is implemented by subclasses. Body and tuples are validated by connexion.operations.validation.validate_operation_output fixes #578 --- connexion/apis/abstract.py | 52 +++++++++++++++++++++++- connexion/apis/aiohttp_api.py | 61 ++++++++++++++++++----------- connexion/apis/flask_api.py | 39 +++++++----------- connexion/apps/aiohttp_app.py | 4 -- connexion/decorators/validation.py | 1 - connexion/operations/validation.py | 26 ++++++++++++ connexion/utils.py | 18 +++++++++ tests/api/test_abstract.py | 35 +++++++++++++++++ tests/api/test_aiohttp_api.py | 61 +++++++++++++++++++++++++++++ tests/api/test_responses.py | 1 - tests/fakeapi/hello.py | 1 - tests/operations/test_validation.py | 21 ++++++++++ tests/test_utils.py | 32 ++++++++++++++- 13 files changed, 294 insertions(+), 58 deletions(-) create mode 100644 connexion/operations/validation.py create mode 100644 tests/api/test_abstract.py create mode 100644 tests/api/test_aiohttp_api.py create mode 100644 tests/operations/test_validation.py diff --git a/connexion/apis/abstract.py b/connexion/apis/abstract.py index bfac29d50..1bcd9f54e 100644 --- a/connexion/apis/abstract.py +++ b/connexion/apis/abstract.py @@ -10,7 +10,7 @@ from ..options import ConnexionOptions from ..resolver import Resolver from ..spec import Specification -from ..utils import Jsonifier +from ..utils import Jsonifier, decode, is_json_mimetype, is_string MODULE_PATH = pathlib.Path(__file__).absolute().parent.parent SWAGGER_UI_URL = 'ui' @@ -236,7 +236,11 @@ 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. + This method converts a response to a user framework response. + + The response can be a ConnexionResponse or an operation handler + result. This type of result is handled by `cls._response_from_handler` + :param response: A response to cast. :param mimetype: The response mimetype. :param request: The request associated with this response (the user framework request). @@ -245,11 +249,55 @@ def get_response(self, response, mimetype=None, request=None): :type mimetype: str """ + @classmethod + @abc.abstractmethod + def _response_from_handler(cls, response, mimetype): + # type: Union[Response, str, Tuple[str, int], Tuple[str, int, dict]] -> Response + """ + 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. + :param mimetype: The response mimetype. + + :return A framwork response. + """ + + @classmethod + def encode_body(cls, body, mimetype=None): + """Helper to appropriatly encode the body. + + If JSON mimetype is given, serialize the body. + If an object other than a string is given, serialize it. + It is needed when user is expecting to play with a JSON + API without specifying the mimetype. + In other case return the body. + + For strings the return value is bytes. + """ + json_encode = mimetype and is_json_mimetype(mimetype) + if json_encode or not is_string(body): + if isinstance(body, six.binary_type): + body = decode(body) + body = cls.jsonifier.dumps(body) + if isinstance(body, six.text_type): + body = body.encode("UTF-8") + return body + @classmethod @abc.abstractmethod def get_connexion_response(cls, response, mimetype=None): """ This method converts the user framework response to a ConnexionResponse. + + It is used after the user returned a response, + to give it to response validators. :param response: A response to cast. """ diff --git a/connexion/apis/aiohttp_api.py b/connexion/apis/aiohttp_api.py index 393a35641..6567bcc21 100644 --- a/connexion/apis/aiohttp_api.py +++ b/connexion/apis/aiohttp_api.py @@ -4,15 +4,20 @@ from urllib.parse import parse_qs import jinja2 +import six import aiohttp_jinja2 from aiohttp import web +from aiohttp.payload import Payload from aiohttp.web_exceptions import HTTPNotFound from connexion.apis.abstract import AbstractAPI from connexion.exceptions import OAuthProblem from connexion.handlers import AuthErrorHandler from connexion.lifecycle import ConnexionRequest, ConnexionResponse -from connexion.utils import Jsonifier, is_json_mimetype +from connexion.operations.validation import validate_operation_output +from connexion.utils import ( + Jsonifier +) try: import ujson as json @@ -205,8 +210,7 @@ def get_response(cls, response, mimetype=None, request=None): 'url': url }) - if isinstance(response, ConnexionResponse): - response = cls._get_aiohttp_response_from_connexion(response, mimetype) + response = cls._get_response(response, mimetype) if isinstance(response, web.StreamResponse): logger.debug('Got stream response with status code (%d)', @@ -218,12 +222,39 @@ def get_response(cls, response, mimetype=None, request=None): return response @classmethod - def get_connexion_response(cls, response, mimetype=None): - response.body = cls._cast_body(response.body, mimetype) + def _get_response(cls, response, mimetype): + """Synchronous part of get_response method.""" + if isinstance(response, ConnexionResponse): + response = cls._get_aiohttp_response_from_connexion(response, mimetype) + else: + response = cls._response_from_handler(response, mimetype) + return response + + @classmethod + def _response_from_handler(cls, response, mimetype=None): + """Return a web.Response from operation handler.""" + if isinstance(response, web.StreamResponse): + """this check handles web.StreamResponse and web.Response.""" + return response + elif ( + isinstance(response, tuple) and + isinstance(response[0], web.StreamResponse) + ): + return response[0] + body, status, headers = validate_operation_output(response) + body = cls.encode_body(body, mimetype) + return web.Response( + body=body, status=status, headers=headers + ) + + @classmethod + def get_connexion_response(cls, response, mimetype=None): if isinstance(response, ConnexionResponse): return response + response = cls._get_response(response, mimetype) + return ConnexionResponse( status_code=response.status, mimetype=response.content_type, @@ -234,10 +265,8 @@ def get_connexion_response(cls, response, mimetype=None): @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) + content_type = response.content_type or response.mimetype or mimetype + body = cls.encode_body(response.body, content_type) return web.Response( status=response.status_code, @@ -246,20 +275,6 @@ def _get_aiohttp_response_from_connexion(cls, response, mimetype): body=body ) - @classmethod - def _cast_body(cls, body, content_type=None): - if not isinstance(body, bytes): - if content_type and is_json_mimetype(content_type): - return json.dumps(body).encode() - - elif isinstance(body, str): - return body.encode() - - else: - return str(body).encode() - else: - return body - @classmethod def _set_jsonifier(cls): cls.jsonifier = Jsonifier(json) diff --git a/connexion/apis/flask_api.py b/connexion/apis/flask_api.py index 8671b45c8..ec9cbe5ac 100644 --- a/connexion/apis/flask_api.py +++ b/connexion/apis/flask_api.py @@ -10,7 +10,8 @@ from connexion.decorators.produces import NoContent from connexion.handlers import AuthErrorHandler from connexion.lifecycle import ConnexionRequest, ConnexionResponse -from connexion.utils import Jsonifier, is_json_mimetype +from connexion.operations.validation import validate_operation_output +from connexion.utils import Jsonifier logger = logging.getLogger('connexion.apis.flask_api') @@ -115,7 +116,7 @@ def get_response(cls, response, mimetype=None, request=None): if isinstance(response, ConnexionResponse): flask_response = cls._get_flask_response_from_connexion(response, mimetype) else: - flask_response = cls._get_flask_response(response, mimetype) + flask_response = cls._response_from_handler(response, mimetype) logger.debug('Got data and status code (%d)', flask_response.status_code, @@ -159,7 +160,7 @@ def _build_flask_response(cls, mimetype=None, content_type=None, flask_response.status_code = status_code if data is not None and data is not NoContent: - data = cls._jsonify_data(data, mimetype) + data = cls.encode_body(data, mimetype) flask_response.set_data(data) elif data is NoContent: @@ -168,33 +169,23 @@ def _build_flask_response(cls, mimetype=None, content_type=None, 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) + def _response_from_handler(cls, response, mimetype): + """Create a framework response from the operation handler data. - return data - - @classmethod - def _get_flask_response(cls, response, mimetype): + Handle all cases describe in `AbstractApi.get_response`. + """ 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) + body, status_code, headers = validate_operation_output(response) + return cls._build_flask_response( + mimetype=mimetype, + headers=headers, + status_code=status_code, + data=body + ) @classmethod def get_connexion_response(cls, response, mimetype=None): diff --git a/connexion/apps/aiohttp_app.py b/connexion/apps/aiohttp_app.py index da4cf4967..d8bcc82c7 100644 --- a/connexion/apps/aiohttp_app.py +++ b/connexion/apps/aiohttp_app.py @@ -137,10 +137,6 @@ def _async_request(): kwargs["params"] = kwargs.get("query_string") res = yield from client.request(method.upper(), url, **kwargs) body = yield from res.read() - print(res.content_type) - if is_json_mimetype(res.content_type): - body = body + b"\n" - print("test_client", body) return ConnexionResponse( status_code=res.status, headers=res.headers, diff --git a/connexion/decorators/validation.py b/connexion/decorators/validation.py index 1a38642ef..c180afcb9 100644 --- a/connexion/decorators/validation.py +++ b/connexion/decorators/validation.py @@ -161,7 +161,6 @@ def wrapper(request): data[k] = coerce_type(param_defn, data[k], 'requestBody', k) except TypeValidationError as e: errs += [str(e)] - print(errs) if errs: return problem(400, 'Bad Request', errs) diff --git a/connexion/operations/validation.py b/connexion/operations/validation.py new file mode 100644 index 000000000..c473d5fbf --- /dev/null +++ b/connexion/operations/validation.py @@ -0,0 +1,26 @@ +import six + +from connexion.utils import normalize_tuple + +BODY_TYPES = (six.text_type, six.binary_type, dict, list) + + +def validate_operation_output(response): + """Will validate the format returned by a handler.""" + if isinstance(response, BODY_TYPES): + response = (response, ) + body, status, headers = normalize_tuple(response, 3) + if not isinstance(body, BODY_TYPES): + raise ValueError( + "first returned value has to be {}, got {}".format( + BODY_TYPES, type(body) + ) + ) + status = status or 200 + if headers is not None and not isinstance(headers, dict): + raise ValueError( + "Type of 3rd return value is dict, got {}".format( + type(headers) + ) + ) + return body, status, headers diff --git a/connexion/utils.py b/connexion/utils.py index f937eacd6..3cf89ca37 100644 --- a/connexion/utils.py +++ b/connexion/utils.py @@ -154,6 +154,10 @@ def is_null(value): return False +def is_string(value): + return isinstance(value, (six.text_type, six.binary_type)) + + class Jsonifier(object): def __init__(self, json_): self.json = json_ @@ -235,3 +239,17 @@ def encode(_string): except (UnicodeEncodeError, UnicodeDecodeError): pass return _string.encode(ENCODINGS[0], errors="ignore") + + +def normalize_tuple(obj, length): + """return a tuple of length `length`.""" + if not isinstance(obj, tuple): + raise ValueError("expected tuple, got {}".format(type(obj))) + elif len(obj) > length: + raise ValueError("length expected is smaller than obj size.") + + new_obj = [o for o in obj] + while len(new_obj) < length: + new_obj.append(None) + + return tuple(new_obj) diff --git a/tests/api/test_abstract.py b/tests/api/test_abstract.py new file mode 100644 index 000000000..7baeb8182 --- /dev/null +++ b/tests/api/test_abstract.py @@ -0,0 +1,35 @@ +import json + +import pytest + +from connexion.apis import AbstractAPI +from connexion.utils import Jsonifier + + +@pytest.mark.parametrize("body,mimetype,expected", [ + (None, None, b"null\n"), + # is returned as it is with mimetype text/plain + ("test", "text/plain", b"test"), + (b"test", "text/plain", b"test"), + ("test", "application/json", b'"test"\n'), + (b"test", "application/json", b'"test"\n'), +]) +def test_encode_body(body, mimetype, expected): + """Test the body encoding. + + Jsonifier adds a `\n` after the serialized string. + """ + assert AbstractAPI.encode_body(body, mimetype) == expected + + +@pytest.mark.parametrize("body", [ + None, + {"test": 1}, + ["test"], +]) +def test_encode_body_objects(body): + encoded = AbstractAPI.encode_body(body, mimetype="application/json") + serde = Jsonifier(json) + assert encoded == serde.dumps(body).encode("UTF-8") + assert encoded.decode("UTF-8")[-1] == "\n" + assert serde.loads(encoded) == body diff --git a/tests/api/test_aiohttp_api.py b/tests/api/test_aiohttp_api.py new file mode 100644 index 000000000..f4f11308e --- /dev/null +++ b/tests/api/test_aiohttp_api.py @@ -0,0 +1,61 @@ +import pytest + +from aiohttp.web import Response, StreamResponse +from connexion.apis.aiohttp_api import AioHttpApi +from connexion.lifecycle import ConnexionResponse + + +@pytest.mark.parametrize("handler_response", [ + Response(), + (Response(), ), + StreamResponse() +]) +def test__response_from_handler_aiohttp_response(handler_response): + response = AioHttpApi._response_from_handler(handler_response) + assert isinstance(response, StreamResponse) + + +@pytest.mark.parametrize("body", [ + "test", + b"test" +]) +def test__response_from_handler_tuple_1(body): + response = AioHttpApi._response_from_handler((body, )) + assert isinstance(response, Response) + assert response.body == b"test" + assert response.status == 200 + + +@pytest.mark.parametrize("response_handler", [ + (("test", 200)), + (("test", 404)) +]) +def test__response_from_handler_tuple_2(response_handler): + response = AioHttpApi._response_from_handler(response_handler) + assert isinstance(response, Response) + assert response.body == b"test" + assert response.status == response_handler[1] + + +@pytest.mark.parametrize("response_handler", [ + (("test", 404, {"x-test": "true"})) +]) +def test__response_from_handler_tuple_3(response_handler): + response = AioHttpApi._response_from_handler(response_handler) + assert isinstance(response, Response) + assert response.body == b"test" + assert response.headers.get("x-test") == "true" + + +@pytest.mark.parametrize("response", [ + Response(), + (("test",)), + (("test", 200)), + (("test", 200, {"Location": "http://test.com"})), + ConnexionResponse() +]) +def test_get_connexion_response(response): + assert isinstance( + AioHttpApi.get_connexion_response(response), + ConnexionResponse + ) diff --git a/tests/api/test_responses.py b/tests/api/test_responses.py index 5d4faa9d0..902055787 100644 --- a/tests/api/test_responses.py +++ b/tests/api/test_responses.py @@ -90,7 +90,6 @@ 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 - print("popo", get_no_content_response.headers) assert get_no_content_response.content_length == 0 diff --git a/tests/fakeapi/hello.py b/tests/fakeapi/hello.py index c7369a8c6..382b808d7 100755 --- a/tests/fakeapi/hello.py +++ b/tests/fakeapi/hello.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import flask from flask import jsonify, redirect from connexion import NoContent, ProblemException, context, problem diff --git a/tests/operations/test_validation.py b/tests/operations/test_validation.py new file mode 100644 index 000000000..b3b52b935 --- /dev/null +++ b/tests/operations/test_validation.py @@ -0,0 +1,21 @@ +import pytest + +from connexion.operations.validation import ( + BODY_TYPES, + validate_operation_output +) + + +@pytest.mark.parametrize("output", [ + ("test", 200, {}), + "test", + b"test", + ("test",), + {}, + [] +]) +def test_validate_operation_output(output): + body, status, headers = validate_operation_output(output) + assert isinstance(body, BODY_TYPES) + assert isinstance(status, int) or status is None + assert isinstance(headers, dict) or headers is None diff --git a/tests/test_utils.py b/tests/test_utils.py index 191c891b1..15098550a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,11 +3,10 @@ import connexion.apps import pytest import six +from conftest import ENCODING_STRINGS from connexion import utils from mock import MagicMock -from .conftest import ENCODING_STRINGS - def test_get_function_from_name(): function = utils.get_function_from_name('math.ceil') @@ -63,3 +62,32 @@ def test_decode(data): @pytest.mark.parametrize("data", ENCODING_STRINGS) def test_encode(data): assert isinstance(utils.encode(data), six.binary_type) + + +@pytest.mark.parametrize("obj,length,expected", [ + [(1,), 3, (1, None, None)], + [(1, 2), 2, (1, 2)] +]) +def test_normalize_tuple(obj, length, expected): + assert utils.normalize_tuple(obj, length) == expected + + +@pytest.mark.parametrize("obj,length", [ + ["1", 1], + [(1, 2), 1] +]) +@pytest.mark.xfail(raises=ValueError) +def test_normalize_tuple_wrong_data(obj, length): + utils.normalize_tuple(obj, length) + + +@pytest.mark.parametrize("obj,expected", [ + ("test", True), + (b"test", True), + ({}, False), + (None, False), + ([], False), + (1, False) +]) +def test_is_string(obj, expected): + assert utils.is_string(obj) is expected From 8eda5e3156f5764f79e3d7b0d0347e293051732c Mon Sep 17 00:00:00 2001 From: panpann Date: Thu, 20 Dec 2018 00:24:23 +0100 Subject: [PATCH 07/15] fix: missing aiohttp error handler AioHttpApp is handling errors like FlaskApp's common_error_handler --- connexion/apps/aiohttp_app.py | 33 ++++++++++++++++++++++++++++++--- connexion/http_facts.py | 26 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/connexion/apps/aiohttp_app.py b/connexion/apps/aiohttp_app.py index d8bcc82c7..7830d9247 100644 --- a/connexion/apps/aiohttp_app.py +++ b/connexion/apps/aiohttp_app.py @@ -8,15 +8,41 @@ from aiohttp.test_utils import TestClient, TestServer from ..apis.aiohttp_api import AioHttpApi -from ..exceptions import ConnexionException +from ..exceptions import ConnexionException, ProblemException +from ..http_facts import HTTP_ERRORS from ..lifecycle import ConnexionResponse +from ..problem import problem from ..tests import AbstractClient -from ..utils import is_json_mimetype from .abstract import AbstractApp logger = logging.getLogger('connexion.aiohttp_app') +@web.middleware +@asyncio.coroutine +def error_middleware(request, handler): + error_response = None + try: + return (yield from handler(request)) + except web.HTTPException as ex: + if ex.status_code >= 400: + error_response = problem( + title=HTTP_ERRORS[ex.status_code]["title"], + detail=HTTP_ERRORS[ex.status_code]["detail"], + status=ex.status_code + ) + except ProblemException as exception: + error_response = exception.to_problem() + except Exception: + error_response = problem( + title=HTTP_ERRORS[500]["title"], + detail=HTTP_ERRORS[500]["detail"], + status=500 + ) + if error_response: + return (yield from AioHttpApi.get_response(error_response)) + + class AioHttpApp(AbstractApp): api_cls = AioHttpApi @@ -27,7 +53,7 @@ def __init__(self, import_name, only_one_api=False, **kwargs): self._api_added = False def create_app(self): - return web.Application(debug=self.debug) + return web.Application(debug=self.debug, middlewares=[error_middleware]) def get_root_path(self): mod = sys.modules.get(self.import_name) @@ -46,6 +72,7 @@ def get_root_path(self): return os.path.dirname(os.path.abspath(filepath)) def set_errors_handlers(self): + """error handlers are set in `create_app`.""" pass def add_api(self, specification, **kwargs): diff --git a/connexion/http_facts.py b/connexion/http_facts.py index be7756c04..197e439bf 100644 --- a/connexion/http_facts.py +++ b/connexion/http_facts.py @@ -2,3 +2,29 @@ 'application/x-www-form-urlencoded', 'multipart/form-data' ] + +HTTP_ERRORS = { + 404: { + "title": "Not Found", + "detail": 'The requested URL was not found on the server. ' + 'If you entered the URL manually please check your spelling and try again.' + }, + 405: { + "title": "Method Not Allowed", + "detail": "The method is not allowed for the requested URL." + }, + 500: { + "title": "Internal Server Error", + "detail": 'The server encountered an internal error and was unable to complete your request. ' + 'Either the server is overloaded or there is an error in the application.' + }, + 400: { + "title": "Bad Request", + "detail": "The browser (or proxy) sent a request that this server could not understand." + }, + 403: { + "title": "Forbidden", + "detail": "You don't have the permission to access the requested resource. " + "It is either read-protected or not readable by the server." + } +} From 1d40dda7072754046eed16de0daf7953532c7239 Mon Sep 17 00:00:00 2001 From: panpann Date: Thu, 20 Dec 2018 08:56:59 +0100 Subject: [PATCH 08/15] fix: isort imports --- connexion/apis/aiohttp_api.py | 6 +- tests/api/test_abstract.py | 1 - tests/api/test_aiohttp_api.py | 1 - tests/api/test_bootstrap.py | 4 +- tests/conftest.py | 108 ++++++++++++------ ...st_validation.py => test_op_validation.py} | 7 +- tests/test_json_validation.py | 2 +- tests/test_lifecycle.py | 2 +- tests/test_utils.py | 3 +- 9 files changed, 79 insertions(+), 55 deletions(-) rename tests/operations/{test_validation.py => test_op_validation.py} (75%) diff --git a/connexion/apis/aiohttp_api.py b/connexion/apis/aiohttp_api.py index 6567bcc21..e4bdd672a 100644 --- a/connexion/apis/aiohttp_api.py +++ b/connexion/apis/aiohttp_api.py @@ -4,20 +4,16 @@ from urllib.parse import parse_qs import jinja2 -import six import aiohttp_jinja2 from aiohttp import web -from aiohttp.payload import Payload from aiohttp.web_exceptions import HTTPNotFound from connexion.apis.abstract import AbstractAPI from connexion.exceptions import OAuthProblem from connexion.handlers import AuthErrorHandler from connexion.lifecycle import ConnexionRequest, ConnexionResponse from connexion.operations.validation import validate_operation_output -from connexion.utils import ( - Jsonifier -) +from connexion.utils import Jsonifier try: import ujson as json diff --git a/tests/api/test_abstract.py b/tests/api/test_abstract.py index 7baeb8182..127925b6a 100644 --- a/tests/api/test_abstract.py +++ b/tests/api/test_abstract.py @@ -1,7 +1,6 @@ import json import pytest - from connexion.apis import AbstractAPI from connexion.utils import Jsonifier diff --git a/tests/api/test_aiohttp_api.py b/tests/api/test_aiohttp_api.py index f4f11308e..df7efb9c8 100644 --- a/tests/api/test_aiohttp_api.py +++ b/tests/api/test_aiohttp_api.py @@ -1,5 +1,4 @@ import pytest - from aiohttp.web import Response, StreamResponse from connexion.apis.aiohttp_api import AioHttpApi from connexion.lifecycle import ConnexionResponse diff --git a/tests/api/test_bootstrap.py b/tests/api/test_bootstrap.py index 3334d0f2a..67e7820b2 100644 --- a/tests/api/test_bootstrap.py +++ b/tests/api/test_bootstrap.py @@ -1,12 +1,10 @@ -import json - import jinja2 import yaml from openapi_spec_validator.loaders import ExtendedSafeLoader import mock import pytest -from conftest import TEST_FOLDER, build_app_from_fixture, SPECS +from conftest import SPECS, TEST_FOLDER, build_app_from_fixture from connexion import App from connexion.exceptions import InvalidSpecification from connexion.lifecycle import ConnexionResponse diff --git a/tests/conftest.py b/tests/conftest.py index d3baaccea..4a140a9da 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,9 +3,10 @@ import pathlib import sys -import pytest import six -from connexion import App + +import pytest +from connexion import AioHttpApp, App logging.basicConfig(level=logging.DEBUG) @@ -13,6 +14,7 @@ FIXTURES_FOLDER = TEST_FOLDER / 'fixtures' SPEC_FOLDER = TEST_FOLDER / "fakeapi" SPECS = ["swagger.yaml", "openapi.yaml"] +APPS = [App, AioHttpApp] ENCODING_STRINGS = [ six.b("test"), six.u("test"), @@ -21,6 +23,21 @@ ] +def get_apps_specs(): + apps = [App] + if sys.version_info[0:2] >= (3, 5): + apps.append(AioHttpApp) + + apps_specs = [] + for app in apps: + for spec in SPECS: + apps_specs.append({"app_cls": app, "spec_file": spec}) + return apps_specs + + +APPS_SPECS = get_apps_specs() + + class FakeResponse(object): def __init__(self, status_code, text): """ @@ -101,86 +118,103 @@ def json_validation_spec_dir(): return FIXTURES_FOLDER / 'json_validation' -def build_app_from_fixture(api_spec_folder, spec_file='openapi.yaml', **kwargs): +def build_app_from_fixture( + api_spec_folder, + app_cls=App, + spec_file='openapi.yaml', + **kwargs +): debug = True if 'debug' in kwargs: debug = kwargs['debug'] del (kwargs['debug']) - cnx_app = App(__name__, - port=5001, - specification_dir=FIXTURES_FOLDER / api_spec_folder, - debug=debug) + cnx_app = app_cls(__name__, + port=5001, + specification_dir=FIXTURES_FOLDER / api_spec_folder, + debug=debug) cnx_app.add_api(spec_file, **kwargs) cnx_app._spec_file = spec_file return cnx_app -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def simple_app(request): - return build_app_from_fixture('simple', request.param, validate_responses=True) + return build_app_from_fixture('simple', + validate_responses=True, + **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def snake_case_app(request): - return build_app_from_fixture('snake_case', request.param, + return build_app_from_fixture('snake_case', validate_responses=True, - pythonic_params=True) + pythonic_params=True, + **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def invalid_resp_allowed_app(request): - return build_app_from_fixture('simple', request.param, - validate_responses=False) + return build_app_from_fixture('simple', + validate_responses=False, + **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def strict_app(request): - return build_app_from_fixture('simple', request.param, + return build_app_from_fixture('simple', validate_responses=True, - strict_validation=True) + strict_validation=True, + **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def problem_app(request): - return build_app_from_fixture('problem', request.param, - validate_responses=True) + return build_app_from_fixture('problem', + validate_responses=True, + **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def schema_app(request): - return build_app_from_fixture('different_schemas', request.param, - validate_responses=True) + return build_app_from_fixture('different_schemas', + validate_responses=True, + **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def secure_endpoint_app(request): - return build_app_from_fixture('secure_endpoint', request.param, - validate_responses=True, pass_context_arg_name='req_context') + return build_app_from_fixture('secure_endpoint', + validate_responses=True, + pass_context_arg_name='req_context', + **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def secure_api_app(request): options = {"swagger_ui": False} - return build_app_from_fixture('secure_api', request.param, - options=options, auth_all_paths=True) + return build_app_from_fixture('secure_api', + options=options, + auth_all_paths=True, + **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def unordered_definition_app(request): - return build_app_from_fixture('unordered_definition', request.param) + return build_app_from_fixture('unordered_definition', **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def bad_operations_app(request): - return build_app_from_fixture('bad_operations', request.param, - resolver_error=501) + return build_app_from_fixture('bad_operations', + resolver_error=501, + **request.param) -@pytest.fixture(scope="session", params=SPECS) +@pytest.fixture(scope="session", params=APPS_SPECS) def query_sanitazion(request): - return build_app_from_fixture('query_sanitazion', request.param) + return build_app_from_fixture('query_sanitazion', **request.param) if sys.version_info < (3, 5, 3) and sys.version_info[0] == 3: diff --git a/tests/operations/test_validation.py b/tests/operations/test_op_validation.py similarity index 75% rename from tests/operations/test_validation.py rename to tests/operations/test_op_validation.py index b3b52b935..e2c2bee3e 100644 --- a/tests/operations/test_validation.py +++ b/tests/operations/test_op_validation.py @@ -1,9 +1,6 @@ import pytest - -from connexion.operations.validation import ( - BODY_TYPES, - validate_operation_output -) +from connexion.operations.validation import (BODY_TYPES, + validate_operation_output) @pytest.mark.parametrize("output", [ diff --git a/tests/test_json_validation.py b/tests/test_json_validation.py index 16f56d61f..e3103b388 100644 --- a/tests/test_json_validation.py +++ b/tests/test_json_validation.py @@ -3,7 +3,7 @@ from jsonschema.validators import _utils, extend import pytest -from conftest import build_app_from_fixture, SPECS +from conftest import SPECS, build_app_from_fixture from connexion import App from connexion.decorators.validation import RequestBodyValidator from connexion.json_schema import Draft4RequestValidator diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 80d327099..8393fc34c 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1,8 +1,8 @@ import json -import pytest import six +import pytest from connexion.lifecycle import ConnexionResponse diff --git a/tests/test_utils.py b/tests/test_utils.py index 15098550a..d09b15ff1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,8 +1,9 @@ import math +import six + import connexion.apps import pytest -import six from conftest import ENCODING_STRINGS from connexion import utils from mock import MagicMock From 67e7dacdfbb5cf6ae4e6b10121d6afc9abe2bebb Mon Sep 17 00:00:00 2001 From: panpann Date: Thu, 20 Dec 2018 09:30:09 +0100 Subject: [PATCH 09/15] fix: aiohttp client remove query_string before sending request --- connexion/apps/aiohttp_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connexion/apps/aiohttp_app.py b/connexion/apps/aiohttp_app.py index 7830d9247..108c33767 100644 --- a/connexion/apps/aiohttp_app.py +++ b/connexion/apps/aiohttp_app.py @@ -161,7 +161,7 @@ def _async_request(): headers = kwargs.setdefault("headers", {}) if "Content-Type" not in headers: headers["Content-Type"] = content_type - kwargs["params"] = kwargs.get("query_string") + kwargs["params"] = kwargs.pop("query_string", None) res = yield from client.request(method.upper(), url, **kwargs) body = yield from res.read() return ConnexionResponse( From b8d979fdad946f540f01830621a80e576c185af8 Mon Sep 17 00:00:00 2001 From: panpann Date: Fri, 4 Jan 2019 00:05:25 +0100 Subject: [PATCH 10/15] handle NoContent response in operation validation --- connexion/apis/abstract.py | 3 +++ connexion/apis/flask_api.py | 10 +++------- connexion/operations/validation.py | 9 +++++++-- tests/api/test_abstract.py | 1 + tests/operations/test_op_validation.py | 10 ++++++++++ 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/connexion/apis/abstract.py b/connexion/apis/abstract.py index 1bcd9f54e..979c4a8bf 100644 --- a/connexion/apis/abstract.py +++ b/connexion/apis/abstract.py @@ -285,6 +285,9 @@ def encode_body(cls, body, mimetype=None): if json_encode or not is_string(body): if isinstance(body, six.binary_type): body = decode(body) + """if body is empty transform it to object.""" + if body == "": + body = {} body = cls.jsonifier.dumps(body) if isinstance(body, six.text_type): body = body.encode("UTF-8") diff --git a/connexion/apis/flask_api.py b/connexion/apis/flask_api.py index ec9cbe5ac..2ca30c381 100644 --- a/connexion/apis/flask_api.py +++ b/connexion/apis/flask_api.py @@ -7,7 +7,6 @@ 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.lifecycle import ConnexionRequest, ConnexionResponse from connexion.operations.validation import validate_operation_output @@ -159,12 +158,8 @@ def _build_flask_response(cls, mimetype=None, content_type=None, flask_response.status_code = status_code - if data is not None and data is not NoContent: - data = cls.encode_body(data, mimetype) - flask_response.set_data(data) - - elif data is NoContent: - flask_response.set_data('') + data = cls.encode_body(data, mimetype) + flask_response.set_data(data) return flask_response @@ -180,6 +175,7 @@ def _response_from_handler(cls, response, mimetype): return flask.current_app.make_response(response) body, status_code, headers = validate_operation_output(response) + return cls._build_flask_response( mimetype=mimetype, headers=headers, diff --git a/connexion/operations/validation.py b/connexion/operations/validation.py index c473d5fbf..c89133f32 100644 --- a/connexion/operations/validation.py +++ b/connexion/operations/validation.py @@ -1,5 +1,6 @@ import six +from connexion.decorators.produces import NoContent from connexion.utils import normalize_tuple BODY_TYPES = (six.text_type, six.binary_type, dict, list) @@ -7,10 +8,14 @@ def validate_operation_output(response): """Will validate the format returned by a handler.""" - if isinstance(response, BODY_TYPES): + if isinstance(response, BODY_TYPES) or response == NoContent: response = (response, ) + body, status, headers = normalize_tuple(response, 3) - if not isinstance(body, BODY_TYPES): + + if body == NoContent: + body = b"" + elif not isinstance(body, BODY_TYPES): raise ValueError( "first returned value has to be {}, got {}".format( BODY_TYPES, type(body) diff --git a/tests/api/test_abstract.py b/tests/api/test_abstract.py index 127925b6a..5eae2a1ae 100644 --- a/tests/api/test_abstract.py +++ b/tests/api/test_abstract.py @@ -12,6 +12,7 @@ (b"test", "text/plain", b"test"), ("test", "application/json", b'"test"\n'), (b"test", "application/json", b'"test"\n'), + ("", "application/json", b"{}\n") ]) def test_encode_body(body, mimetype, expected): """Test the body encoding. diff --git a/tests/operations/test_op_validation.py b/tests/operations/test_op_validation.py index e2c2bee3e..bf2394c0b 100644 --- a/tests/operations/test_op_validation.py +++ b/tests/operations/test_op_validation.py @@ -1,4 +1,5 @@ import pytest +from connexion.decorators.produces import NoContent from connexion.operations.validation import (BODY_TYPES, validate_operation_output) @@ -16,3 +17,12 @@ def test_validate_operation_output(output): assert isinstance(body, BODY_TYPES) assert isinstance(status, int) or status is None assert isinstance(headers, dict) or headers is None + + +@pytest.mark.parametrize("output", [ + NoContent, + (NoContent,) +]) +def test_validate_operation_no_content(output): + body, _, _ = validate_operation_output(output) + assert body == b"" From 4506e08167f1ecfcb4cd4f663c44b3952298efb4 Mon Sep 17 00:00:00 2001 From: panpann Date: Fri, 4 Jan 2019 00:11:07 +0100 Subject: [PATCH 11/15] make tests from test_headers.py compatible with aiohttp app --- tests/api/test_headers.py | 13 +++++++++++-- tests/fixtures/simple/openapi.yaml | 28 +++++++++++++++------------- tests/fixtures/simple/swagger.yaml | 24 +++++++++++++----------- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/tests/api/test_headers.py b/tests/api/test_headers.py index c66e24d37..9ec1f4226 100644 --- a/tests/api/test_headers.py +++ b/tests/api/test_headers.py @@ -1,12 +1,20 @@ import json +from six.moves.urllib.parse import urlparse + def test_headers_jsonifier(simple_app): + """Don't check on localhost to make this test works for aiohttp. + + aiohttp returns 127.0.0.1 with its port instead of localhost. + """ app_client = simple_app.test_client() response = app_client.post('/v1.0/goodday/dan', data={}) # type: flask.Response + assert response.status_code == 201 - assert response.headers["Location"] == "http://localhost/my/uri" + location = urlparse(response.headers["Location"]) + assert location.path == "/my/uri" def test_headers_produces(simple_app): @@ -14,7 +22,8 @@ def test_headers_produces(simple_app): response = app_client.post('/v1.0/goodevening/dan', data={}) # type: flask.Response assert response.status_code == 201 - assert response.headers["Location"] == "http://localhost/my/uri" + location = urlparse(response.headers["Location"]) + assert location.path == "/my/uri" def test_header_not_returned(simple_app): diff --git a/tests/fixtures/simple/openapi.yaml b/tests/fixtures/simple/openapi.yaml index e1b05a99d..216b725a8 100644 --- a/tests/fixtures/simple/openapi.yaml +++ b/tests/fixtures/simple/openapi.yaml @@ -532,14 +532,16 @@ paths: responses: '200': description: OK - '/goodday/{name}': + # this route needs to be declared before /goodday/{name}. If it isn't, aiohttp will handle + # it with the endpoint fakeapi.hello.post_goodday + /goodday/noheader: post: summary: Generate good day greeting description: Generates a good day message. - operationId: fakeapi.hello.post_goodday + operationId: fakeapi.hello.post_goodday_no_header responses: '201': - description: gooday response + description: goodday response headers: Location: description: The URI of the created resource @@ -549,21 +551,14 @@ paths: 'application/json': schema: type: object - parameters: - - name: name - in: path - description: Name of the person to greet. - required: true - schema: - type: string - /goodday/noheader: + '/goodday/{name}': post: summary: Generate good day greeting description: Generates a good day message. - operationId: fakeapi.hello.post_goodday_no_header + operationId: fakeapi.hello.post_goodday responses: '201': - description: goodday response + description: gooday response headers: Location: description: The URI of the created resource @@ -573,6 +568,13 @@ paths: 'application/json': schema: type: object + parameters: + - name: name + in: path + description: Name of the person to greet. + required: true + schema: + type: string '/goodevening/{name}': post: summary: Generate good evening diff --git a/tests/fixtures/simple/swagger.yaml b/tests/fixtures/simple/swagger.yaml index c8067f1a0..57136c185 100644 --- a/tests/fixtures/simple/swagger.yaml +++ b/tests/fixtures/simple/swagger.yaml @@ -508,12 +508,14 @@ paths: responses: 200: description: OK - - /goodday/{name}: + + # this route needs to be declared before /goodday/{name}. If it isn't, aiohttp will handle + # it with the endpoint fakeapi.hello.post_goodday + /goodday/noheader: post: summary: Generate good day greeting description: Generates a good day message. - operationId: fakeapi.hello.post_goodday + operationId: fakeapi.hello.post_goodday_no_header responses: 201: description: gooday response @@ -523,18 +525,12 @@ paths: description: The URI of the created resource schema: type: object - parameters: - - name: name - in: path - description: Name of the person to greet. - required: true - type: string - /goodday/noheader: + /goodday/{name}: post: summary: Generate good day greeting description: Generates a good day message. - operationId: fakeapi.hello.post_goodday_no_header + operationId: fakeapi.hello.post_goodday responses: 201: description: gooday response @@ -544,6 +540,12 @@ paths: description: The URI of the created resource schema: type: object + parameters: + - name: name + in: path + description: Name of the person to greet. + required: true + type: string /goodevening/{name}: post: From d85bc6846df7cca3da2cf2f004d1df92abbce2da Mon Sep 17 00:00:00 2001 From: panpann Date: Fri, 4 Jan 2019 00:11:21 +0100 Subject: [PATCH 12/15] ConnexionResponse content type's is equal to mimetype by default --- connexion/lifecycle.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/connexion/lifecycle.py b/connexion/lifecycle.py index 3c262ec0d..ac992acc1 100644 --- a/connexion/lifecycle.py +++ b/connexion/lifecycle.py @@ -44,7 +44,10 @@ def __init__(self, self.mimetype = mimetype self.body = body self.headers = headers or {} - self.content_type = content_type or self.headers.get("Content-Type") + self.content_type = ( + content_type or + self.headers.get("Content-Type", self.mimetype) + ) @property def text(self): From e488a6f78486633244ec499839943327f8655e45 Mon Sep 17 00:00:00 2001 From: panpann Date: Mon, 7 Jan 2019 21:55:41 +0100 Subject: [PATCH 13/15] handle HTTPStatus and Enum status in validate_operation_output --- connexion/apis/flask_api.py | 4 ---- connexion/operations/validation.py | 5 +++++ tests/operations/test_op_validation.py | 29 +++++++++++++++++++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/connexion/apis/flask_api.py b/connexion/apis/flask_api.py index 2ca30c381..272b4d50c 100644 --- a/connexion/apis/flask_api.py +++ b/connexion/apis/flask_api.py @@ -152,10 +152,6 @@ def _build_flask_response(cls, mimetype=None, content_type=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 data = cls.encode_body(data, mimetype) diff --git a/connexion/operations/validation.py b/connexion/operations/validation.py index c89133f32..dbeb0d9cc 100644 --- a/connexion/operations/validation.py +++ b/connexion/operations/validation.py @@ -21,7 +21,12 @@ def validate_operation_output(response): BODY_TYPES, type(body) ) ) + status = status or 200 + if hasattr(status, "value"): + """Handle Enum and http.HTTPStatus status. see #504""" + status = status.value + if headers is not None and not isinstance(headers, dict): raise ValueError( "Type of 3rd return value is dict, got {}".format( diff --git a/tests/operations/test_op_validation.py b/tests/operations/test_op_validation.py index bf2394c0b..96fa27353 100644 --- a/tests/operations/test_op_validation.py +++ b/tests/operations/test_op_validation.py @@ -1,3 +1,5 @@ +import sys + import pytest from connexion.decorators.produces import NoContent from connexion.operations.validation import (BODY_TYPES, @@ -10,7 +12,7 @@ b"test", ("test",), {}, - [] + [], ]) def test_validate_operation_output(output): body, status, headers = validate_operation_output(output) @@ -26,3 +28,28 @@ def test_validate_operation_output(output): def test_validate_operation_no_content(output): body, _, _ = validate_operation_output(output) assert body == b"" + + +@pytest.mark.skipif(sys.version_info < (3, 4), + reason="requires python3.4 or higher") +def test_validate_operation_output_enum(): + """Support enum status, see #504.""" + from enum import Enum + + class HTTPStatus(Enum): + OK = 200 + + output = ("test", HTTPStatus.OK) + _, status, _ = validate_operation_output(output) + assert isinstance(status, int) and status == 200 + + +@pytest.mark.skipif(sys.version_info < (3, 5), + reason="requires python3.5 or higher") +def test_validate_operation_output_httpstatus(): + """Support http.HTTPStatus, see #504.""" + from http import HTTPStatus + + output = ("test", HTTPStatus.OK) + _, status, _ = validate_operation_output(output) + assert isinstance(status, int) and status == 200 From b93cb7362fb53ffb60a95b263c631a497fbd442b Mon Sep 17 00:00:00 2001 From: panpann Date: Mon, 7 Jan 2019 21:55:54 +0100 Subject: [PATCH 14/15] test custom encoder only on FlaskApp --- tests/api/test_responses.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/api/test_responses.py b/tests/api/test_responses.py index 902055787..08231c27e 100644 --- a/tests/api/test_responses.py +++ b/tests/api/test_responses.py @@ -1,8 +1,10 @@ import json from struct import unpack +import pytest from werkzeug.test import Client, EnvironBuilder +from connexion import FlaskApp from connexion.apps.flask_app import FlaskJSONEncoder @@ -134,6 +136,8 @@ def test_default_object_body(simple_app): def test_custom_encoder(simple_app): + if not isinstance(simple_app, FlaskApp): + pytest.skip("flask test only") class CustomEncoder(FlaskJSONEncoder): def default(self, o): @@ -141,9 +145,8 @@ def default(self, o): return "cool result" return FlaskJSONEncoder.default(self, o) - flask_app = simple_app.app - flask_app.json_encoder = CustomEncoder - app_client = flask_app.test_client() + simple_app.app.json_encoder = CustomEncoder + app_client = simple_app.test_client() resp = app_client.get('/v1.0/custom-json-response') assert resp.status_code == 200 From 2f9c42e734885295b73d4e16217ad9a5aa8f4534 Mon Sep 17 00:00:00 2001 From: panpann Date: Mon, 7 Jan 2019 21:57:21 +0100 Subject: [PATCH 15/15] fix AioHttpApp.test_client when Content-Type is not set --- connexion/apps/aiohttp_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/connexion/apps/aiohttp_app.py b/connexion/apps/aiohttp_app.py index 108c33767..5d220d7ba 100644 --- a/connexion/apps/aiohttp_app.py +++ b/connexion/apps/aiohttp_app.py @@ -34,6 +34,7 @@ def error_middleware(request, handler): except ProblemException as exception: error_response = exception.to_problem() except Exception: + logger.exception("aiohttp error") error_response = problem( title=HTTP_ERRORS[500]["title"], detail=HTTP_ERRORS[500]["detail"], @@ -156,7 +157,7 @@ def _request( @asyncio.coroutine def _async_request(): nonlocal client - content_type = kwargs.get("content_type") + content_type = kwargs.pop("content_type", None) if content_type: headers = kwargs.setdefault("headers", {}) if "Content-Type" not in headers: