From 0031ff74c04146fa6ba91925a7f54e0d5f7b2159 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Tue, 31 Oct 2023 16:44:18 +0000 Subject: [PATCH 1/2] Request binary format support --- openapi_core/contrib/aiohttp/requests.py | 4 +- openapi_core/contrib/django/requests.py | 4 +- openapi_core/contrib/falcon/requests.py | 11 ++--- openapi_core/contrib/requests/requests.py | 6 +-- openapi_core/contrib/starlette/requests.py | 6 +-- openapi_core/contrib/werkzeug/requests.py | 4 +- .../deserializing/media_types/datatypes.py | 2 +- .../media_types/deserializers.py | 9 ++-- .../deserializing/media_types/exceptions.py | 4 +- .../deserializing/media_types/util.py | 37 ++++++++-------- openapi_core/protocols.py | 2 +- openapi_core/testing/requests.py | 2 +- openapi_core/validation/request/validators.py | 6 +-- openapi_core/validation/validators.py | 6 +-- .../data/v3.0/djangoproject/pets/views.py | 2 +- .../contrib/django/test_django_project.py | 6 +-- .../contrib/falcon/test_falcon_project.py | 9 ++-- .../data/v3.0/flaskproject/pets/views.py | 4 +- .../contrib/flask/test_flask_project.py | 13 ++---- .../requests/test_requests_validation.py | 4 -- .../starlette/test_starlette_project.py | 4 -- tests/integration/test_petstore.py | 38 ++++++++-------- .../test_read_only_write_only.py | 6 +-- .../test_request_unmarshaller.py | 10 ++--- .../validation/test_request_validators.py | 2 +- tests/unit/contrib/django/test_django.py | 10 ++--- .../unit/contrib/flask/test_flask_requests.py | 6 +-- .../test_media_types_deserializers.py | 43 +++++++++++++------ 28 files changed, 130 insertions(+), 130 deletions(-) diff --git a/openapi_core/contrib/aiohttp/requests.py b/openapi_core/contrib/aiohttp/requests.py index e2dc0a8e..c7f330c0 100644 --- a/openapi_core/contrib/aiohttp/requests.py +++ b/openapi_core/contrib/aiohttp/requests.py @@ -19,7 +19,7 @@ class Empty: class AIOHTTPOpenAPIWebRequest: __slots__ = ("request", "parameters", "_get_body", "_body") - def __init__(self, request: web.Request, *, body: str | None): + def __init__(self, request: web.Request, *, body: bytes | None): if not isinstance(request, web.Request): raise TypeError( f"'request' argument is not type of {web.Request.__qualname__!r}" @@ -45,7 +45,7 @@ def method(self) -> str: return self.request.method.lower() @property - def body(self) -> str | None: + def body(self) -> bytes | None: return self._body @property diff --git a/openapi_core/contrib/django/requests.py b/openapi_core/contrib/django/requests.py index fe41a3cf..8b4f1987 100644 --- a/openapi_core/contrib/django/requests.py +++ b/openapi_core/contrib/django/requests.py @@ -76,9 +76,9 @@ def method(self) -> str: return self.request.method.lower() @property - def body(self) -> str: + def body(self) -> bytes: assert isinstance(self.request.body, bytes) - return self.request.body.decode("utf-8") + return self.request.body @property def content_type(self) -> str: diff --git a/openapi_core/contrib/falcon/requests.py b/openapi_core/contrib/falcon/requests.py index 7ebf7274..af08b7b7 100644 --- a/openapi_core/contrib/falcon/requests.py +++ b/openapi_core/contrib/falcon/requests.py @@ -49,13 +49,14 @@ def method(self) -> str: return self.request.method.lower() @property - def body(self) -> Optional[str]: + def body(self) -> Optional[bytes]: + # Falcon doesn't store raw request stream. + # That's why we need to revert deserialized data + # Support falcon-jsonify. if hasattr(self.request, "json"): - return dumps(self.request.json) + return dumps(self.request.json).encode("utf-8") - # Falcon doesn't store raw request stream. - # That's why we need to revert serialized data media = self.request.get_media( default_when_empty=self.default_when_empty, ) @@ -74,7 +75,7 @@ def body(self) -> Optional[str]: return None else: assert isinstance(body, bytes) - return body.decode("utf-8") + return body @property def content_type(self) -> str: diff --git a/openapi_core/contrib/requests/requests.py b/openapi_core/contrib/requests/requests.py index 549ed90b..2e33f02d 100644 --- a/openapi_core/contrib/requests/requests.py +++ b/openapi_core/contrib/requests/requests.py @@ -64,14 +64,14 @@ def method(self) -> str: return method and method.lower() or "" @property - def body(self) -> Optional[str]: + def body(self) -> Optional[bytes]: if self.request.body is None: return None if isinstance(self.request.body, bytes): - return self.request.body.decode("utf-8") + return self.request.body assert isinstance(self.request.body, str) # TODO: figure out if request._body_position is relevant - return self.request.body + return self.request.body.encode("utf-8") @property def content_type(self) -> str: diff --git a/openapi_core/contrib/starlette/requests.py b/openapi_core/contrib/starlette/requests.py index 60dc610c..b556fd8f 100644 --- a/openapi_core/contrib/starlette/requests.py +++ b/openapi_core/contrib/starlette/requests.py @@ -34,14 +34,14 @@ def method(self) -> str: return self.request.method.lower() @property - def body(self) -> Optional[str]: + def body(self) -> Optional[bytes]: body = self._get_body() if body is None: return None if isinstance(body, bytes): - return body.decode("utf-8") + return body assert isinstance(body, str) - return body + return body.encode("utf-8") @property def content_type(self) -> str: diff --git a/openapi_core/contrib/werkzeug/requests.py b/openapi_core/contrib/werkzeug/requests.py index edd62c98..cd23c67d 100644 --- a/openapi_core/contrib/werkzeug/requests.py +++ b/openapi_core/contrib/werkzeug/requests.py @@ -39,8 +39,8 @@ def method(self) -> str: return self.request.method.lower() @property - def body(self) -> Optional[str]: - return self.request.get_data(as_text=True) + def body(self) -> Optional[bytes]: + return self.request.get_data(as_text=False) @property def content_type(self) -> str: diff --git a/openapi_core/deserializing/media_types/datatypes.py b/openapi_core/deserializing/media_types/datatypes.py index db226cfe..4d8f8fd8 100644 --- a/openapi_core/deserializing/media_types/datatypes.py +++ b/openapi_core/deserializing/media_types/datatypes.py @@ -2,5 +2,5 @@ from typing import Callable from typing import Dict -DeserializerCallable = Callable[[Any], Any] +DeserializerCallable = Callable[[bytes], Any] MediaTypeDeserializersDict = Dict[str, DeserializerCallable] diff --git a/openapi_core/deserializing/media_types/deserializers.py b/openapi_core/deserializing/media_types/deserializers.py index e7169c4c..2169cc05 100644 --- a/openapi_core/deserializing/media_types/deserializers.py +++ b/openapi_core/deserializing/media_types/deserializers.py @@ -41,7 +41,9 @@ def __init__( extra_media_type_deserializers = {} self.extra_media_type_deserializers = extra_media_type_deserializers - def deserialize(self, mimetype: str, value: Any, **parameters: str) -> Any: + def deserialize( + self, mimetype: str, value: bytes, **parameters: str + ) -> Any: deserializer_callable = self.get_deserializer_callable(mimetype) try: @@ -75,7 +77,7 @@ def __init__( self.encoding = encoding self.parameters = parameters - def deserialize(self, value: Any) -> Any: + def deserialize(self, value: bytes) -> Any: deserialized = self.media_types_deserializer.deserialize( self.mimetype, value, **self.parameters ) @@ -192,5 +194,4 @@ def decode_property_content_type( value = location.getlist(prop_name) return list(map(prop_deserializer.deserialize, value)) - value = location[prop_name] - return prop_deserializer.deserialize(value) + return prop_deserializer.deserialize(location[prop_name]) diff --git a/openapi_core/deserializing/media_types/exceptions.py b/openapi_core/deserializing/media_types/exceptions.py index 66dd904d..a5ecfeb4 100644 --- a/openapi_core/deserializing/media_types/exceptions.py +++ b/openapi_core/deserializing/media_types/exceptions.py @@ -8,9 +8,9 @@ class MediaTypeDeserializeError(DeserializeError): """Media type deserialize operation error""" mimetype: str - value: str + value: bytes def __str__(self) -> str: return ( "Failed to deserialize value with {mimetype} mimetype: {value}" - ).format(value=self.value, mimetype=self.mimetype) + ).format(value=self.value.decode("utf-8"), mimetype=self.mimetype) diff --git a/openapi_core/deserializing/media_types/util.py b/openapi_core/deserializing/media_types/util.py index 1469bed1..fb5cc645 100644 --- a/openapi_core/deserializing/media_types/util.py +++ b/openapi_core/deserializing/media_types/util.py @@ -10,16 +10,11 @@ from werkzeug.datastructures import ImmutableMultiDict -def binary_loads(value: Union[str, bytes], **parameters: str) -> bytes: - charset = "utf-8" - if "charset" in parameters: - charset = parameters["charset"] - if isinstance(value, str): - return value.encode(charset) +def binary_loads(value: bytes, **parameters: str) -> bytes: return value -def plain_loads(value: Union[str, bytes], **parameters: str) -> str: +def plain_loads(value: bytes, **parameters: str) -> str: charset = "utf-8" if "charset" in parameters: charset = parameters["charset"] @@ -32,30 +27,36 @@ def plain_loads(value: Union[str, bytes], **parameters: str) -> str: return value -def json_loads(value: Union[str, bytes], **parameters: str) -> Any: +def json_loads(value: bytes, **parameters: str) -> Any: return loads(value) -def xml_loads(value: Union[str, bytes], **parameters: str) -> Element: - return fromstring(value) +def xml_loads(value: bytes, **parameters: str) -> Element: + charset = "utf-8" + if "charset" in parameters: + charset = parameters["charset"] + return fromstring(value.decode(charset)) -def urlencoded_form_loads(value: Any, **parameters: str) -> Mapping[str, Any]: - return ImmutableMultiDict(parse_qsl(value)) +def urlencoded_form_loads( + value: bytes, **parameters: str +) -> Mapping[str, Any]: + # only UTF-8 is conforming + return ImmutableMultiDict(parse_qsl(value.decode("utf-8"))) -def data_form_loads( - value: Union[str, bytes], **parameters: str -) -> Mapping[str, Any]: - if isinstance(value, bytes): - value = value.decode("ASCII", errors="surrogateescape") +def data_form_loads(value: bytes, **parameters: str) -> Mapping[str, Any]: + charset = "ASCII" + if "charset" in parameters: + charset = parameters["charset"] + decoded = value.decode(charset, errors="surrogateescape") boundary = "" if "boundary" in parameters: boundary = parameters["boundary"] parser = Parser() mimetype = "multipart/form-data" header = f'Content-Type: {mimetype}; boundary="{boundary}"' - text = "\n\n".join([header, value]) + text = "\n\n".join([header, decoded]) parts = parser.parsestr(text, headersonly=False) return ImmutableMultiDict( [ diff --git a/openapi_core/protocols.py b/openapi_core/protocols.py index d60b36bf..771d1f27 100644 --- a/openapi_core/protocols.py +++ b/openapi_core/protocols.py @@ -17,7 +17,7 @@ def method(self) -> str: ... @property - def body(self) -> Optional[str]: + def body(self) -> Optional[bytes]: ... @property diff --git a/openapi_core/testing/requests.py b/openapi_core/testing/requests.py index 9fe50e77..81e97f24 100644 --- a/openapi_core/testing/requests.py +++ b/openapi_core/testing/requests.py @@ -20,7 +20,7 @@ def __init__( view_args: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, Any]] = None, cookies: Optional[Dict[str, Any]] = None, - data: Optional[str] = None, + data: Optional[bytes] = None, content_type: str = "application/json", ): self.host_url = host_url diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index 1781fd2b..dc12bb34 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -248,7 +248,7 @@ def _get_security_value( @ValidationErrorWrapper(RequestBodyValidationError, InvalidRequestBody) def _get_body( - self, body: Optional[str], mimetype: str, operation: SchemaPath + self, body: Optional[bytes], mimetype: str, operation: SchemaPath ) -> Any: if "requestBody" not in operation: return None @@ -262,8 +262,8 @@ def _get_body( return value def _get_body_value( - self, body: Optional[str], request_body: SchemaPath - ) -> Any: + self, body: Optional[bytes], request_body: SchemaPath + ) -> bytes: if not body: if request_body.getkey("required", False): raise MissingRequiredRequestBody diff --git a/openapi_core/validation/validators.py b/openapi_core/validation/validators.py index ad82705e..03e80f1b 100644 --- a/openapi_core/validation/validators.py +++ b/openapi_core/validation/validators.py @@ -106,7 +106,7 @@ def _deserialise_media_type( media_type: SchemaPath, mimetype: str, parameters: Mapping[str, str], - value: Any, + value: bytes, ) -> Any: schema = media_type.get("schema") encoding = None @@ -222,7 +222,7 @@ def _get_complex_param_or_header( def _get_content_schema_value_and_schema( self, - raw: Any, + raw: bytes, content: SchemaPath, mimetype: Optional[str] = None, ) -> Tuple[Any, Optional[SchemaPath]]: @@ -246,7 +246,7 @@ def _get_content_schema_value_and_schema( return casted, schema def _get_content_and_schema( - self, raw: Any, content: SchemaPath, mimetype: Optional[str] = None + self, raw: bytes, content: SchemaPath, mimetype: Optional[str] = None ) -> Tuple[Any, Optional[SchemaPath]]: casted, schema = self._get_content_schema_value_and_schema( raw, content, mimetype diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py b/tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py index 16a8f1c1..cb83ce71 100644 --- a/tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py +++ b/tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py @@ -110,7 +110,7 @@ def get(self, request, petId): ) return django_response - def post(self, request): + def post(self, request, petId): assert request.openapi assert not request.openapi.errors diff --git a/tests/integration/contrib/django/test_django_project.py b/tests/integration/contrib/django/test_django_project.py index a9c3b90c..43bb779f 100644 --- a/tests/integration/contrib/django/test_django_project.py +++ b/tests/integration/contrib/django/test_django_project.py @@ -412,10 +412,6 @@ def test_get_valid(self, client, data_gif): assert response.status_code == 200 assert b"".join(list(response.streaming_content)) == data_gif - @pytest.mark.xfail( - reason="request binary format not supported", - strict=True, - ) def test_post_valid(self, client, data_gif): client.cookies.load({"user": 1}) content_type = "image/gif" @@ -425,7 +421,7 @@ def test_post_valid(self, client, data_gif): "HTTP_API_KEY": self.api_key_encoded, } response = client.post( - "/v1/pets/12/photo", data_gif, content_type, secure=True, **headers + "/v1/pets/12/photo", data_gif, content_type, **headers ) assert response.status_code == 201 diff --git a/tests/integration/contrib/falcon/test_falcon_project.py b/tests/integration/contrib/falcon/test_falcon_project.py index 6984acbe..22fa7496 100644 --- a/tests/integration/contrib/falcon/test_falcon_project.py +++ b/tests/integration/contrib/falcon/test_falcon_project.py @@ -393,24 +393,23 @@ def test_get_valid(self, client, data_gif): assert response.status_code == 200 @pytest.mark.xfail( - reason="request binary format not supported", + reason="falcon request binary handler not implemented", strict=True, ) - def test_post_valid(self, client, data_json): + def test_post_valid(self, client, data_gif): cookies = {"user": 1} - content_type = "image/gif" + content_type = "image/jpeg" headers = { "Authorization": "Basic testuser", "Api-Key": self.api_key_encoded, "Content-Type": content_type, } - body = dumps(data_json) response = client.simulate_post( "/v1/pets/1/photo", host="petstore.swagger.io", headers=headers, - body=body, + body=data_gif, cookies=cookies, ) diff --git a/tests/integration/contrib/flask/data/v3.0/flaskproject/pets/views.py b/tests/integration/contrib/flask/data/v3.0/flaskproject/pets/views.py index 2cc15b7b..091b942e 100644 --- a/tests/integration/contrib/flask/data/v3.0/flaskproject/pets/views.py +++ b/tests/integration/contrib/flask/data/v3.0/flaskproject/pets/views.py @@ -1,6 +1,8 @@ from base64 import b64decode from io import BytesIO +from flask import Response +from flask import request from flask.helpers import send_file from openapi_core.contrib.flask.views import FlaskOpenAPIView @@ -23,4 +25,4 @@ def get(self, petId): def post(self, petId): data = request.stream.read() - response.status = HTTP_201 + return Response(status=201) diff --git a/tests/integration/contrib/flask/test_flask_project.py b/tests/integration/contrib/flask/test_flask_project.py index b90b06ae..e481fc1b 100644 --- a/tests/integration/contrib/flask/test_flask_project.py +++ b/tests/integration/contrib/flask/test_flask_project.py @@ -51,7 +51,7 @@ def test_get_valid(self, client, data_gif): "Api-Key": self.api_key_encoded, } - client.set_cookie("petstore.swagger.io", "user", "1") + client.set_cookie("user", "1", domain="petstore.swagger.io") response = client.get( "/v1/pets/1/photo", headers=headers, @@ -60,10 +60,6 @@ def test_get_valid(self, client, data_gif): assert response.get_data() == data_gif assert response.status_code == 200 - @pytest.mark.xfail( - reason="request binary format not supported", - strict=True, - ) def test_post_valid(self, client, data_gif): content_type = "image/gif" headers = { @@ -71,15 +67,12 @@ def test_post_valid(self, client, data_gif): "Api-Key": self.api_key_encoded, "Content-Type": content_type, } - data = { - "file": data_gif, - } - client.set_cookie("petstore.swagger.io", "user", "1") + client.set_cookie("user", "1", domain="petstore.swagger.io") response = client.post( "/v1/pets/1/photo", headers=headers, - data=data, + data=data_gif, ) assert not response.text diff --git a/tests/integration/contrib/requests/test_requests_validation.py b/tests/integration/contrib/requests/test_requests_validation.py index df2182b0..18e05fc8 100644 --- a/tests/integration/contrib/requests/test_requests_validation.py +++ b/tests/integration/contrib/requests/test_requests_validation.py @@ -198,10 +198,6 @@ def test_response_binary_valid(self, response_unmarshaller, data_gif): assert not result.errors assert result.data == data_gif - @pytest.mark.xfail( - reason="request binary format not supported", - strict=True, - ) @responses.activate def test_request_binary_valid(self, request_unmarshaller, data_gif): headers = { diff --git a/tests/integration/contrib/starlette/test_starlette_project.py b/tests/integration/contrib/starlette/test_starlette_project.py index c4783208..40779c73 100644 --- a/tests/integration/contrib/starlette/test_starlette_project.py +++ b/tests/integration/contrib/starlette/test_starlette_project.py @@ -58,10 +58,6 @@ def test_get_valid(self, client, data_gif): assert response.get_data() == data_gif assert response.status_code == 200 - @pytest.mark.xfail( - reason="request binary format not supported", - strict=True, - ) def test_post_valid(self, client, data_gif): content_type = "image/gif" headers = { diff --git a/tests/integration/test_petstore.py b/tests/integration/test_petstore.py index 2fa5441f..0c959dba 100644 --- a/tests/integration/test_petstore.py +++ b/tests/integration/test_petstore.py @@ -688,7 +688,7 @@ def test_post_birds(self, spec, spec_dict): "healthy": pet_healthy, }, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -770,7 +770,7 @@ def test_post_cats(self, spec, spec_dict): }, "extra": None, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -841,7 +841,7 @@ def test_post_cats_boolean_string(self, spec, spec_dict): "healthy": pet_healthy, }, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -915,7 +915,7 @@ def test_post_urlencoded(self, spec, spec_dict): "healthy": pet_healthy, }, } - data = urlencode(data_json) + data = urlencode(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -985,7 +985,7 @@ def test_post_no_one_of_schema(self, spec): "name": pet_name, "alias": alias, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -1037,7 +1037,7 @@ def test_post_cats_only_required_body(self, spec, spec_dict): "healthy": pet_healthy, }, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -1088,7 +1088,7 @@ def test_post_pets_raises_invalid_mimetype(self, spec): "name": "Cat", "tag": "cats", } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -1141,7 +1141,7 @@ def test_post_pets_missing_cookie(self, spec, spec_dict): "healthy": pet_healthy, }, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -1184,7 +1184,7 @@ def test_post_pets_missing_header(self, spec, spec_dict): "healthy": pet_healthy, }, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() cookies = { "user": "123", } @@ -1223,7 +1223,7 @@ def test_post_pets_raises_invalid_server_error(self, spec): "name": "Cat", "tag": "cats", } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": "12345", } @@ -1509,7 +1509,7 @@ def test_post_tags_extra_body_properties(self, spec): "name": pet_name, "alias": alias, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() request = MockRequest( host_url, @@ -1539,7 +1539,7 @@ def test_post_tags_empty_body(self, spec): host_url = "http://petstore.swagger.io/v1" path_pattern = "/v1/tags" data_json = {} - data = json.dumps(data_json) + data = json.dumps(data_json).encode() request = MockRequest( host_url, @@ -1569,7 +1569,7 @@ def test_post_tags_wrong_property_type(self, spec): host_url = "http://petstore.swagger.io/v1" path_pattern = "/v1/tags" tag_name = 123 - data = json.dumps(tag_name) + data = json.dumps(tag_name).encode() request = MockRequest( host_url, @@ -1602,7 +1602,7 @@ def test_post_tags_additional_properties(self, spec): data_json = { "name": pet_name, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() request = MockRequest( host_url, @@ -1658,7 +1658,7 @@ def test_post_tags_created_now(self, spec): "created": created, "name": pet_name, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() request = MockRequest( host_url, @@ -1715,7 +1715,7 @@ def test_post_tags_created_datetime(self, spec): "created": created, "name": pet_name, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() request = MockRequest( host_url, @@ -1787,7 +1787,7 @@ def test_post_tags_urlencoded(self, spec): "created": created, "name": pet_name, } - data = urlencode(data_json) + data = urlencode(data_json).encode() content_type = "application/x-www-form-urlencoded" request = MockRequest( @@ -1861,7 +1861,7 @@ def test_post_tags_created_invalid_type(self, spec): "created": created, "name": pet_name, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() request = MockRequest( host_url, @@ -1918,7 +1918,7 @@ def test_delete_tags_with_requestbody(self, spec): data_json = { "ids": ids, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() request = MockRequest( host_url, "DELETE", diff --git a/tests/integration/unmarshalling/test_read_only_write_only.py b/tests/integration/unmarshalling/test_read_only_write_only.py index 3a54636b..9cf2c9c2 100644 --- a/tests/integration/unmarshalling/test_read_only_write_only.py +++ b/tests/integration/unmarshalling/test_read_only_write_only.py @@ -37,7 +37,7 @@ def test_write_a_read_only_property(self, request_unmarshaller): "id": 10, "name": "Pedro", } - ) + ).encode() request = MockRequest( host_url="", method="POST", path="/users", data=data @@ -77,7 +77,7 @@ def test_write_only_property(self, request_unmarshaller): "name": "Pedro", "hidden": False, } - ) + ).encode() request = MockRequest( host_url="", method="POST", path="/users", data=data @@ -98,7 +98,7 @@ def test_read_a_write_only_property(self, response_unmarshaller): "name": "Pedro", "hidden": True, } - ) + ).encode() request = MockRequest(host_url="", method="POST", path="/users") response = MockResponse(data) diff --git a/tests/integration/unmarshalling/test_request_unmarshaller.py b/tests/integration/unmarshalling/test_request_unmarshaller.py index a09675e8..df774373 100644 --- a/tests/integration/unmarshalling/test_request_unmarshaller.py +++ b/tests/integration/unmarshalling/test_request_unmarshaller.py @@ -174,7 +174,7 @@ def test_missing_body(self, request_unmarshaller): ) def test_invalid_content_type(self, request_unmarshaller): - data = "csv,data" + data = b"csv,data" headers = { "api-key": self.api_key_encoded, } @@ -231,7 +231,7 @@ def test_invalid_complex_parameter(self, request_unmarshaller, spec_dict): "healthy": True, }, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -296,7 +296,7 @@ def test_post_pets(self, request_unmarshaller, spec_dict): "healthy": True, }, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "api-key": self.api_key_encoded, } @@ -338,7 +338,7 @@ def test_post_pets(self, request_unmarshaller, spec_dict): assert result.body.address.city == pet_city def test_post_pets_plain_no_schema(self, request_unmarshaller): - data = "plain text" + data = b"plain text" headers = { "api-key": self.api_key_encoded, } @@ -368,7 +368,7 @@ def test_post_pets_plain_no_schema(self, request_unmarshaller): }, ) assert result.security == {} - assert result.body == data + assert result.body == data.decode() def test_get_pet_unauthorized(self, request_unmarshaller): request = MockRequest( diff --git a/tests/integration/validation/test_request_validators.py b/tests/integration/validation/test_request_validators.py index 61ad611a..5cae21a9 100644 --- a/tests/integration/validation/test_request_validators.py +++ b/tests/integration/validation/test_request_validators.py @@ -89,7 +89,7 @@ def test_security_not_found(self, request_validator): ) def test_media_type_not_found(self, request_validator): - data = "csv,data" + data = b"csv,data" headers = { "api-key": self.api_key_encoded, } diff --git a/tests/unit/contrib/django/test_django.py b/tests/unit/contrib/django/test_django.py index be4735af..3c6df42d 100644 --- a/tests/unit/contrib/django/test_django.py +++ b/tests/unit/contrib/django/test_django.py @@ -83,7 +83,7 @@ def test_no_resolver(self, request_factory): assert openapi_request.host_url == request._current_scheme_host assert openapi_request.path == request.path assert openapi_request.path_pattern is None - assert openapi_request.body == "" + assert openapi_request.body == b"" assert openapi_request.content_type == request.content_type def test_simple(self, request_factory): @@ -104,7 +104,7 @@ def test_simple(self, request_factory): assert openapi_request.host_url == request._current_scheme_host assert openapi_request.path == request.path assert openapi_request.path_pattern == request.path - assert openapi_request.body == "" + assert openapi_request.body == b"" assert openapi_request.content_type == request.content_type def test_url_rule(self, request_factory): @@ -125,7 +125,7 @@ def test_url_rule(self, request_factory): assert openapi_request.host_url == request._current_scheme_host assert openapi_request.path == request.path assert openapi_request.path_pattern == "/admin/auth/group/{object_id}/" - assert openapi_request.body == "" + assert openapi_request.body == b"" assert openapi_request.content_type == request.content_type def test_url_regexp_pattern(self, request_factory): @@ -146,7 +146,7 @@ def test_url_regexp_pattern(self, request_factory): assert openapi_request.host_url == request._current_scheme_host assert openapi_request.path == request.path assert openapi_request.path_pattern == request.path - assert openapi_request.body == "" + assert openapi_request.body == b"" assert openapi_request.content_type == request.content_type def test_drf_default_value_pattern(self, request_factory): @@ -167,7 +167,7 @@ def test_drf_default_value_pattern(self, request_factory): assert openapi_request.host_url == request._current_scheme_host assert openapi_request.path == request.path assert openapi_request.path_pattern == "/object/{pk}/action/" - assert openapi_request.body == "" + assert openapi_request.body == b"" assert openapi_request.content_type == request.content_type diff --git a/tests/unit/contrib/flask/test_flask_requests.py b/tests/unit/contrib/flask/test_flask_requests.py index 3348ed62..63e51abf 100644 --- a/tests/unit/contrib/flask/test_flask_requests.py +++ b/tests/unit/contrib/flask/test_flask_requests.py @@ -31,7 +31,7 @@ def test_simple(self, request_factory, request): assert openapi_request.method == "get" assert openapi_request.host_url == request.host_url assert openapi_request.path == request.path - assert openapi_request.body == "" + assert openapi_request.body == b"" assert openapi_request.content_type == "application/octet-stream" def test_multiple_values(self, request_factory, request): @@ -59,7 +59,7 @@ def test_multiple_values(self, request_factory, request): assert openapi_request.method == "get" assert openapi_request.host_url == request.host_url assert openapi_request.path == request.path - assert openapi_request.body == "" + assert openapi_request.body == b"" assert openapi_request.content_type == "application/octet-stream" def test_url_rule(self, request_factory, request): @@ -81,5 +81,5 @@ def test_url_rule(self, request_factory, request): assert openapi_request.host_url == request.host_url assert openapi_request.path == request.path assert openapi_request.path_pattern == "/browse/{id}/" - assert openapi_request.body == "" + assert openapi_request.body == b"" assert openapi_request.content_type == "application/octet-stream" diff --git a/tests/unit/deserializing/test_media_types_deserializers.py b/tests/unit/deserializing/test_media_types_deserializers.py index 56ccb17f..5b8104a2 100644 --- a/tests/unit/deserializing/test_media_types_deserializers.py +++ b/tests/unit/deserializing/test_media_types_deserializers.py @@ -74,7 +74,7 @@ def test_plain_valid( def test_json_valid(self, deserializer_factory, mimetype): parameters = {"charset": "utf-8"} deserializer = deserializer_factory(mimetype, parameters=parameters) - value = '{"test": "test"}' + value = b'{"test": "test"}' result = deserializer.deserialize(value) @@ -90,7 +90,7 @@ def test_json_valid(self, deserializer_factory, mimetype): ) def test_json_empty(self, deserializer_factory, mimetype): deserializer = deserializer_factory(mimetype) - value = "" + value = b"" with pytest.raises(DeserializeError): deserializer.deserialize(value) @@ -104,7 +104,7 @@ def test_json_empty(self, deserializer_factory, mimetype): ) def test_json_empty_object(self, deserializer_factory, mimetype): deserializer = deserializer_factory(mimetype) - value = "{}" + value = b"{}" result = deserializer.deserialize(value) @@ -119,11 +119,26 @@ def test_json_empty_object(self, deserializer_factory, mimetype): ) def test_xml_empty(self, deserializer_factory, mimetype): deserializer = deserializer_factory(mimetype) - value = "" + value = b"" with pytest.raises(DeserializeError): deserializer.deserialize(value) + @pytest.mark.parametrize( + "mimetype", + [ + "application/xml", + "application/xhtml+xml", + ], + ) + def test_xml_default_charset_valid(self, deserializer_factory, mimetype): + deserializer = deserializer_factory(mimetype) + value = b"text" + + result = deserializer.deserialize(value) + + assert type(result) is Element + @pytest.mark.parametrize( "mimetype", [ @@ -134,7 +149,7 @@ def test_xml_empty(self, deserializer_factory, mimetype): def test_xml_valid(self, deserializer_factory, mimetype): parameters = {"charset": "utf-8"} deserializer = deserializer_factory(mimetype, parameters=parameters) - value = "text" + value = b"text" result = deserializer.deserialize(value) @@ -143,7 +158,7 @@ def test_xml_valid(self, deserializer_factory, mimetype): def test_octet_stream_empty(self, deserializer_factory): mimetype = "application/octet-stream" deserializer = deserializer_factory(mimetype) - value = "" + value = b"" result = deserializer.deserialize(value) @@ -180,7 +195,7 @@ def test_urlencoded_form_empty(self, deserializer_factory): schema_dict = {} schema = SchemaPath.from_dict(schema_dict) deserializer = deserializer_factory(mimetype, schema=schema) - value = "" + value = b"" result = deserializer.deserialize(value) @@ -206,7 +221,7 @@ def test_urlencoded_form_simple(self, deserializer_factory): deserializer = deserializer_factory( mimetype, schema=schema, encoding=encoding ) - value = "name=foo+bar" + value = b"name=foo+bar" result = deserializer.deserialize(value) @@ -229,7 +244,7 @@ def test_urlencoded_complex(self, deserializer_factory): } schema = SchemaPath.from_dict(schema_dict) deserializer = deserializer_factory(mimetype, schema=schema) - value = "prop=a&prop=b&prop=c" + value = b"prop=a&prop=b&prop=c" result = deserializer.deserialize(value) @@ -260,7 +275,7 @@ def test_urlencoded_content_type(self, deserializer_factory): deserializer = deserializer_factory( mimetype, schema=schema, encoding=encoding ) - value = 'prop=["a","b","c"]' + value = b'prop=["a","b","c"]' result = deserializer.deserialize(value) @@ -300,7 +315,7 @@ def test_urlencoded_deepobject(self, deserializer_factory): deserializer = deserializer_factory( mimetype, schema=schema, encoding=encoding ) - value = "color[R]=100&color[G]=200&color[B]=150" + value = b"color[R]=100&color[G]=200&color[B]=150" result = deserializer.deserialize(value) @@ -312,12 +327,12 @@ def test_urlencoded_deepobject(self, deserializer_factory): }, } - @pytest.mark.parametrize("value", [b"", ""]) - def test_multipart_form_empty(self, deserializer_factory, value): + def test_multipart_form_empty(self, deserializer_factory): mimetype = "multipart/form-data" schema_dict = {} schema = SchemaPath.from_dict(schema_dict) deserializer = deserializer_factory(mimetype, schema=schema) + value = b"" result = deserializer.deserialize(value) @@ -416,7 +431,7 @@ def custom_deserializer(value): custom_mimetype, extra_media_type_deserializers=extra_media_type_deserializers, ) - value = "{}" + value = b"{}" result = deserializer.deserialize( value, From 10fdbeadef1b3667c29656224117723cf71add05 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Wed, 1 Nov 2023 15:48:48 +0000 Subject: [PATCH 2/2] Response binary format support --- openapi_core/contrib/aiohttp/responses.py | 8 +++--- openapi_core/contrib/django/responses.py | 16 ++++++++--- openapi_core/contrib/falcon/responses.py | 13 +++++++-- openapi_core/contrib/requests/responses.py | 4 +-- openapi_core/contrib/starlette/responses.py | 19 ++++++++++--- openapi_core/contrib/werkzeug/responses.py | 10 +++++-- openapi_core/protocols.py | 2 +- openapi_core/testing/responses.py | 2 +- .../validation/response/validators.py | 16 ++++++++--- .../contrib/aiohttp/test_aiohttp_project.py | 4 --- .../contrib/django/test_django_project.py | 4 --- .../contrib/falcon/test_falcon_project.py | 4 --- .../contrib/flask/test_flask_project.py | 4 --- .../requests/test_requests_validation.py | 4 --- .../v3.0/starletteproject/pets/endpoints.py | 6 ++-- .../starlette/test_starlette_project.py | 6 +--- tests/integration/test_petstore.py | 28 +++++++++---------- .../test_read_only_write_only.py | 2 +- .../test_response_unmarshaller.py | 18 ++++++------ .../validation/test_response_validators.py | 20 ++++++------- tests/unit/contrib/django/test_django.py | 4 +-- .../contrib/flask/test_flask_responses.py | 2 +- tests/unit/contrib/requests/conftest.py | 2 +- .../requests/test_requests_responses.py | 2 +- 24 files changed, 109 insertions(+), 91 deletions(-) diff --git a/openapi_core/contrib/aiohttp/responses.py b/openapi_core/contrib/aiohttp/responses.py index 53a63698..ed337968 100644 --- a/openapi_core/contrib/aiohttp/responses.py +++ b/openapi_core/contrib/aiohttp/responses.py @@ -13,13 +13,13 @@ def __init__(self, response: web.Response): self.response = response @property - def data(self) -> str: + def data(self) -> bytes: if self.response.body is None: - return "" + return b"" if isinstance(self.response.body, bytes): - return self.response.body.decode("utf-8") + return self.response.body assert isinstance(self.response.body, str) - return self.response.body + return self.response.body.encode("utf-8") @property def status_code(self) -> int: diff --git a/openapi_core/contrib/django/responses.py b/openapi_core/contrib/django/responses.py index ded826f4..19a58943 100644 --- a/openapi_core/contrib/django/responses.py +++ b/openapi_core/contrib/django/responses.py @@ -1,20 +1,28 @@ """OpenAPI core contrib django responses module""" +from itertools import tee + from django.http.response import HttpResponse +from django.http.response import StreamingHttpResponse from werkzeug.datastructures import Headers class DjangoOpenAPIResponse: def __init__(self, response: HttpResponse): - if not isinstance(response, HttpResponse): + if not isinstance(response, (HttpResponse, StreamingHttpResponse)): raise TypeError( - f"'response' argument is not type of {HttpResponse}" + f"'response' argument is not type of {HttpResponse} or {StreamingHttpResponse}" ) self.response = response @property - def data(self) -> str: + def data(self) -> bytes: + if isinstance(self.response, StreamingHttpResponse): + resp_iter1, resp_iter2 = tee(self.response._iterator) + self.response.streaming_content = resp_iter1 + content = b"".join(map(self.response.make_bytes, resp_iter2)) + return content assert isinstance(self.response.content, bytes) - return self.response.content.decode("utf-8") + return self.response.content @property def status_code(self) -> int: diff --git a/openapi_core/contrib/falcon/responses.py b/openapi_core/contrib/falcon/responses.py index 9aaa015a..1eddb7c2 100644 --- a/openapi_core/contrib/falcon/responses.py +++ b/openapi_core/contrib/falcon/responses.py @@ -1,4 +1,6 @@ """OpenAPI core contrib falcon responses module""" +from itertools import tee + from falcon.response import Response from werkzeug.datastructures import Headers @@ -10,11 +12,16 @@ def __init__(self, response: Response): self.response = response @property - def data(self) -> str: + def data(self) -> bytes: if self.response.text is None: - return "" + if self.response.stream is None: + return b"" + resp_iter1, resp_iter2 = tee(self.response.stream) + self.response.stream = resp_iter1 + content = b"".join(resp_iter2) + return content assert isinstance(self.response.text, str) - return self.response.text + return self.response.text.encode("utf-8") @property def status_code(self) -> int: diff --git a/openapi_core/contrib/requests/responses.py b/openapi_core/contrib/requests/responses.py index be4d0650..b0dacc78 100644 --- a/openapi_core/contrib/requests/responses.py +++ b/openapi_core/contrib/requests/responses.py @@ -10,9 +10,9 @@ def __init__(self, response: Response): self.response = response @property - def data(self) -> str: + def data(self) -> bytes: assert isinstance(self.response.content, bytes) - return self.response.content.decode("utf-8") + return self.response.content @property def status_code(self) -> int: diff --git a/openapi_core/contrib/starlette/responses.py b/openapi_core/contrib/starlette/responses.py index 247f59a3..3070b6ec 100644 --- a/openapi_core/contrib/starlette/responses.py +++ b/openapi_core/contrib/starlette/responses.py @@ -1,20 +1,31 @@ """OpenAPI core contrib starlette responses module""" +from typing import Optional + from starlette.datastructures import Headers from starlette.responses import Response +from starlette.responses import StreamingResponse class StarletteOpenAPIResponse: - def __init__(self, response: Response): + def __init__(self, response: Response, data: Optional[bytes] = None): if not isinstance(response, Response): raise TypeError(f"'response' argument is not type of {Response}") self.response = response + if data is None and isinstance(response, StreamingResponse): + raise RuntimeError( + f"'data' argument is required for {StreamingResponse}" + ) + self._data = data + @property - def data(self) -> str: + def data(self) -> bytes: + if self._data is not None: + return self._data if isinstance(self.response.body, bytes): - return self.response.body.decode("utf-8") + return self.response.body assert isinstance(self.response.body, str) - return self.response.body + return self.response.body.encode("utf-8") @property def status_code(self) -> int: diff --git a/openapi_core/contrib/werkzeug/responses.py b/openapi_core/contrib/werkzeug/responses.py index 6b930c0b..e3d229f9 100644 --- a/openapi_core/contrib/werkzeug/responses.py +++ b/openapi_core/contrib/werkzeug/responses.py @@ -1,4 +1,6 @@ """OpenAPI core contrib werkzeug responses module""" +from itertools import tee + from werkzeug.datastructures import Headers from werkzeug.wrappers import Response @@ -10,8 +12,12 @@ def __init__(self, response: Response): self.response = response @property - def data(self) -> str: - return self.response.get_data(as_text=True) + def data(self) -> bytes: + if not self.response.is_sequence: + resp_iter1, resp_iter2 = tee(self.response.iter_encoded()) + self.response.response = resp_iter1 + return b"".join(resp_iter2) + return self.response.get_data(as_text=False) @property def status_code(self) -> int: diff --git a/openapi_core/protocols.py b/openapi_core/protocols.py index 771d1f27..82bf1532 100644 --- a/openapi_core/protocols.py +++ b/openapi_core/protocols.py @@ -120,7 +120,7 @@ class Response(Protocol): """ @property - def data(self) -> str: + def data(self) -> Optional[bytes]: ... @property diff --git a/openapi_core/testing/responses.py b/openapi_core/testing/responses.py index b957829b..ddd068a4 100644 --- a/openapi_core/testing/responses.py +++ b/openapi_core/testing/responses.py @@ -9,7 +9,7 @@ class MockResponse: def __init__( self, - data: str, + data: bytes, status_code: int = 200, headers: Optional[Dict[str, Any]] = None, content_type: str = "application/json", diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index c67de77b..02af9d46 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -5,6 +5,7 @@ from typing import Iterator from typing import List from typing import Mapping +from typing import Optional from jsonschema_path import SchemaPath from openapi_spec_validator import OpenAPIV30SpecValidator @@ -41,7 +42,7 @@ class BaseResponseValidator(BaseValidator): def _iter_errors( self, status_code: int, - data: str, + data: Optional[bytes], headers: Mapping[str, Any], mimetype: str, operation: SchemaPath, @@ -66,7 +67,11 @@ def _iter_errors( yield from exc.context def _iter_data_errors( - self, status_code: int, data: str, mimetype: str, operation: SchemaPath + self, + status_code: int, + data: Optional[bytes], + mimetype: str, + operation: SchemaPath, ) -> Iterator[Exception]: try: operation_response = self._find_operation_response( @@ -114,7 +119,10 @@ def _find_operation_response( @ValidationErrorWrapper(DataValidationError, InvalidData) def _get_data( - self, data: str, mimetype: str, operation_response: SchemaPath + self, + data: Optional[bytes], + mimetype: str, + operation_response: SchemaPath, ) -> Any: if "content" not in operation_response: return None @@ -125,7 +133,7 @@ def _get_data( value, _ = self._get_content_and_schema(raw_data, content, mimetype) return value - def _get_data_value(self, data: str) -> Any: + def _get_data_value(self, data: Optional[bytes]) -> bytes: if not data: raise MissingData diff --git a/tests/integration/contrib/aiohttp/test_aiohttp_project.py b/tests/integration/contrib/aiohttp/test_aiohttp_project.py index f7abfee3..9b1aee14 100644 --- a/tests/integration/contrib/aiohttp/test_aiohttp_project.py +++ b/tests/integration/contrib/aiohttp/test_aiohttp_project.py @@ -38,10 +38,6 @@ def api_key_encoded(self): class TestPetPhotoView(BaseTestPetstore): - @pytest.mark.xfail( - reason="response binary format not supported", - strict=True, - ) async def test_get_valid(self, client, data_gif): headers = { "Authorization": "Basic testuser", diff --git a/tests/integration/contrib/django/test_django_project.py b/tests/integration/contrib/django/test_django_project.py index 43bb779f..6614eeaf 100644 --- a/tests/integration/contrib/django/test_django_project.py +++ b/tests/integration/contrib/django/test_django_project.py @@ -398,10 +398,6 @@ def test_get_skip_response_validation(self, client): class TestPetPhotoView(BaseTestDjangoProject): - @pytest.mark.xfail( - reason="response binary format not supported", - strict=True, - ) def test_get_valid(self, client, data_gif): headers = { "HTTP_AUTHORIZATION": "Basic testuser", diff --git a/tests/integration/contrib/falcon/test_falcon_project.py b/tests/integration/contrib/falcon/test_falcon_project.py index 22fa7496..ca9bc066 100644 --- a/tests/integration/contrib/falcon/test_falcon_project.py +++ b/tests/integration/contrib/falcon/test_falcon_project.py @@ -371,10 +371,6 @@ def test_delete_method_invalid(self, client): class TestPetPhotoResource(BaseTestFalconProject): - @pytest.mark.xfail( - reason="response binary format not supported", - strict=True, - ) def test_get_valid(self, client, data_gif): cookies = {"user": 1} headers = { diff --git a/tests/integration/contrib/flask/test_flask_project.py b/tests/integration/contrib/flask/test_flask_project.py index e481fc1b..ddeb9320 100644 --- a/tests/integration/contrib/flask/test_flask_project.py +++ b/tests/integration/contrib/flask/test_flask_project.py @@ -41,10 +41,6 @@ def api_key_encoded(self): class TestPetPhotoView(BaseTestFlaskProject): - @pytest.mark.xfail( - reason="response binary format not supported", - strict=True, - ) def test_get_valid(self, client, data_gif): headers = { "Authorization": "Basic testuser", diff --git a/tests/integration/contrib/requests/test_requests_validation.py b/tests/integration/contrib/requests/test_requests_validation.py index 18e05fc8..54d98096 100644 --- a/tests/integration/contrib/requests/test_requests_validation.py +++ b/tests/integration/contrib/requests/test_requests_validation.py @@ -165,10 +165,6 @@ def request_unmarshaller(self, spec): def response_unmarshaller(self, spec): return V30ResponseUnmarshaller(spec) - @pytest.mark.xfail( - reason="response binary format not supported", - strict=True, - ) @responses.activate def test_response_binary_valid(self, response_unmarshaller, data_gif): responses.add( diff --git a/tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/endpoints.py b/tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/endpoints.py index eb0b2c76..99f88ceb 100644 --- a/tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/endpoints.py +++ b/tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/endpoints.py @@ -24,11 +24,13 @@ def pet_photo_endpoint(request): openapi_request = StarletteOpenAPIRequest(request) request_unmarshalled = unmarshal_request(openapi_request, spec=spec) if request.method == "GET": - response = StreamingResponse([OPENID_LOGO], media_type="image/gif") + contents = iter([OPENID_LOGO]) + response = StreamingResponse(contents, media_type="image/gif") + openapi_response = StarletteOpenAPIResponse(response, data=OPENID_LOGO) elif request.method == "POST": contents = request.body() response = Response(status_code=201) - openapi_response = StarletteOpenAPIResponse(response) + openapi_response = StarletteOpenAPIResponse(response) response_unmarshalled = unmarshal_response( openapi_request, openapi_response, spec=spec ) diff --git a/tests/integration/contrib/starlette/test_starlette_project.py b/tests/integration/contrib/starlette/test_starlette_project.py index 40779c73..f2cef304 100644 --- a/tests/integration/contrib/starlette/test_starlette_project.py +++ b/tests/integration/contrib/starlette/test_starlette_project.py @@ -38,10 +38,6 @@ def api_key_encoded(self): class TestPetPhotoView(BaseTestPetstore): - @pytest.mark.xfail( - reason="response binary format not supported", - strict=True, - ) def test_get_valid(self, client, data_gif): headers = { "Authorization": "Basic testuser", @@ -55,7 +51,7 @@ def test_get_valid(self, client, data_gif): cookies=cookies, ) - assert response.get_data() == data_gif + assert response.content == data_gif assert response.status_code == 200 def test_post_valid(self, client, data_gif): diff --git a/tests/integration/test_petstore.py b/tests/integration/test_petstore.py index 0c959dba..59650ad4 100644 --- a/tests/integration/test_petstore.py +++ b/tests/integration/test_petstore.py @@ -123,7 +123,7 @@ def test_get_pets(self, spec): data_json = { "data": [], } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() headers = { "Content-Type": "application/json", "x-next": "next-url", @@ -185,7 +185,7 @@ def test_get_pets_response(self, spec): } ], } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data) response_result = unmarshal_response(request, response, spec=spec) @@ -286,7 +286,7 @@ def test_get_pets_invalid_response(self, spec, response_unmarshaller): } ], } - response_data = json.dumps(response_data_json) + response_data = json.dumps(response_data_json).encode() response = MockResponse(response_data) with pytest.raises(InvalidData) as exc_info: @@ -349,7 +349,7 @@ def test_get_pets_ids_param(self, spec): data_json = { "data": [], } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data) response_result = unmarshal_response(request, response, spec=spec) @@ -398,7 +398,7 @@ def test_get_pets_tags_param(self, spec): data_json = { "data": [], } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data) response_result = unmarshal_response(request, response, spec=spec) @@ -1267,7 +1267,7 @@ def test_post_pets_raises_invalid_server_error(self, spec): }, }, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data) with pytest.raises(ServerNotFound): @@ -1362,7 +1362,7 @@ def test_get_pet(self, spec): }, }, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data) response_result = unmarshal_response(request, response, spec=spec) @@ -1413,7 +1413,7 @@ def test_get_pet_not_found(self, spec): "message": message, "rootCause": rootCause, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data, status_code=404) response_result = unmarshal_response(request, response, spec=spec) @@ -1492,7 +1492,7 @@ def test_get_tags(self, spec): assert result.body is None data_json = ["cats", "birds"] - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data) response_result = unmarshal_response(request, response, spec=spec) @@ -1637,7 +1637,7 @@ def test_post_tags_additional_properties(self, spec): "rootCause": rootCause, "additionalinfo": additionalinfo, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data, status_code=404) response_result = unmarshal_response(request, response, spec=spec) @@ -1694,7 +1694,7 @@ def test_post_tags_created_now(self, spec): "rootCause": "Tag already exist", "additionalinfo": "Tag Dog already exist", } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data, status_code=404) response_result = unmarshal_response(request, response, spec=spec) @@ -1753,7 +1753,7 @@ def test_post_tags_created_datetime(self, spec): "rootCause": rootCause, "additionalinfo": additionalinfo, } - response_data = json.dumps(response_data_json) + response_data = json.dumps(response_data_json).encode() response = MockResponse(response_data, status_code=404) result = unmarshal_response( @@ -1827,7 +1827,7 @@ def test_post_tags_urlencoded(self, spec): "rootCause": rootCause, "additionalinfo": additionalinfo, } - response_data = json.dumps(response_data_json) + response_data = json.dumps(response_data_json).encode() response = MockResponse(response_data, status_code=404) result = unmarshal_response( @@ -1898,7 +1898,7 @@ def test_post_tags_created_invalid_type(self, spec): "rootCause": rootCause, "additionalinfo": additionalinfo, } - data = json.dumps(data_json) + data = json.dumps(data_json).encode() response = MockResponse(data, status_code=404) response_result = unmarshal_response(request, response, spec=spec) diff --git a/tests/integration/unmarshalling/test_read_only_write_only.py b/tests/integration/unmarshalling/test_read_only_write_only.py index 9cf2c9c2..d8727cac 100644 --- a/tests/integration/unmarshalling/test_read_only_write_only.py +++ b/tests/integration/unmarshalling/test_read_only_write_only.py @@ -55,7 +55,7 @@ def test_read_only_property_response(self, response_unmarshaller): "id": 10, "name": "Pedro", } - ) + ).encode() request = MockRequest(host_url="", method="POST", path="/users") diff --git a/tests/integration/unmarshalling/test_response_unmarshaller.py b/tests/integration/unmarshalling/test_response_unmarshaller.py index cfec2ba3..515696a0 100644 --- a/tests/integration/unmarshalling/test_response_unmarshaller.py +++ b/tests/integration/unmarshalling/test_response_unmarshaller.py @@ -39,7 +39,7 @@ def response_unmarshaller(self, spec): def test_invalid_server(self, response_unmarshaller): request = MockRequest("http://petstore.invalid.net/v1", "get", "/") - response = MockResponse("Not Found", status_code=404) + response = MockResponse(b"Not Found", status_code=404) result = response_unmarshaller.unmarshal(request, response) @@ -50,7 +50,7 @@ def test_invalid_server(self, response_unmarshaller): def test_invalid_operation(self, response_unmarshaller): request = MockRequest(self.host_url, "patch", "/v1/pets") - response = MockResponse("Not Found", status_code=404) + response = MockResponse(b"Not Found", status_code=404) result = response_unmarshaller.unmarshal(request, response) @@ -61,7 +61,7 @@ def test_invalid_operation(self, response_unmarshaller): def test_invalid_response(self, response_unmarshaller): request = MockRequest(self.host_url, "get", "/v1/pets") - response = MockResponse("Not Found", status_code=409) + response = MockResponse(b"Not Found", status_code=409) result = response_unmarshaller.unmarshal(request, response) @@ -72,7 +72,7 @@ def test_invalid_response(self, response_unmarshaller): def test_invalid_content_type(self, response_unmarshaller): request = MockRequest(self.host_url, "get", "/v1/pets") - response = MockResponse("Not Found", content_type="text/csv") + response = MockResponse(b"Not Found", content_type="text/csv") result = response_unmarshaller.unmarshal(request, response) @@ -93,20 +93,20 @@ def test_missing_body(self, response_unmarshaller): def test_invalid_media_type(self, response_unmarshaller): request = MockRequest(self.host_url, "get", "/v1/pets") - response = MockResponse("abcde") + response = MockResponse(b"abcde") result = response_unmarshaller.unmarshal(request, response) assert result.errors == [DataValidationError()] assert result.errors[0].__cause__ == MediaTypeDeserializeError( - mimetype="application/json", value="abcde" + mimetype="application/json", value=b"abcde" ) assert result.data is None assert result.headers == {} def test_invalid_media_type_value(self, response_unmarshaller): request = MockRequest(self.host_url, "get", "/v1/pets") - response = MockResponse("{}") + response = MockResponse(b"{}") result = response_unmarshaller.unmarshal(request, response) @@ -154,7 +154,7 @@ def test_invalid_header(self, response_unmarshaller): }, ], } - response_data = json.dumps(response_json) + response_data = json.dumps(response_json).encode() headers = { "x-delete-confirm": "true", "x-delete-date": "today", @@ -181,7 +181,7 @@ def test_get_pets(self, response_unmarshaller): }, ], } - response_data = json.dumps(response_json) + response_data = json.dumps(response_json).encode() response = MockResponse(response_data) result = response_unmarshaller.unmarshal(request, response) diff --git a/tests/integration/validation/test_response_validators.py b/tests/integration/validation/test_response_validators.py index 260b2a72..807aa13e 100644 --- a/tests/integration/validation/test_response_validators.py +++ b/tests/integration/validation/test_response_validators.py @@ -40,28 +40,28 @@ def response_validator(self, spec): def test_invalid_server(self, response_validator): request = MockRequest("http://petstore.invalid.net/v1", "get", "/") - response = MockResponse("Not Found", status_code=404) + response = MockResponse(b"Not Found", status_code=404) with pytest.raises(PathNotFound): response_validator.validate(request, response) def test_invalid_operation(self, response_validator): request = MockRequest(self.host_url, "patch", "/v1/pets") - response = MockResponse("Not Found", status_code=404) + response = MockResponse(b"Not Found", status_code=404) with pytest.raises(OperationNotFound): response_validator.validate(request, response) def test_invalid_response(self, response_validator): request = MockRequest(self.host_url, "get", "/v1/pets") - response = MockResponse("Not Found", status_code=409) + response = MockResponse(b"Not Found", status_code=409) with pytest.raises(ResponseNotFound): response_validator.validate(request, response) def test_invalid_content_type(self, response_validator): request = MockRequest(self.host_url, "get", "/v1/pets") - response = MockResponse("Not Found", content_type="text/csv") + response = MockResponse(b"Not Found", content_type="text/csv") with pytest.raises(DataValidationError) as exc_info: response_validator.validate(request, response) @@ -77,18 +77,18 @@ def test_missing_body(self, response_validator): def test_invalid_media_type(self, response_validator): request = MockRequest(self.host_url, "get", "/v1/pets") - response = MockResponse("abcde") + response = MockResponse(b"abcde") with pytest.raises(DataValidationError) as exc_info: response_validator.validate(request, response) assert exc_info.value.__cause__ == MediaTypeDeserializeError( - mimetype="application/json", value="abcde" + mimetype="application/json", value=b"abcde" ) def test_invalid_media_type_value(self, response_validator): request = MockRequest(self.host_url, "get", "/v1/pets") - response = MockResponse("{}") + response = MockResponse(b"{}") with pytest.raises(DataValidationError) as exc_info: response_validator.validate(request, response) @@ -102,7 +102,7 @@ def test_invalid_value(self, response_validator): {"id": 1, "name": "Sparky"}, ], } - response_data = json.dumps(response_json) + response_data = json.dumps(response_json).encode() response = MockResponse(response_data) with pytest.raises(InvalidData) as exc_info: @@ -128,7 +128,7 @@ def test_invalid_header(self, response_validator): }, ], } - response_data = json.dumps(response_json) + response_data = json.dumps(response_json).encode() headers = { "x-delete-confirm": "true", "x-delete-date": "today", @@ -152,7 +152,7 @@ def test_valid(self, response_validator): }, ], } - response_data = json.dumps(response_json) + response_data = json.dumps(response_json).encode() response = MockResponse(response_data) result = response_validator.validate(request, response) diff --git a/tests/unit/contrib/django/test_django.py b/tests/unit/contrib/django/test_django.py index 3c6df42d..49621937 100644 --- a/tests/unit/contrib/django/test_django.py +++ b/tests/unit/contrib/django/test_django.py @@ -182,12 +182,12 @@ def test_stream_response(self, response_factory): openapi_response = DjangoOpenAPIResponse(response) - assert openapi_response.data == "foo\nbar\nbaz\n" + assert openapi_response.data == b"foo\nbar\nbaz\n" assert openapi_response.status_code == response.status_code assert openapi_response.content_type == response["Content-Type"] def test_redirect_response(self, response_factory): - data = "/redirected/" + data = b"/redirected/" response = response_factory(data, status_code=302) openapi_response = DjangoOpenAPIResponse(response) diff --git a/tests/unit/contrib/flask/test_flask_responses.py b/tests/unit/contrib/flask/test_flask_responses.py index 3741e5a8..c2b893ac 100644 --- a/tests/unit/contrib/flask/test_flask_responses.py +++ b/tests/unit/contrib/flask/test_flask_responses.py @@ -9,7 +9,7 @@ def test_type_invalid(self): FlaskOpenAPIResponse(None) def test_invalid_server(self, response_factory): - data = "Not Found" + data = b"Not Found" status_code = 404 response = response_factory(data, status_code=status_code) diff --git a/tests/unit/contrib/requests/conftest.py b/tests/unit/contrib/requests/conftest.py index 65b2c913..121b5149 100644 --- a/tests/unit/contrib/requests/conftest.py +++ b/tests/unit/contrib/requests/conftest.py @@ -37,7 +37,7 @@ def response_factory(): def create_response( data, status_code=200, content_type="application/json" ): - fp = BytesIO(bytes(data, "latin-1")) + fp = BytesIO(data) raw = HTTPResponse(fp, preload_content=False) resp = Response() resp.headers = CaseInsensitiveDict( diff --git a/tests/unit/contrib/requests/test_requests_responses.py b/tests/unit/contrib/requests/test_requests_responses.py index 6d515046..f032e658 100644 --- a/tests/unit/contrib/requests/test_requests_responses.py +++ b/tests/unit/contrib/requests/test_requests_responses.py @@ -9,7 +9,7 @@ def test_type_invalid(self): RequestsOpenAPIResponse(None) def test_invalid_server(self, response_factory): - data = "Not Found" + data = b"Not Found" status_code = 404 response = response_factory(data, status_code=status_code)