From 3fecd3e7351bfff330c5421551d8e26f88cbfc28 Mon Sep 17 00:00:00 2001 From: Daniel Grossmann-Kavanagh Date: Wed, 29 Jan 2020 00:24:48 -0800 Subject: [PATCH] fix nested additionalProperties (#1138) --- connexion/operations/openapi.py | 28 ++++++++++++---------------- tests/api/test_responses.py | 20 +++++++++++++++++++- tests/fakeapi/hello.py | 2 ++ tests/fixtures/simple/openapi.yaml | 19 +++++++++++++++++++ 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/connexion/operations/openapi.py b/connexion/operations/openapi.py index 2d0b1792c..d32552117 100644 --- a/connexion/operations/openapi.py +++ b/connexion/operations/openapi.py @@ -1,5 +1,5 @@ import logging -from copy import deepcopy +from copy import copy, deepcopy from connexion.operations.abstract import AbstractOperation @@ -303,23 +303,30 @@ def _get_typed_body_values(self, body_arg, body_props, additional_props): return res - def _build_default_obj(self, _properties, res={}): + def _build_default_obj_recursive(self, _properties, res): """ takes disparate and nested default keys, and builds up a default object """ for key, prop in _properties.items(): if 'default' in prop and key not in res: - res[key] = prop['default'] + res[key] = copy(prop['default']) elif prop.get('type') == 'object' and 'properties' in prop: res.setdefault(key, {}) - res[key] = self._build_default_obj(prop['properties'], res[key]) + res[key] = self._build_default_obj_recursive(prop['properties'], res[key]) return res + def _get_default_obj(self, schema): + try: + return deepcopy(schema["default"]) + except KeyError: + _properties = schema.get("properties", {}) + return self._build_default_obj_recursive(_properties, {}) + def _get_query_defaults(self, query_defns): defaults = {} for k, v in query_defns.items(): try: if v["schema"]["type"] == "object": - defaults[k] = self._build_default_obj(v["schema"]["properties"]) + defaults[k] = self._get_default_obj(v["schema"]) else: defaults[k] = v["schema"]["default"] except KeyError: @@ -345,16 +352,5 @@ def _get_val_from_param(self, value, query_defn): if query_schema["type"] == "array": return [make_type(part, query_schema["items"]["type"]) for part in value] - elif query_schema["type"] == "object" and 'properties' in query_schema: - return_dict = {} - for prop_key in query_schema['properties'].keys(): - prop_value = value.get(prop_key, None) - if prop_value is not None: # False is a valid value for boolean values - try: - return_dict[prop_key] = make_type(value[prop_key], - query_schema['properties'][prop_key]['type']) - except (KeyError, TypeError): - return value - return return_dict else: return make_type(value, query_schema["type"]) diff --git a/tests/api/test_responses.py b/tests/api/test_responses.py index 9cae54ac2..76b6e406e 100644 --- a/tests/api/test_responses.py +++ b/tests/api/test_responses.py @@ -142,7 +142,7 @@ def test_empty(simple_app): def test_exploded_deep_object_param_endpoint_openapi_simple(simple_openapi_app): app_client = simple_openapi_app.app.test_client() - response = app_client.get('/v1.0/exploded-deep-object-param?id[foo]=bar&id[foofoo]=barbar') # type: flask.Response + response = app_client.get('/v1.0/exploded-deep-object-param?id[foo]=bar') # type: flask.Response assert response.status_code == 200 response_data = json.loads(response.data.decode('utf-8', 'replace')) assert response_data == {'foo': 'bar', 'foo4': 'blubb'} @@ -166,6 +166,13 @@ def test_exploded_deep_object_param_endpoint_openapi_additional_properties(simpl assert response_data == {'foo': 'bar', 'fooint': '2'} +def test_exploded_deep_object_param_endpoint_openapi_additional_properties_false(simple_openapi_app): + app_client = simple_openapi_app.app.test_client() + + response = app_client.get('/v1.0/exploded-deep-object-param?id[foo]=bar&id[foofoo]=barbar') # type: flask.Response + assert response.status_code == 400 + + def test_exploded_deep_object_param_endpoint_openapi_with_dots(simple_openapi_app): app_client = simple_openapi_app.app.test_client() @@ -220,6 +227,17 @@ def test_empty_object_body(simple_app): assert response['stack'] == {} +def test_nested_additional_properties(simple_openapi_app): + app_client = simple_openapi_app.app.test_client() + resp = app_client.post( + '/v1.0/test-nested-additional-properties', + data=json.dumps({"nested": {"object": True}}), + headers={'Content-Type': 'application/json'}) + assert resp.status_code == 200 + response = json.loads(resp.data.decode('utf-8', 'replace')) + assert response == {"nested": {"object": True}} + + def test_custom_encoder(simple_app): class CustomEncoder(FlaskJSONEncoder): diff --git a/tests/fakeapi/hello.py b/tests/fakeapi/hello.py index 9bac8e6df..1abc0860f 100755 --- a/tests/fakeapi/hello.py +++ b/tests/fakeapi/hello.py @@ -281,6 +281,8 @@ def test_default_param(name): def test_default_object_body(stack): return {"stack": stack} +def test_nested_additional_properties(body): + return body def test_default_integer_body(stack_version): return stack_version diff --git a/tests/fixtures/simple/openapi.yaml b/tests/fixtures/simple/openapi.yaml index 73c127298..80a3bf4fc 100644 --- a/tests/fixtures/simple/openapi.yaml +++ b/tests/fixtures/simple/openapi.yaml @@ -163,6 +163,7 @@ paths: explode: true schema: type: object + additionalProperties: false properties: foo: type: string @@ -277,6 +278,24 @@ paths: $ref: '#/components/schemas/new_stack' default: image_version: default_image + /test-nested-additional-properties: + post: + summary: Test if nested additionalProperties are cast + operationId: fakeapi.hello.test_nested_additional_properties + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + nested: + type: object + properties: {} + additionalProperties: + type: boolean /test-default-integer-body: post: summary: Test if default integer body param is passed to handler.