diff --git a/openapi_core/deserializing/parameters/factories.py b/openapi_core/deserializing/parameters/factories.py index f9cfa504..85288e67 100644 --- a/openapi_core/deserializing/parameters/factories.py +++ b/openapi_core/deserializing/parameters/factories.py @@ -1,5 +1,3 @@ -import warnings - from openapi_core.deserializing.parameters.deserializers import ( PrimitiveDeserializer, ) @@ -16,12 +14,6 @@ class ParameterDeserializersFactory(object): } def create(self, param): - if param.getkey('deprecated', False): - warnings.warn( - "{0} parameter is deprecated".format(param['name']), - DeprecationWarning, - ) - style = get_style(param) deserialize_callable = self.PARAMETER_STYLE_DESERIALIZERS[style] diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index 58df805b..f19e86e3 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -1,6 +1,7 @@ """OpenAPI core validation request validators module""" from __future__ import division from itertools import chain +import warnings from openapi_core.casting.schemas.exceptions import CastError from openapi_core.deserializing.exceptions import DeserializeError @@ -160,41 +161,52 @@ def _get_parameters(self, request, params): continue seen.add((param_name, param_location)) try: - raw_value = self._get_parameter_value(param, request) - except MissingRequiredParameter as exc: - errors.append(exc) - continue + value = self._get_parameter(param, request) except MissingParameter: - if 'schema' not in param: - continue - schema = param / 'schema' - if 'default' not in schema: - continue - casted = schema['default'] - else: - try: - deserialised = self._deserialise_parameter( - param, raw_value) - except DeserializeError as exc: - errors.append(exc) - continue - - try: - casted = self._cast(param, deserialised) - except CastError as exc: - errors.append(exc) - continue - - try: - unmarshalled = self._unmarshal(param, casted) - except (ValidateError, UnmarshalError) as exc: + continue + except ( + MissingRequiredParameter, DeserializeError, + CastError, ValidateError, UnmarshalError, + ) as exc: errors.append(exc) + continue else: locations.setdefault(param_location, {}) - locations[param_location][param_name] = unmarshalled + locations[param_location][param_name] = value return RequestParameters(**locations), errors + def _get_parameter(self, param, request): + if param.getkey('deprecated', False): + warnings.warn( + "{0} parameter is deprecated".format(param['name']), + DeprecationWarning, + ) + + try: + raw_value = self._get_parameter_value(param, request) + except MissingParameter: + if 'schema' not in param: + raise + schema = param / 'schema' + if 'default' not in schema: + raise + casted = schema['default'] + else: + # Simple scenario + if 'content' not in param: + deserialised = self._deserialise_parameter(param, raw_value) + schema = param / 'schema' + # Complex scenario + else: + content = param / 'content' + mimetype, media_type = next(content.items()) + deserialised = self._deserialise_data(mimetype, raw_value) + schema = media_type / 'schema' + casted = self._cast(schema, deserialised) + unmarshalled = self._unmarshal(schema, casted) + return unmarshalled + def _get_body(self, request, operation): if 'requestBody' not in operation: return None, [] @@ -224,8 +236,12 @@ def _get_body(self, request, operation): except CastError as exc: return None, [exc, ] + if 'schema' not in media_type: + return casted, [] + + schema = media_type / 'schema' try: - body = self._unmarshal(media_type, casted) + body = self._unmarshal(schema, casted) except (ValidateError, UnmarshalError) as exc: return None, [exc, ] diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index 0013f292..f1cdb0c3 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -105,8 +105,12 @@ def _get_data(self, response, operation_response): except CastError as exc: return None, [exc, ] + if 'schema' not in media_type: + return casted, [] + + schema = media_type / 'schema' try: - data = self._unmarshal(media_type, casted) + data = self._unmarshal(schema, casted) except (ValidateError, UnmarshalError) as exc: return None, [exc, ] diff --git a/openapi_core/validation/validators.py b/openapi_core/validation/validators.py index a50fc6b7..55a3f109 100644 --- a/openapi_core/validation/validators.py +++ b/openapi_core/validation/validators.py @@ -52,19 +52,10 @@ def _deserialise_data(self, mimetype, value): deserializer = self.media_type_deserializers_factory.create(mimetype) return deserializer(value) - def _cast(self, param_or_media_type, value): - # return param_or_media_type.cast(value) - if 'schema' not in param_or_media_type: - return value - - schema = param_or_media_type / 'schema' + def _cast(self, schema, value): caster = self.schema_casters_factory.create(schema) return caster(value) - def _unmarshal(self, param_or_media_type, value): - if 'schema' not in param_or_media_type: - return value - - schema = param_or_media_type / 'schema' + def _unmarshal(self, schema, value): unmarshaller = self.schema_unmarshallers_factory.create(schema) return unmarshaller(value) diff --git a/tests/integration/data/v3.0/petstore.yaml b/tests/integration/data/v3.0/petstore.yaml index 01b90f4a..fe10a175 100644 --- a/tests/integration/data/v3.0/petstore.yaml +++ b/tests/integration/data/v3.0/petstore.yaml @@ -83,15 +83,17 @@ paths: type: object required: - lat - - long + - lon properties: lat: type: number - long: + lon: type: number responses: '200': $ref: "#/components/responses/PetsResponse" + '404': + $ref: "#/components/responses/HtmlResponse" post: summary: Create a pet description: Creates new pet entry @@ -119,6 +121,13 @@ paths: type: integer format: int32 required: true + - name: userdata + in: cookie + content: + application/json: + schema: + $ref: '#/components/schemas/Userdata' + required: false requestBody: required: true content: @@ -128,6 +137,7 @@ paths: example: name: "Pet" wings: [] + text/plain: {} responses: '201': description: Null response @@ -220,6 +230,13 @@ paths: $ref: "#/components/responses/ErrorResponse" components: schemas: + Userdata: + type: object + required: + - name + properties: + name: + type: string Utctime: oneOf: - type: string @@ -406,6 +423,10 @@ components: application/json: schema: $ref: "#/components/schemas/ExtendedError" + HtmlResponse: + description: HTML page + content: + text/html: {} PetsResponse: description: An paged array of pets headers: diff --git a/tests/integration/validation/test_petstore.py b/tests/integration/validation/test_petstore.py index 7f2af239..486b158c 100644 --- a/tests/integration/validation/test_petstore.py +++ b/tests/integration/validation/test_petstore.py @@ -139,6 +139,38 @@ def test_get_pets_response(self, spec, response_validator): assert response_result.data.data[0].id == 1 assert response_result.data.data[0].name == 'Cat' + def test_get_pets_response_no_schema(self, spec, response_validator): + host_url = 'http://petstore.swagger.io/v1' + path_pattern = '/v1/pets' + query_params = { + 'limit': '20', + } + + request = MockRequest( + host_url, 'GET', '/pets', + path_pattern=path_pattern, args=query_params, + ) + + parameters = validate_parameters(spec, request) + body = validate_body(spec, request) + + assert parameters == RequestParameters( + query={ + 'limit': 20, + 'page': 1, + 'search': '', + } + ) + assert body is None + + data = '' + response = MockResponse(data, status_code=404, mimetype='text/html') + + response_result = response_validator.validate(request, response) + + assert response_result.errors == [] + assert response_result.data == data + def test_get_pets_invalid_response(self, spec, response_validator): host_url = 'http://petstore.swagger.io/v1' path_pattern = '/v1/pets' @@ -393,9 +425,6 @@ def test_get_pets_param_order(self, spec): assert body is None - @pytest.mark.xfail( - reason="No parameters deserialization support for complex scenarios" - ) def test_get_pets_param_coordinates(self, spec): host_url = 'http://petstore.swagger.io/v1' path_pattern = '/v1/pets' @@ -453,8 +482,13 @@ def test_post_birds(self, spec, spec_dict): headers = { 'api_key': self.api_key_encoded, } + userdata = { + 'name': 'user1', + } + userdata_json = json.dumps(userdata) cookies = { 'user': '123', + 'userdata': userdata_json, } request = MockRequest( @@ -471,6 +505,9 @@ def test_post_birds(self, spec, spec_dict): }, cookie={ 'user': 123, + 'userdata': { + 'name': 'user1', + }, }, ) diff --git a/tests/integration/validation/test_validators.py b/tests/integration/validation/test_validators.py index 235341d6..3168d6f6 100644 --- a/tests/integration/validation/test_validators.py +++ b/tests/integration/validation/test_validators.py @@ -194,6 +194,66 @@ def test_invalid_content_type(self, validator): }, ) + def test_invalid_complex_parameter(self, validator, spec_dict): + pet_name = 'Cat' + pet_tag = 'cats' + pet_street = 'Piekna' + pet_city = 'Warsaw' + data_json = { + 'name': pet_name, + 'tag': pet_tag, + 'position': 2, + 'address': { + 'street': pet_street, + 'city': pet_city, + }, + 'ears': { + 'healthy': True, + } + } + data = json.dumps(data_json) + headers = { + 'api_key': self.api_key_encoded, + } + userdata = { + 'name': 1, + } + userdata_json = json.dumps(userdata) + cookies = { + 'user': '123', + 'userdata': userdata_json, + } + request = MockRequest( + 'https://development.gigantic-server.com', 'post', '/v1/pets', + path_pattern='/v1/pets', data=data, + headers=headers, cookies=cookies, + ) + + result = validator.validate(request) + + assert len(result.errors) == 1 + assert type(result.errors[0]) == InvalidSchemaValue + assert result.parameters == RequestParameters( + header={ + 'api_key': self.api_key, + }, + cookie={ + 'user': 123, + }, + ) + assert result.security == {} + + schemas = spec_dict['components']['schemas'] + pet_model = schemas['PetCreate']['x-model'] + address_model = schemas['Address']['x-model'] + assert result.body.__class__.__name__ == pet_model + assert result.body.name == pet_name + assert result.body.tag == pet_tag + assert result.body.position == 2 + assert result.body.address.__class__.__name__ == address_model + assert result.body.address.street == pet_street + assert result.body.address.city == pet_city + def test_post_pets(self, validator, spec_dict): pet_name = 'Cat' pet_tag = 'cats' @@ -248,6 +308,35 @@ def test_post_pets(self, validator, spec_dict): assert result.body.address.street == pet_street assert result.body.address.city == pet_city + def test_post_pets_plain_no_schema(self, validator, spec_dict): + data = 'plain text' + headers = { + 'api_key': self.api_key_encoded, + } + cookies = { + 'user': '123', + } + request = MockRequest( + 'https://development.gigantic-server.com', 'post', '/v1/pets', + path_pattern='/v1/pets', data=data, + headers=headers, cookies=cookies, + mimetype='text/plain', + ) + + result = validator.validate(request) + + assert result.errors == [] + assert result.parameters == RequestParameters( + header={ + 'api_key': self.api_key, + }, + cookie={ + 'user': 123, + }, + ) + assert result.security == {} + assert result.body == data + def test_get_pet_unauthorized(self, validator): request = MockRequest( self.host_url, 'get', '/v1/pets/1', diff --git a/tests/unit/deserializing/test_parameters_deserializers.py b/tests/unit/deserializing/test_parameters_deserializers.py index a34cc815..06036acd 100644 --- a/tests/unit/deserializing/test_parameters_deserializers.py +++ b/tests/unit/deserializing/test_parameters_deserializers.py @@ -17,20 +17,6 @@ def create_deserializer(param): return ParameterDeserializersFactory().create(param) return create_deserializer - def test_deprecated(self, deserializer_factory): - spec = { - 'name': 'param', - 'in': 'query', - 'deprecated': True, - } - param = SpecPath.from_spec(spec) - value = 'test' - - with pytest.warns(DeprecationWarning): - result = deserializer_factory(param)(value) - - assert result == value - def test_query_empty(self, deserializer_factory): spec = { 'name': 'param',