diff --git a/tests/integration/cassettes/gzip_httpx_old_format.yaml b/tests/integration/cassettes/gzip_httpx_old_format.yaml new file mode 100644 index 0000000..76f238e --- /dev/null +++ b/tests/integration/cassettes/gzip_httpx_old_format.yaml @@ -0,0 +1,41 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate, br + connection: + - keep-alive + host: + - httpbin.org + user-agent: + - python-httpx/0.23.0 + method: GET + uri: https://httpbin.org/gzip + response: + content: "{\n \"gzipped\": true, \n \"headers\": {\n \"Accept\": \"*/*\", + \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\", + \n \"User-Agent\": \"python-httpx/0.23.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-62a62a8d-5f39b5c50c744da821d6ea99\"\n + \ }, \n \"method\": \"GET\", \n \"origin\": \"146.200.25.115\"\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '230' + Content-Type: + - application/json + Date: + - Sun, 12 Jun 2022 18:03:57 GMT + Server: + - gunicorn/19.9.0 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/integration/cassettes/gzip_requests.yaml b/tests/integration/cassettes/gzip_requests.yaml new file mode 100644 index 0000000..7b9b79e --- /dev/null +++ b/tests/integration/cassettes/gzip_requests.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.0 + method: GET + uri: https://httpbin.org/gzip + response: + body: + string: !!binary | + H4sIAKwrpmIA/z2OSwrCMBCG956izLIkfQSxkl2RogfQA9R2bIM1iUkqaOndnYDIrGa+/zELDB9l + LfYgg5uRwYhtj86DXKDuOrQBJKR5Cuy38kZ3pld6oHu0sqTH29QGZMnVkepgtMYuKKNJcEe0vJ3U + C4mcjI9hpaiygqaUW7ETFYGLR8frAXXE9h1Go7nD54w++FxkYp8VsDJ4IBH6E47NmVzGqUHFkn8g + rJsvp2omYs8AAAA= + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Origin: + - '*' + Connection: + - Close + Content-Encoding: + - gzip + Content-Length: + - '182' + Content-Type: + - application/json + Date: + - Sun, 12 Jun 2022 18:08:44 GMT + Server: + - Pytest-HTTPBIN/0.1.0 + status: + code: 200 + message: great +version: 1 diff --git a/tests/integration/test_httpx.py b/tests/integration/test_httpx.py index 723daed..5a03aa2 100644 --- a/tests/integration/test_httpx.py +++ b/tests/integration/test_httpx.py @@ -1,5 +1,5 @@ import pytest - +import os import vcr asyncio = pytest.importorskip("asyncio") @@ -218,8 +218,8 @@ def test_work_with_gzipped_data(httpbin, do_request, yml): with vcr.use_cassette(yml) as cassette: cassette_response = do_request(headers=headers)("GET", url) - assert cassette_response.headers["content-encoding"] == "gzip" - assert cassette_response.read() + # Show we can read the content of the cassette. + assert cassette_response.json()['gzipped'] == True assert cassette.play_count == 1 @@ -287,6 +287,34 @@ def test_stream(tmpdir, httpbin, do_request): assert cassette.play_count == 1 +# Regular cassette formats support the status reason, +# but the old HTTPX cassette format does not. +@pytest.mark.parametrize( + "cassette_name,reason", + [ + ("requests", "great"), + ("httpx_old_format", "OK"), + ], +) +def test_load_gzipped(do_request, cassette_name, reason): + mydir = os.path.dirname(os.path.realpath(__file__)) + yml = f"{mydir}/cassettes/gzip_{cassette_name}.yaml" + url = "https://httpbin.org/gzip" + + with vcr.use_cassette(yml) as cassette: + cassette_response = do_request()("GET", url) + assert str(cassette_response.request.url) == url + assert cassette.play_count == 1 + + # Should be able to load up the JSON inside, + # regardless whether the content is the gzipped + # in the cassette or not. + json = cassette_response.json() + assert json["method"] == "GET", json + assert cassette_response.status_code == 200 + assert cassette_response.reason_phrase == reason + + @pytest.mark.online def test_text_content_type(tmpdir, httpbin, do_request): url = httpbin.url + "/json" diff --git a/vcr/stubs/httpx_stubs.py b/vcr/stubs/httpx_stubs.py index 4d4b96a..34855c5 100644 --- a/vcr/stubs/httpx_stubs.py +++ b/vcr/stubs/httpx_stubs.py @@ -7,6 +7,8 @@ import httpx from vcr.errors import CannotOverwriteExistingCassetteException from vcr.request import Request as VcrRequest +from vcr.filters import decode_response +from vcr.serializers.compat import convert_body_to_bytes _httpx_signature = inspect.signature(httpx.Client.request) @@ -62,17 +64,30 @@ def _from_serialized_headers(headers): @patch("httpx.Response.close", MagicMock()) @patch("httpx.Response.read", MagicMock()) def _from_serialized_response(request, serialized_response, history=None): - content = serialized_response.get("content") - if isinstance(content, str): - content = content.encode("utf-8") + + # HTTPX cassette format. + if "status_code" in serialized_response: + serialized_response = decode_response(convert_body_to_bytes({ + 'headers': serialized_response['headers'], + 'body': {'string': serialized_response['content']}, + 'status': {'code': serialized_response['status_code']}, + })) + # We don't store the reason phrase in this format. + extensions = None + + # Cassette format that all other stubs use. + else: + extensions = {"reason_phrase": serialized_response["status"]["message"].encode()} + response = httpx.Response( - status_code=serialized_response.get("status_code"), + status_code=serialized_response["status"]["code"], request=request, - headers=_from_serialized_headers(serialized_response.get("headers")), - content=content, + headers=_from_serialized_headers(serialized_response["headers"]), + content=serialized_response["body"]["string"], history=history or [], + extensions=extensions, ) - response._content = content + return response