From cb820a2a2f2475fde62083f04412d3f3d710ce6f Mon Sep 17 00:00:00 2001 From: Colin-b Date: Sun, 22 Sep 2024 15:49:21 +0200 Subject: [PATCH 1/3] Allow to match on client provided async iterable content --- CHANGELOG.md | 7 +++- README.md | 2 +- pytest_httpx/_httpx_mock.py | 11 ++++-- pytest_httpx/_request_matcher.py | 5 +-- tests/test_httpx_async.py | 61 +++++++++++++++++++++++++++++++- tests/test_httpx_sync.py | 57 +++++++++++++++++++++++++++++ 6 files changed, 136 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 841a254..8f9f53a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.31.1] - 2024-09-22 +### Fixed +- It is now possible to match on content provided as async iterable by the client. + ## [0.31.0] - 2024-09-20 ### Changed - Tests will now fail at teardown by default if some requests were issued but were not matched. @@ -337,7 +341,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - First release, should be considered as unstable for now as design might change. -[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.0...HEAD +[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.1...HEAD +[0.31.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.0...v0.31.1 [0.31.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.30.0...v0.31.0 [0.30.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.29.0...v0.30.0 [0.29.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.28.0...v0.29.0 diff --git a/README.md b/README.md index 3095b3b..4c73816 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Build status Coverage Code style: black -Number of tests +Number of tests Number of downloads

diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 33ac08b..9e17d4c 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -1,8 +1,8 @@ import copy import inspect from operator import methodcaller -from typing import Union, Optional, Callable, Any, NoReturn -from collections.abc import Awaitable +from typing import Union, Optional, Callable, Any, NoReturn, AsyncIterable +from collections.abc import Awaitable, Iterable import httpx from pytest import Mark @@ -157,6 +157,8 @@ def _handle_request( real_transport: httpx.HTTPTransport, request: httpx.Request, ) -> httpx.Response: + # Store the content in request for future matching + request.read() self._requests.append((real_transport, request)) callback = self._get_callback(real_transport, request) @@ -173,6 +175,11 @@ async def _handle_async_request( real_transport: httpx.AsyncHTTPTransport, request: httpx.Request, ) -> httpx.Response: + # Store the content in request for future matching + if isinstance(request.stream, AsyncIterable): + await request.aread() + else: + request.read() self._requests.append((real_transport, request)) callback = self._get_callback(real_transport, request) diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index 04c4ab4..1143ca3 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -98,11 +98,12 @@ def _headers_match(self, request: httpx.Request) -> bool: def _content_match(self, request: httpx.Request) -> bool: if self.content is None and self.json is None: return True + if self.content is not None: - return request.read() == self.content + return request.content == self.content try: # httpx._content.encode_json hard codes utf-8 encoding. - return json.loads(request.read().decode("utf-8")) == self.json + return json.loads(request.content.decode("utf-8")) == self.json except json.decoder.JSONDecodeError: return False diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index 1d5d721..ee3cc61 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -2,6 +2,7 @@ import math import re import time +from collections.abc import AsyncIterable import httpx import pytest @@ -2007,7 +2008,7 @@ async def test_streams_are_not_cascading_resulting_in_maximum_recursion( ) -> None: httpx_mock.add_response(json={"abc": "def"}) async with httpx.AsyncClient() as client: - tasks = [client.get("https://example.com/") for _ in range(950)] + tasks = [client.get("https://test_url") for _ in range(950)] await asyncio.gather(*tasks) # No need to assert anything, this test case ensure that no error was raised by the gather @@ -2033,3 +2034,61 @@ async def handle_async_request( response = await client.post("https://test_url", content=b"This is the body") assert response.read() == b"" assert response.headers["x-prefix"] == "test" + + +@pytest.mark.asyncio +async def test_response_selection_content_matching_with_async_iterable( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response(match_content=b"full content 1", content=b"matched 1") + httpx_mock.add_response(match_content=b"full content 2", content=b"matched 2") + + async def stream_content_1() -> AsyncIterable[bytes]: + yield b"full" + yield b" " + yield b"content" + yield b" 1" + + async def stream_content_2() -> AsyncIterable[bytes]: + yield b"full" + yield b" " + yield b"content" + yield b" 2" + + async with httpx.AsyncClient() as client: + response_2 = await client.put("https://test_url", content=stream_content_2()) + response_1 = await client.put("https://test_url", content=stream_content_1()) + assert response_1.content == b"matched 1" + assert response_2.content == b"matched 2" + + +@pytest.mark.asyncio +async def test_request_selection_content_matching_with_async_iterable( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response(match_content=b"full content 1") + httpx_mock.add_response(match_content=b"full content 2") + + async def stream_content_1() -> AsyncIterable[bytes]: + yield b"full" + yield b" " + yield b"content" + yield b" 1" + + async def stream_content_2() -> AsyncIterable[bytes]: + yield b"full" + yield b" " + yield b"content" + yield b" 2" + + async with httpx.AsyncClient() as client: + await client.put("https://test_url_2", content=stream_content_2()) + await client.put("https://test_url_1", content=stream_content_1()) + assert ( + httpx_mock.get_request(match_content=b"full content 1").url + == "https://test_url_1" + ) + assert ( + httpx_mock.get_request(match_content=b"full content 2").url + == "https://test_url_2" + ) diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py index 3cbfb3f..a4c6047 100644 --- a/tests/test_httpx_sync.py +++ b/tests/test_httpx_sync.py @@ -1,4 +1,5 @@ import re +from collections.abc import Iterable from unittest.mock import ANY import httpx @@ -1718,3 +1719,59 @@ def handle_request( response = client.post("https://test_url", content=b"This is the body") assert response.read() == b"" assert response.headers["x-prefix"] == "test" + + +def test_response_selection_content_matching_with_iterable( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response(match_content=b"full content 1", content=b"matched 1") + httpx_mock.add_response(match_content=b"full content 2", content=b"matched 2") + + def stream_content_1() -> Iterable[bytes]: + yield b"full" + yield b" " + yield b"content" + yield b" 1" + + def stream_content_2() -> Iterable[bytes]: + yield b"full" + yield b" " + yield b"content" + yield b" 2" + + with httpx.Client() as client: + response_2 = client.put("https://test_url", content=stream_content_2()) + response_1 = client.put("https://test_url", content=stream_content_1()) + assert response_1.content == b"matched 1" + assert response_2.content == b"matched 2" + + +def test_request_selection_content_matching_with_iterable( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response(match_content=b"full content 1") + httpx_mock.add_response(match_content=b"full content 2") + + def stream_content_1() -> Iterable[bytes]: + yield b"full" + yield b" " + yield b"content" + yield b" 1" + + def stream_content_2() -> Iterable[bytes]: + yield b"full" + yield b" " + yield b"content" + yield b" 2" + + with httpx.Client() as client: + client.put("https://test_url_2", content=stream_content_2()) + client.put("https://test_url_1", content=stream_content_1()) + assert ( + httpx_mock.get_request(match_content=b"full content 1").url + == "https://test_url_1" + ) + assert ( + httpx_mock.get_request(match_content=b"full content 2").url + == "https://test_url_2" + ) From 29eb9c31bdb83b7d7c0ea9a4434dd71df8586c1e Mon Sep 17 00:00:00 2001 From: Colin-b Date: Sun, 22 Sep 2024 15:49:31 +0200 Subject: [PATCH 2/3] Bump version --- pytest_httpx/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_httpx/version.py b/pytest_httpx/version.py index 63fd052..91963a7 100644 --- a/pytest_httpx/version.py +++ b/pytest_httpx/version.py @@ -3,4 +3,4 @@ # Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0) # Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0) # Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9) -__version__ = "0.31.0" +__version__ = "0.31.1" From 6eddd0565d4a592e58313376fb48352bbef66d45 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Sun, 22 Sep 2024 15:51:09 +0200 Subject: [PATCH 3/3] Remove dead code --- pytest_httpx/_httpx_mock.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 9e17d4c..9465418 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -176,10 +176,7 @@ async def _handle_async_request( request: httpx.Request, ) -> httpx.Response: # Store the content in request for future matching - if isinstance(request.stream, AsyncIterable): - await request.aread() - else: - request.read() + await request.aread() self._requests.append((real_transport, request)) callback = self._get_callback(real_transport, request)