Skip to content

Commit

Permalink
Merge pull request #152 from Colin-b/bugfix/iter_content
Browse files Browse the repository at this point in the history
Allow to match on client provided async iterable content
  • Loading branch information
Colin-b authored Sep 22, 2024
2 parents 414fb36 + 6eddd05 commit d906a89
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 8 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Build status" src="https://github.com/Colin-b/pytest_httpx/workflows/Release/badge.svg"></a>
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-211 passed-blue"></a>
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-215 passed-blue"></a>
<a href="https://pypi.org/project/pytest-httpx/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/pytest_httpx"></a>
</p>

Expand Down
8 changes: 6 additions & 2 deletions pytest_httpx/_httpx_mock.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -173,6 +175,8 @@ async def _handle_async_request(
real_transport: httpx.AsyncHTTPTransport,
request: httpx.Request,
) -> httpx.Response:
# Store the content in request for future matching
await request.aread()
self._requests.append((real_transport, request))

callback = self._get_callback(real_transport, request)
Expand Down
5 changes: 3 additions & 2 deletions pytest_httpx/_request_matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pytest_httpx/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
61 changes: 60 additions & 1 deletion tests/test_httpx_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import math
import re
import time
from collections.abc import AsyncIterable

import httpx
import pytest
Expand Down Expand Up @@ -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

Expand All @@ -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"
)
57 changes: 57 additions & 0 deletions tests/test_httpx_sync.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from collections.abc import Iterable
from unittest.mock import ANY

import httpx
Expand Down Expand Up @@ -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"
)

0 comments on commit d906a89

Please sign in to comment.