From 037bd9e68802ce4ac41be4b8b4b56b95f9f790c1 Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 00:20:43 +0800
Subject: [PATCH 01/23] Added the match_json matcher
Added the match_json matcher which checks if the passed object matches the json decoded request body
---
pytest_httpx/_httpx_mock.py | 25 ++++++++++++++++++++-
tests/test_httpx_async.py | 43 +++++++++++++++++++++++++++++++++++++
2 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index ca11510..301de70 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -1,5 +1,6 @@
import copy
import inspect
+import json
import re
from typing import List, Union, Optional, Callable, Tuple, Pattern, Any, Dict, Awaitable
@@ -15,12 +16,14 @@ def __init__(
method: Optional[str] = None,
match_headers: Optional[Dict[str, Any]] = None,
match_content: Optional[bytes] = None,
+ match_json_content: Optional[Union[dict[str, Any], str, float, int]] = None,
):
self.nb_calls = 0
self.url = httpx.URL(url) if url and isinstance(url, str) else url
self.method = method.upper() if method else method
self.headers = match_headers
self.content = match_content
+ self.json_content = match_json_content
def match(self, request: httpx.Request) -> bool:
return (
@@ -28,6 +31,7 @@ def match(self, request: httpx.Request) -> bool:
and self._method_match(request)
and self._headers_match(request)
and self._content_match(request)
+ and self._json_content_match(request)
)
def _url_match(self, request: httpx.Request) -> bool:
@@ -62,6 +66,15 @@ def _headers_match(self, request: httpx.Request) -> bool:
for header_name, header_value in self.headers.items()
)
+ def _json_content_match(self, request: httpx.Request) -> bool:
+ if self.json_content is None:
+ return True
+ try:
+ # httpx._content.encode_json hard codes utf-8 encoding.
+ return json.loads(request.read().decode("utf-8")) == self.json_content
+ except json.decoder.JSONDecodeError:
+ return False
+
def _content_match(self, request: httpx.Request) -> bool:
if self.content is None:
return True
@@ -76,8 +89,12 @@ def __str__(self) -> str:
matcher_description += f" with {self.headers} headers"
if self.content is not None:
matcher_description += f" and {self.content} body"
+ if self.json_content is not None:
+ matcher_description += f" and {self.json_content} json body"
elif self.content is not None:
matcher_description += f" with {self.content} body"
+ elif self.json_content is not None:
+ matcher_description += f" with {self.json_content} json body"
return matcher_description
@@ -100,7 +117,7 @@ def add_response(
self,
status_code: int = 200,
http_version: str = "HTTP/1.1",
- headers: _httpx_internals.HeaderTypes = None,
+ headers: Optional[_httpx_internals.HeaderTypes] = None,
content: Optional[bytes] = None,
text: Optional[str] = None,
html: Optional[str] = None,
@@ -124,6 +141,7 @@ def add_response(
:param method: HTTP method identifying the request(s) to match.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
+ :param match_json_content: Any object that should match json.loads(request.body.decode(encoding))
"""
json = copy.deepcopy(json) if json is not None else None
@@ -160,6 +178,7 @@ def add_callback(
:param method: HTTP method identifying the request(s) to match.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
+ :param match_json_content: Any object that should match json.loads(request.body.decode(encoding))
"""
self._callbacks.append((_RequestMatcher(**matchers), callback))
@@ -173,6 +192,7 @@ def add_exception(self, exception: Exception, **matchers) -> None:
:param method: HTTP method identifying the request(s) to match.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
+ :param match_json_content: Any object that should match json.loads(request.body.decode(encoding))
"""
def exception_callback(request: httpx.Request) -> None:
@@ -229,6 +249,7 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
]
)
expect_body = any([matcher.content is not None for matcher in matchers])
+ expect_json = any([matcher.json_content is not None for matcher in matchers])
request_description = f"{request.method} request on {request.url}"
if expect_headers:
@@ -237,6 +258,8 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
request_description += f" and {request.read()} body"
elif expect_body:
request_description += f" with {request.read()} body"
+ elif expect_json:
+ request_description += f" with {request.read().decode('utf-8')} body"
matchers_description = "\n".join([str(matcher) for matcher in matchers])
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index bf61950..df7fc0b 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1142,6 +1142,49 @@ async def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.reset(assert_all_responses_were_requested=False)
+@pytest.mark.asyncio
+async def test_json_content_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+
+ async with httpx.AsyncClient() as client:
+ response = await client.post("https://test_url", json={"b": 2, "a": 1})
+ assert response.read() == b""
+
+
+@pytest.mark.asyncio
+async def test_json_content_not_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+
+ async with httpx.AsyncClient() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
+ assert (
+ str(exception_info.value)
+ == """No response can be found for POST request on https://test_url with {"c": 3, "b": 2, "a": 1} body amongst:
+Match all requests with {'a': 1, 'b': 2} json body"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
+@pytest.mark.asyncio
+async def test_json_content_not_matching_invalid_json(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+
+ async with httpx.AsyncClient() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ await client.post("https://test_url", content=b"foobar")
+ assert (
+ str(exception_info.value)
+ == """No response can be found for POST request on https://test_url with foobar body amongst:
+Match all requests with {'a': 1, 'b': 2} json body"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
@pytest.mark.asyncio
async def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
From b95192d462482f5cdc39cc47badb09b697eb624b Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 00:29:32 +0800
Subject: [PATCH 02/23] Add docs
---
README.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/README.md b/README.md
index 11cd0d0..951e24d 100644
--- a/README.md
+++ b/README.md
@@ -182,6 +182,24 @@ def test_content_matching(httpx_mock: HTTPXMock):
response = client.post("https://test_url", content=b"This is the body")
```
+#### Matching on HTTP json body
+
+Use `match_json_content` parameter to specify the json that will be matched with the json decoded HTTP body to reply to.
+
+Maching is performed on equality after json decoding the body.
+
+```python
+import httpx
+from pytest_httpx import HTTPXMock
+
+def test_json_matching(httpx_mock: HTTPXMock):
+ httpx_mock.add_response(match_json_content={"a": "json", "b": 2})
+
+ with httpx.Client() as client:
+ response = client.post("https://test_url", json={"a": "json", "b": 2})
+```
+
+
### Add JSON response
Use `json` parameter to add a JSON response using python values.
From 9ba035c8602d4b2d1ed9b5c74d68669730af7400 Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 00:37:34 +0800
Subject: [PATCH 03/23] Added sync tests
---
tests/test_httpx_sync.py | 42 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index 233b206..32c7c4b 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -1,4 +1,5 @@
import re
+from typing import Any
import httpx
import pytest
@@ -895,6 +896,47 @@ def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.reset(assert_all_responses_were_requested=False)
+@pytest.mark.parametrize("json", [{"a": 1, "b": 2}, "somestring", "25", 25.3])
+def test_json_content_matching(json: Any, httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json_content=json)
+
+ with httpx.Client() as client:
+ response = client.post("https://test_url", json=json)
+ assert response.read() == b""
+
+
+def test_json_content_not_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+
+ with httpx.Client() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
+ assert (
+ str(exception_info.value)
+ == """No response can be found for POST request on https://test_url with {"c": 3, "b": 2, "a": 1} body amongst:
+Match all requests with {'a': 1, 'b': 2} json body"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
+def test_json_content_not_matching_invalid_json(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+
+ with httpx.Client() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ client.post("https://test_url", content=b"foobar")
+ assert (
+ str(exception_info.value)
+ == """No response can be found for POST request on https://test_url with foobar body amongst:
+Match all requests with {'a': 1, 'b': 2} json body"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={"user-agent": f"python-httpx/{httpx.__version__}"},
From 044daa4b371f1d5fb25bca9cf24b0cba5aa13a8e Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 01:05:13 +0800
Subject: [PATCH 04/23] Improved error message when combined match_headers and
match_json_content are specified
---
pytest_httpx/_httpx_mock.py | 2 ++
tests/test_httpx_async.py | 20 ++++++++++++++++++++
2 files changed, 22 insertions(+)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 301de70..7808be2 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -256,6 +256,8 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
request_description += f" with {dict({name: value for name, value in request.headers.items() if name in expect_headers})} headers"
if expect_body:
request_description += f" and {request.read()} body"
+ elif expect_json:
+ request_description += f" and {request.read().decode('utf-8')} body"
elif expect_body:
request_description += f" with {request.read()} body"
elif expect_json:
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index df7fc0b..55287f1 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1168,6 +1168,26 @@ async def test_json_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.reset(assert_all_responses_were_requested=False)
+@pytest.mark.asyncio
+async def test_headers_and_json_content_not_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(
+ match_json_content={"a": 1, "b": 2},
+ match_headers={"foo": "bar"},
+ )
+
+ async with httpx.AsyncClient() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
+ assert (
+ str(exception_info.value)
+ == """No response can be found for POST request on https://test_url with {} headers and {"c": 3, "b": 2, "a": 1} body amongst:
+Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
@pytest.mark.asyncio
async def test_json_content_not_matching_invalid_json(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
From fc564b256d00873c02994581676bcffc844a7225 Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 01:12:46 +0800
Subject: [PATCH 05/23] Broaden the typehint of match_json_content. It doesn't
really matter what is passed there since it is just compared to the json
decoded body
---
pytest_httpx/_httpx_mock.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 7808be2..83a88e6 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -16,7 +16,7 @@ def __init__(
method: Optional[str] = None,
match_headers: Optional[Dict[str, Any]] = None,
match_content: Optional[bytes] = None,
- match_json_content: Optional[Union[dict[str, Any], str, float, int]] = None,
+ match_json_content: Optional[Any] = None,
):
self.nb_calls = 0
self.url = httpx.URL(url) if url and isinstance(url, str) else url
From d74210cb0d847ab598c457797e7f426da5945c9f Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 01:21:25 +0800
Subject: [PATCH 06/23] Added the feature to CHANGELOG.md
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05d3cef..29c6f48 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+- Added `match_json_content` which allows matching a json decoded body against an arbitrary python object for equality.
## [0.23.1] - 2023-08-02
### Fixed
From 25022372e86f2ad7a393525f0e03da5df0f1038c Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 12:57:12 +0800
Subject: [PATCH 07/23] Add Any type to **matchers argument to statisfy strict
type checking
---
pytest_httpx/_httpx_mock.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index ca11510..01d4f97 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -106,7 +106,7 @@ def add_response(
html: Optional[str] = None,
stream: Any = None,
json: Any = None,
- **matchers,
+ **matchers: Any,
) -> None:
"""
Mock the response that will be sent if a request match.
@@ -148,7 +148,7 @@ def add_callback(
[httpx.Request],
Union[Optional[httpx.Response], Awaitable[Optional[httpx.Response]]],
],
- **matchers,
+ **matchers: Any,
) -> None:
"""
Mock the action that will take place if a request match.
@@ -163,7 +163,7 @@ def add_callback(
"""
self._callbacks.append((_RequestMatcher(**matchers), callback))
- def add_exception(self, exception: Exception, **matchers) -> None:
+ def add_exception(self, exception: Exception, **matchers: Any) -> None:
"""
Raise an exception if a request match.
@@ -275,7 +275,7 @@ def _get_callback(
matcher.nb_calls += 1
return callback
- def get_requests(self, **matchers) -> List[httpx.Request]:
+ def get_requests(self, **matchers: Any) -> List[httpx.Request]:
"""
Return all requests sent that match (empty list if no requests were matched).
@@ -288,7 +288,7 @@ def get_requests(self, **matchers) -> List[httpx.Request]:
matcher = _RequestMatcher(**matchers)
return [request for request in self._requests if matcher.match(request)]
- def get_request(self, **matchers) -> Optional[httpx.Request]:
+ def get_request(self, **matchers: Any) -> Optional[httpx.Request]:
"""
Return the single request that match (or None).
From 9786c0e0e526c340093e60c65543e20edf7161d6 Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 13:11:40 +0800
Subject: [PATCH 08/23] Add changelog
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05d3cef..a625d1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Changed
+- Add `:Any` type hint to `**matchers` function arguments to satisfy strict type checking mode in pyright
## [0.23.1] - 2023-08-02
### Fixed
From 0d32e1e261c61ad71c1d135ddce4580ec468cc53 Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 14:56:42 +0800
Subject: [PATCH 09/23] Make sure the match_headers errors are displayed
correctly even if the header names are turned into lower case by httpx
---
CHANGELOG.md | 3 +++
pytest_httpx/_httpx_mock.py | 7 ++++++-
tests/test_httpx_async.py | 22 ++++++++++++++++++++++
tests/test_httpx_sync.py | 21 +++++++++++++++++++++
4 files changed, 52 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05d3cef..f15b8f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Fixed
+- If httpx lower cases a header name (like authorization) make sure the header is still displayed in the error if another parameter doesn't match
+
## [0.23.1] - 2023-08-02
### Fixed
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index ca11510..a3942bd 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -232,7 +232,12 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
request_description = f"{request.method} request on {request.url}"
if expect_headers:
- request_description += f" with {dict({name: value for name, value in request.headers.items() if name in expect_headers})} headers"
+ present_headers = dict(
+ (name, request.headers.get(name))
+ for name in expect_headers
+ if request.headers.get(name) is not None
+ )
+ request_description += f" with {present_headers} headers"
if expect_body:
request_description += f" and {request.read()} body"
elif expect_body:
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index bf61950..ecdc079 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1116,6 +1116,28 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.reset(assert_all_responses_were_requested=False)
+@pytest.mark.asyncio
+async def test_url_not_matching_authorizaiton_headers_matching(httpx_mock: HTTPXMock):
+ httpx_mock.add_response(
+ method="GET",
+ url="http://test_url?q=b",
+ match_headers={"Authorization": "Bearer: Something"},
+ )
+ async with httpx.AsyncClient() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ await client.get(
+ "http://test_url", headers={"Authorization": "Bearer: Something"}
+ )
+ assert (
+ str(exception_info.value)
+ == """No response can be found for GET request on http://test_url with {'Authorization': 'Bearer: Something'} headers amongst:
+Match GET requests on http://test_url?q=b with {'Authorization': 'Bearer: Something'} headers"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
@pytest.mark.asyncio
async def test_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_content=b"This is the body")
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index 233b206..57de781 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -139,6 +139,27 @@ def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None:
assert response.text == "test content"
+def test_url_not_matching_authorizaiton_headers_matching(httpx_mock: HTTPXMock):
+ httpx_mock.add_response(
+ method="GET",
+ url="http://test_url?q=b",
+ match_headers={"Authorization": "Bearer: Something"},
+ )
+ with httpx.Client() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ client.get(
+ "http://test_url", headers={"Authorization": "Bearer: Something"}
+ )
+ assert (
+ str(exception_info.value)
+ == """No response can be found for GET request on http://test_url with {'Authorization': 'Bearer: Something'} headers amongst:
+Match GET requests on http://test_url?q=b with {'Authorization': 'Bearer: Something'} headers"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
From 9b2f974071cc13958102742ddb5036742bd891da Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Fri, 1 Sep 2023 15:04:48 +0800
Subject: [PATCH 10/23] Make sure we don't trip up Sonar Cloud
---
tests/test_httpx_async.py | 14 ++++++--------
tests/test_httpx_sync.py | 14 ++++++--------
2 files changed, 12 insertions(+), 16 deletions(-)
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index ecdc079..c6fecd7 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1117,21 +1117,19 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_url_not_matching_authorizaiton_headers_matching(httpx_mock: HTTPXMock):
+async def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(
method="GET",
- url="http://test_url?q=b",
- match_headers={"Authorization": "Bearer: Something"},
+ url="https://test_url?q=b",
+ match_headers={"MyHeader": "Something"},
)
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
- await client.get(
- "http://test_url", headers={"Authorization": "Bearer: Something"}
- )
+ await client.get("https://test_url", headers={"MyHeader": "Something"})
assert (
str(exception_info.value)
- == """No response can be found for GET request on http://test_url with {'Authorization': 'Bearer: Something'} headers amongst:
-Match GET requests on http://test_url?q=b with {'Authorization': 'Bearer: Something'} headers"""
+ == """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
+Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
# Clean up responses to avoid assertion failure
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index 57de781..0e68d6e 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -139,21 +139,19 @@ def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None:
assert response.text == "test content"
-def test_url_not_matching_authorizaiton_headers_matching(httpx_mock: HTTPXMock):
+def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(
method="GET",
- url="http://test_url?q=b",
- match_headers={"Authorization": "Bearer: Something"},
+ url="https://test_url?q=b",
+ match_headers={"MyHeader": "Something"},
)
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
- client.get(
- "http://test_url", headers={"Authorization": "Bearer: Something"}
- )
+ client.get("https://test_url", headers={"MyHeader": "Something"})
assert (
str(exception_info.value)
- == """No response can be found for GET request on http://test_url with {'Authorization': 'Bearer: Something'} headers amongst:
-Match GET requests on http://test_url?q=b with {'Authorization': 'Bearer: Something'} headers"""
+ == """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
+Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
# Clean up responses to avoid assertion failure
From 4ef937f05d6655428244306d7c84934d6bd0e48d Mon Sep 17 00:00:00 2001
From: Colin Bounouar
Date: Sat, 2 Sep 2023 23:21:39 +0200
Subject: [PATCH 11/23] Update CHANGELOG.md
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a625d1a..c8d6714 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
-### Changed
+### Fixed
- Add `:Any` type hint to `**matchers` function arguments to satisfy strict type checking mode in pyright
## [0.23.1] - 2023-08-02
From 65d9ef77a3ab732d96a9bda3eb5aaa351ab9d0ba Mon Sep 17 00:00:00 2001
From: Colin Bounouar
Date: Sun, 3 Sep 2023 13:30:44 +0200
Subject: [PATCH 12/23] Update tests/test_httpx_sync.py
---
tests/test_httpx_sync.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index 0e68d6e..42f9e43 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -139,7 +139,7 @@ def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None:
assert response.text == "test content"
-def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock):
+def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="GET",
url="https://test_url?q=b",
From 2796f54a5b6324f803d78760833a12fdba93113f Mon Sep 17 00:00:00 2001
From: Colin Bounouar
Date: Sun, 3 Sep 2023 13:30:49 +0200
Subject: [PATCH 13/23] Update tests/test_httpx_async.py
---
tests/test_httpx_async.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index c6fecd7..a028bd6 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1117,7 +1117,7 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock):
+async def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="GET",
url="https://test_url?q=b",
From 40a707eb625b27957d9c4f4a650fa2893af7e84e Mon Sep 17 00:00:00 2001
From: Colin Bounouar
Date: Sun, 3 Sep 2023 13:30:56 +0200
Subject: [PATCH 14/23] Update pytest_httpx/_httpx_mock.py
---
pytest_httpx/_httpx_mock.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 7d38317..07f1196 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -232,11 +232,11 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
request_description = f"{request.method} request on {request.url}"
if expect_headers:
- present_headers = dict(
- (name, request.headers.get(name))
+ present_headers = {
+ name: request.headers.get(name)
for name in expect_headers
if request.headers.get(name) is not None
- )
+ }
request_description += f" with {present_headers} headers"
if expect_body:
request_description += f" and {request.read()} body"
From 56a8a9530e48e9b604c57689cdcca6de6627801a Mon Sep 17 00:00:00 2001
From: Colin Bounouar
Date: Sun, 3 Sep 2023 13:31:02 +0200
Subject: [PATCH 15/23] Update CHANGELOG.md
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b3aa96..ef3fc10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
-- If httpx lower cases a header name (like authorization) make sure the header is still displayed in the error if another parameter doesn't match
+- If `httpx` lower cases a header name (like authorization), make sure the header is still displayed in the error if another parameter doesn't match.
- Add `:Any` type hint to `**matchers` function arguments to satisfy strict type checking mode in pyright
## [0.23.1] - 2023-08-02
From c3236d1fc45f2b0c4e5a844480566443271357b1 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Sun, 3 Sep 2023 13:56:54 +0200
Subject: [PATCH 16/23] Make it explicit that httpx sends headers as lower
cased
---
CHANGELOG.md | 5 +++--
README.md | 4 +++-
pytest_httpx/_httpx_mock.py | 8 ++++----
tests/test_httpx_async.py | 17 +++++++++++++++--
tests/test_httpx_sync.py | 12 +++++++++++-
5 files changed, 36 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef3fc10..c354326 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,8 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
-- If `httpx` lower cases a header name (like authorization), make sure the header is still displayed in the error if another parameter doesn't match.
-- Add `:Any` type hint to `**matchers` function arguments to satisfy strict type checking mode in pyright
+- Do not exclude a request header from the error message when a request is not matched if the header name was not provided as lower-cased to `match_headers`.
+ - It is now explicit that `httpx` sends headers as lower cased.
+- Add `:Any` type hint to `**matchers` function arguments to satisfy strict type checking mode in [`pyright`](https://microsoft.github.io/pyright/#/).
## [0.23.1] - 2023-08-02
### Fixed
diff --git a/README.md b/README.md
index 11cd0d0..890a26b 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -164,6 +164,8 @@ def test_headers_matching(httpx_mock: HTTPXMock):
response = client.get("https://test_url")
```
+Note that even if `httpx` sends headers names as lower cased, the header name case will be ignored when matching.
+
#### Matching on HTTP body
Use `match_content` parameter to specify the full HTTP body to reply to.
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 07f1196..ce4798c 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -222,7 +222,7 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
matchers = [matcher for matcher, _ in self._callbacks]
expect_headers = set(
[
- header
+ header.lower()
for matcher in matchers
if matcher.headers
for header in matcher.headers
@@ -233,9 +233,9 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
request_description = f"{request.method} request on {request.url}"
if expect_headers:
present_headers = {
- name: request.headers.get(name)
- for name in expect_headers
- if request.headers.get(name) is not None
+ name: value
+ for name, value in request.headers.items()
+ if name in expect_headers
}
request_description += f" with {present_headers} headers"
if expect_body:
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index a028bd6..14b13d0 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1093,6 +1093,17 @@ async def test_headers_matching(httpx_mock: HTTPXMock) -> None:
assert response.content == b""
+@pytest.mark.asyncio
+async def test_headers_matching_ignores_case(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}
+ )
+
+ async with httpx.AsyncClient() as client:
+ response = await client.get("https://test_url")
+ assert response.content == b""
+
+
@pytest.mark.asyncio
async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
@@ -1117,7 +1128,9 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> None:
+async def test_url_not_matching_upper_case_headers_matching(
+ httpx_mock: HTTPXMock,
+) -> None:
httpx_mock.add_response(
method="GET",
url="https://test_url?q=b",
@@ -1128,7 +1141,7 @@ async def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMoc
await client.get("https://test_url", headers={"MyHeader": "Something"})
assert (
str(exception_info.value)
- == """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
+ == """No response can be found for GET request on https://test_url with {'myheader': 'Something'} headers amongst:
Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index 42f9e43..f2a3a51 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -150,7 +150,7 @@ def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) ->
client.get("https://test_url", headers={"MyHeader": "Something"})
assert (
str(exception_info.value)
- == """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
+ == """No response can be found for GET request on https://test_url with {'myheader': 'Something'} headers amongst:
Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
@@ -868,6 +868,16 @@ def test_headers_matching(httpx_mock: HTTPXMock) -> None:
assert response.content == b""
+def test_headers_matching_ignores_case(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}
+ )
+
+ with httpx.Client() as client:
+ response = client.get("https://test_url")
+ assert response.content == b""
+
+
def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
From 87d56a533b1f017ab34b8e8184dec981064229fe Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Sun, 3 Sep 2023 16:07:44 +0200
Subject: [PATCH 17/23] Handle header casing
---
CHANGELOG.md | 9 +-
README.md | 8 +-
pytest_httpx/_httpx_mock.py | 31 +++--
tests/test_httpx_async.py | 226 +++++++++++++++++++++++-------------
tests/test_httpx_sync.py | 223 ++++++++++++++++++++++-------------
5 files changed, 327 insertions(+), 170 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c354326..cd2cc94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,9 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Changed
+- Even if it was never documented as a feature, the `match_headers` parameter was not considering header names case when matching.
+ - As this might have been considered a feature by some users, the fact that `match_headers` will now respect casing is documented as a breaking change.
+
### Fixed
-- Do not exclude a request header from the error message when a request is not matched if the header name was not provided as lower-cased to `match_headers`.
- - It is now explicit that `httpx` sends headers as lower cased.
+- Matching on headers does not ignore name case anymore, the name must now be cased as sent (as some servers might expect a specific case).
+- Error message in case a request does not match will now include request headers with mismatching name case as well.
+- Error message in case a request does not match will now include request headers when not provided as lower-cased to `match_headers`.
- Add `:Any` type hint to `**matchers` function arguments to satisfy strict type checking mode in [`pyright`](https://microsoft.github.io/pyright/#/).
## [0.23.1] - 2023-08-02
diff --git a/README.md b/README.md
index 890a26b..416cb6a 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -158,14 +158,12 @@ from pytest_httpx import HTTPXMock
def test_headers_matching(httpx_mock: HTTPXMock):
- httpx_mock.add_response(match_headers={'user-agent': 'python-httpx/0.23.0'})
+ httpx_mock.add_response(match_headers={'User-Agent': 'python-httpx/0.24.1'})
with httpx.Client() as client:
response = client.get("https://test_url")
```
-Note that even if `httpx` sends headers names as lower cased, the header name case will be ignored when matching.
-
#### Matching on HTTP body
Use `match_content` parameter to specify the full HTTP body to reply to.
@@ -517,7 +515,7 @@ def test_timeout(httpx_mock: HTTPXMock):
## Check sent requests
The best way to ensure the content of your requests is still to use the `match_headers` and / or `match_content` parameters when adding a response.
-In the same spirit, ensuring that no request was issued does not necessarily requires any code.
+In the same spirit, ensuring that no request was issued does not necessarily require any code.
In any case, you always have the ability to retrieve the requests that were issued.
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index ce4798c..140c676 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -57,8 +57,18 @@ def _headers_match(self, request: httpx.Request) -> bool:
if not self.headers:
return True
+ encoding = request.headers.encoding
+ request_headers = {}
+ # Can be cleaned based on the outcome of https://github.com/encode/httpx/discussions/2841
+ for raw_name, raw_value in request.headers.raw:
+ if raw_name in request_headers:
+ request_headers[raw_name] += b", " + raw_value
+ else:
+ request_headers[raw_name] = raw_value
+
return all(
- request.headers.get(header_name) == header_value
+ request_headers.get(header_name.encode(encoding))
+ == header_value.encode(encoding)
for header_name, header_value in self.headers.items()
)
@@ -220,9 +230,11 @@ async def _handle_async_request(
def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
matchers = [matcher for matcher, _ in self._callbacks]
+ headers_encoding = request.headers.encoding
expect_headers = set(
[
- header.lower()
+ # httpx uses lower cased header names as internal key
+ header.lower().encode(headers_encoding)
for matcher in matchers
if matcher.headers
for header in matcher.headers
@@ -232,11 +244,16 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
request_description = f"{request.method} request on {request.url}"
if expect_headers:
- present_headers = {
- name: value
- for name, value in request.headers.items()
- if name in expect_headers
- }
+ present_headers = {}
+ # Can be cleaned based on the outcome of https://github.com/encode/httpx/discussions/2841
+ for name, lower_name, value in request.headers._list:
+ if lower_name in expect_headers:
+ name = name.decode(headers_encoding)
+ if name in present_headers:
+ present_headers[name] += f", {value.decode(headers_encoding)}"
+ else:
+ present_headers[name] = value.decode(headers_encoding)
+
request_description += f" with {present_headers} headers"
if expect_body:
request_description += f" and {request.read()} body"
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index 14b13d0..122f9fe 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1085,7 +1085,7 @@ async def test_request_retrieval_with_more_than_one(httpx_mock):
@pytest.mark.asyncio
async def test_headers_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
- match_headers={"user-agent": f"python-httpx/{httpx.__version__}"}
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}
)
async with httpx.AsyncClient() as client:
@@ -1094,23 +1094,93 @@ async def test_headers_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_headers_matching_ignores_case(httpx_mock: HTTPXMock) -> None:
+async def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"})
+
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ "https://test_url",
+ headers=[("my-custom-header", "value1"), ("my-custom-header", "value2")],
+ )
+ assert response.content == b""
+
+
+@pytest.mark.asyncio
+async def test_multi_value_headers_not_matching_single_value_issued(
+ httpx_mock: HTTPXMock,
+) -> None:
+ httpx_mock.add_response(match_headers={"my-custom-header": "value1"})
+
+ async with httpx.AsyncClient() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ await client.get(
+ "https://test_url",
+ headers=[
+ ("my-custom-header", "value1"),
+ ("my-custom-header", "value2"),
+ ],
+ )
+ assert (
+ str(exception_info.value)
+ == """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst:
+Match all requests with {'my-custom-header': 'value1'} headers"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
+@pytest.mark.asyncio
+async def test_multi_value_headers_not_matching_multi_value_issued(
+ httpx_mock: HTTPXMock,
+) -> None:
+ httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"})
+
+ async with httpx.AsyncClient() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ await client.get(
+ "https://test_url",
+ headers=[
+ ("my-custom-header", "value1"),
+ ("my-custom-header", "value3"),
+ ],
+ )
+ assert (
+ str(exception_info.value)
+ == """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst:
+Match all requests with {'my-custom-header': 'value1, value2'} headers"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
+@pytest.mark.asyncio
+async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
- match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}
+ match_headers={"user-agent": f"python-httpx/{httpx.__version__}"}
)
async with httpx.AsyncClient() as client:
- response = await client.get("https://test_url")
- assert response.content == b""
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ await client.get("https://test_url")
+ assert (
+ str(exception_info.value)
+ == f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
+Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
@pytest.mark.asyncio
async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
- "host2": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
+ "Host2": "test_url",
}
)
@@ -1119,8 +1189,8 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
await client.get("https://test_url")
assert (
str(exception_info.value)
- == f"""No response can be found for GET request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2', 'host2': 'test_url'}} headers"""
+ == f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
+Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
)
# Clean up responses to avoid assertion failure
@@ -1141,7 +1211,7 @@ async def test_url_not_matching_upper_case_headers_matching(
await client.get("https://test_url", headers={"MyHeader": "Something"})
assert (
str(exception_info.value)
- == """No response can be found for GET request on https://test_url with {'myheader': 'Something'} headers amongst:
+ == """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
@@ -1178,7 +1248,7 @@ async def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
async def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
- match_headers={"user-agent": f"python-httpx/{httpx.__version__}"},
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
@@ -1191,8 +1261,8 @@ async def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -1202,8 +1272,8 @@ async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock)
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -1214,8 +1284,8 @@ async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock)
async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -1225,8 +1295,8 @@ async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock)
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1237,8 +1307,8 @@ async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock)
async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1248,8 +1318,8 @@ async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1260,7 +1330,7 @@ async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
async def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
- match_headers={"user-agent": f"python-httpx/{httpx.__version__}"},
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
@@ -1276,8 +1346,8 @@ async def test_headers_not_matching_and_url_and_content_matching(
httpx_mock.add_response(
url="https://test_url",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -1287,8 +1357,8 @@ async def test_headers_not_matching_and_url_and_content_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -1302,8 +1372,8 @@ async def test_url_and_headers_not_matching_and_content_matching(
httpx_mock.add_response(
url="https://test_url2",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -1313,8 +1383,8 @@ async def test_url_and_headers_not_matching_and_content_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -1328,8 +1398,8 @@ async def test_url_and_headers_matching_and_content_not_matching(
httpx_mock.add_response(
url="https://test_url",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -1339,8 +1409,8 @@ async def test_url_and_headers_matching_and_content_not_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1354,8 +1424,8 @@ async def test_headers_matching_and_url_and_content_not_matching(
httpx_mock.add_response(
url="https://test_url2",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -1365,8 +1435,8 @@ async def test_headers_matching_and_url_and_content_not_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1380,8 +1450,8 @@ async def test_url_matching_and_headers_and_content_not_matching(
httpx_mock.add_response(
url="https://test_url",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1391,8 +1461,8 @@ async def test_url_matching_and_headers_and_content_not_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1404,8 +1474,8 @@ async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -
httpx_mock.add_response(
url="https://test_url2",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1415,8 +1485,8 @@ async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1430,7 +1500,7 @@ async def test_method_and_url_and_headers_and_content_matching(
httpx_mock.add_response(
url="https://test_url",
method="POST",
- match_headers={"user-agent": f"python-httpx/{httpx.__version__}"},
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
@@ -1447,8 +1517,8 @@ async def test_headers_not_matching_and_method_and_url_and_content_matching(
url="https://test_url",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -1458,8 +1528,8 @@ async def test_headers_not_matching_and_method_and_url_and_content_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -1474,8 +1544,8 @@ async def test_url_and_headers_not_matching_and_method_and_content_matching(
url="https://test_url2",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -1485,8 +1555,8 @@ async def test_url_and_headers_not_matching_and_method_and_content_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -1501,8 +1571,8 @@ async def test_method_and_url_and_headers_matching_and_content_not_matching(
url="https://test_url",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -1512,8 +1582,8 @@ async def test_method_and_url_and_headers_matching_and_content_not_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1528,8 +1598,8 @@ async def test_method_and_headers_matching_and_url_and_content_not_matching(
url="https://test_url2",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -1539,8 +1609,8 @@ async def test_method_and_headers_matching_and_url_and_content_not_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1555,8 +1625,8 @@ async def test_method_and_url_matching_and_headers_and_content_not_matching(
url="https://test_url",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1566,8 +1636,8 @@ async def test_method_and_url_matching_and_headers_and_content_not_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1582,8 +1652,8 @@ async def test_method_matching_and_url_and_headers_and_content_not_matching(
url="https://test_url2",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1593,8 +1663,8 @@ async def test_method_matching_and_url_and_headers_and_content_not_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1609,8 +1679,8 @@ async def test_method_and_url_and_headers_and_content_not_matching(
url="https://test_url2",
method="PUT",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1620,8 +1690,8 @@ async def test_method_and_url_and_headers_and_content_not_matching(
await client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match PUT requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match PUT requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index f2a3a51..1ddc352 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -150,7 +150,7 @@ def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) ->
client.get("https://test_url", headers={"MyHeader": "Something"})
assert (
str(exception_info.value)
- == """No response can be found for GET request on https://test_url with {'myheader': 'Something'} headers amongst:
+ == """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst:
Match GET requests on https://test_url?q=b with {'MyHeader': 'Something'} headers"""
)
@@ -860,7 +860,7 @@ def test_request_retrieval_with_more_than_one(httpx_mock):
def test_headers_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
- match_headers={"user-agent": f"python-httpx/{httpx.__version__}"}
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}
)
with httpx.Client() as client:
@@ -868,22 +868,89 @@ def test_headers_matching(httpx_mock: HTTPXMock) -> None:
assert response.content == b""
-def test_headers_matching_ignores_case(httpx_mock: HTTPXMock) -> None:
+def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"})
+
+ with httpx.Client() as client:
+ response = client.get(
+ "https://test_url",
+ headers=[("my-custom-header", "value1"), ("my-custom-header", "value2")],
+ )
+ assert response.content == b""
+
+
+def test_multi_value_headers_not_matching_single_value_issued(
+ httpx_mock: HTTPXMock,
+) -> None:
+ httpx_mock.add_response(match_headers={"my-custom-header": "value1"})
+
+ with httpx.Client() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ client.get(
+ "https://test_url",
+ headers=[
+ ("my-custom-header", "value1"),
+ ("my-custom-header", "value2"),
+ ],
+ )
+ assert (
+ str(exception_info.value)
+ == """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst:
+Match all requests with {'my-custom-header': 'value1'} headers"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
+def test_multi_value_headers_not_matching_multi_value_issued(
+ httpx_mock: HTTPXMock,
+) -> None:
+ httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"})
+
+ with httpx.Client() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ client.get(
+ "https://test_url",
+ headers=[
+ ("my-custom-header", "value1"),
+ ("my-custom-header", "value3"),
+ ],
+ )
+ assert (
+ str(exception_info.value)
+ == """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst:
+Match all requests with {'my-custom-header': 'value1, value2'} headers"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
+def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
- match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}
+ match_headers={"user-agent": f"python-httpx/{httpx.__version__}"}
)
with httpx.Client() as client:
- response = client.get("https://test_url")
- assert response.content == b""
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ client.get("https://test_url")
+ assert (
+ str(exception_info.value)
+ == f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
+Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
- "host2": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
+ "Host2": "test_url",
}
)
@@ -892,8 +959,8 @@ def test_headers_not_matching(httpx_mock: HTTPXMock) -> None:
client.get("https://test_url")
assert (
str(exception_info.value)
- == f"""No response can be found for GET request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2', 'host2': 'test_url'}} headers"""
+ == f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst:
+Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers"""
)
# Clean up responses to avoid assertion failure
@@ -926,7 +993,7 @@ def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
- match_headers={"user-agent": f"python-httpx/{httpx.__version__}"},
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
@@ -938,8 +1005,8 @@ def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -949,8 +1016,8 @@ def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> Non
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -960,8 +1027,8 @@ def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> Non
def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -971,8 +1038,8 @@ def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> Non
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -982,8 +1049,8 @@ def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> Non
def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -993,8 +1060,8 @@ def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1004,7 +1071,7 @@ def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None:
def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="https://test_url",
- match_headers={"user-agent": f"python-httpx/{httpx.__version__}"},
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
@@ -1019,8 +1086,8 @@ def test_headers_not_matching_and_url_and_content_matching(
httpx_mock.add_response(
url="https://test_url",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -1030,8 +1097,8 @@ def test_headers_not_matching_and_url_and_content_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -1044,8 +1111,8 @@ def test_url_and_headers_not_matching_and_content_matching(
httpx_mock.add_response(
url="https://test_url2",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -1055,8 +1122,8 @@ def test_url_and_headers_not_matching_and_content_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -1069,8 +1136,8 @@ def test_url_and_headers_matching_and_content_not_matching(
httpx_mock.add_response(
url="https://test_url",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -1080,8 +1147,8 @@ def test_url_and_headers_matching_and_content_not_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1094,8 +1161,8 @@ def test_headers_matching_and_url_and_content_not_matching(
httpx_mock.add_response(
url="https://test_url2",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -1105,8 +1172,8 @@ def test_headers_matching_and_url_and_content_not_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1119,8 +1186,8 @@ def test_url_matching_and_headers_and_content_not_matching(
httpx_mock.add_response(
url="https://test_url",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1130,8 +1197,8 @@ def test_url_matching_and_headers_and_content_not_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1142,8 +1209,8 @@ def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None
httpx_mock.add_response(
url="https://test_url2",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1153,8 +1220,8 @@ def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match all requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1165,7 +1232,7 @@ def test_method_and_url_and_headers_and_content_matching(httpx_mock: HTTPXMock)
httpx_mock.add_response(
url="https://test_url",
method="POST",
- match_headers={"user-agent": f"python-httpx/{httpx.__version__}"},
+ match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"},
match_content=b"This is the body",
)
@@ -1181,8 +1248,8 @@ def test_headers_not_matching_and_method_and_url_and_content_matching(
url="https://test_url",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -1192,8 +1259,8 @@ def test_headers_not_matching_and_method_and_url_and_content_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -1207,8 +1274,8 @@ def test_url_and_headers_not_matching_and_method_and_content_matching(
url="https://test_url2",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body",
)
@@ -1218,8 +1285,8 @@ def test_url_and_headers_not_matching_and_method_and_content_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body"""
)
# Clean up responses to avoid assertion failure
@@ -1233,8 +1300,8 @@ def test_method_and_url_and_headers_matching_and_content_not_matching(
url="https://test_url",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -1244,8 +1311,8 @@ def test_method_and_url_and_headers_matching_and_content_not_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1259,8 +1326,8 @@ def test_method_and_headers_matching_and_url_and_content_not_matching(
url="https://test_url2",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url",
},
match_content=b"This is the body2",
)
@@ -1270,8 +1337,8 @@ def test_method_and_headers_matching_and_url_and_content_not_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1285,8 +1352,8 @@ def test_method_and_url_matching_and_headers_and_content_not_matching(
url="https://test_url",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1296,8 +1363,8 @@ def test_method_and_url_matching_and_headers_and_content_not_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1311,8 +1378,8 @@ def test_method_matching_and_url_and_headers_and_content_not_matching(
url="https://test_url2",
method="POST",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1322,8 +1389,8 @@ def test_method_matching_and_url_and_headers_and_content_not_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match POST requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match POST requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
@@ -1337,8 +1404,8 @@ def test_method_and_url_and_headers_and_content_not_matching(
url="https://test_url2",
method="PUT",
match_headers={
- "user-agent": f"python-httpx/{httpx.__version__}",
- "host": "test_url2",
+ "User-Agent": f"python-httpx/{httpx.__version__}",
+ "Host": "test_url2",
},
match_content=b"This is the body2",
)
@@ -1348,8 +1415,8 @@ def test_method_and_url_and_headers_and_content_not_matching(
client.post("https://test_url", content=b"This is the body")
assert (
str(exception_info.value)
- == f"""No response can be found for POST request on https://test_url with {{'host': 'test_url', 'user-agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
-Match PUT requests on https://test_url2 with {{'user-agent': 'python-httpx/{httpx.__version__}', 'host': 'test_url2'}} headers and b'This is the body2' body"""
+ == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst:
+Match PUT requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body"""
)
# Clean up responses to avoid assertion failure
From ed6e1f33b3fb227bd63227bf670c69d485c5f2e7 Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Mon, 4 Sep 2023 11:56:07 +0800
Subject: [PATCH 18/23] Adjusted comments
---
CHANGELOG.md | 2 +-
README.md | 5 +++--
pytest_httpx/_httpx_mock.py | 42 ++++++++++++++++++++-----------------
tests/test_httpx_async.py | 28 +++++++++++++++----------
tests/test_httpx_sync.py | 21 ++++++++++++-------
5 files changed, 57 insertions(+), 41 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9019bea..aceb028 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
-- Added `match_json_content` which allows matching a json decoded body against an arbitrary python object for equality.
+- Added `match_json` which allows matching a json decoded body against an arbitrary python object for equality.
### Changed
- Even if it was never documented as a feature, the `match_headers` parameter was not considering header names case when matching.
diff --git a/README.md b/README.md
index 1135803..728c728 100644
--- a/README.md
+++ b/README.md
@@ -184,7 +184,8 @@ def test_content_matching(httpx_mock: HTTPXMock):
#### Matching on HTTP json body
-Use `match_json_content` parameter to specify the json that will be matched with the json decoded HTTP body to reply to.
+Use `match_json` parameter to specify the json that will be matched with the json decoded HTTP body to reply to.
+Only one of `match_json` and `match_content` should be used.
Maching is performed on equality after json decoding the body.
@@ -193,7 +194,7 @@ import httpx
from pytest_httpx import HTTPXMock
def test_json_matching(httpx_mock: HTTPXMock):
- httpx_mock.add_response(match_json_content={"a": "json", "b": 2})
+ httpx_mock.add_response(match_json={"a": "json", "b": 2})
with httpx.Client() as client:
response = client.post("https://test_url", json={"a": "json", "b": 2})
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 3a4a60e..a2b383d 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -16,14 +16,18 @@ def __init__(
method: Optional[str] = None,
match_headers: Optional[Dict[str, Any]] = None,
match_content: Optional[bytes] = None,
- match_json_content: Optional[Any] = None,
+ match_json: Optional[Any] = None,
):
self.nb_calls = 0
self.url = httpx.URL(url) if url and isinstance(url, str) else url
self.method = method.upper() if method else method
self.headers = match_headers
+ if match_content is not None and match_json is not None:
+ raise ValueError(
+ "Only one parameter of match_json or match_content can be supplied."
+ )
self.content = match_content
- self.json_content = match_json_content
+ self.json = match_json
def match(self, request: httpx.Request) -> bool:
return (
@@ -31,7 +35,7 @@ def match(self, request: httpx.Request) -> bool:
and self._method_match(request)
and self._headers_match(request)
and self._content_match(request)
- and self._json_content_match(request)
+ and self._match_json(request)
)
def _url_match(self, request: httpx.Request) -> bool:
@@ -76,12 +80,12 @@ def _headers_match(self, request: httpx.Request) -> bool:
for header_name, header_value in self.headers.items()
)
- def _json_content_match(self, request: httpx.Request) -> bool:
- if self.json_content is None:
+ def _match_json(self, request: httpx.Request) -> bool:
+ if self.json is None:
return True
try:
# httpx._content.encode_json hard codes utf-8 encoding.
- return json.loads(request.read().decode("utf-8")) == self.json_content
+ return json.loads(request.read().decode("utf-8")) == self.json
except json.decoder.JSONDecodeError:
return False
@@ -99,12 +103,12 @@ def __str__(self) -> str:
matcher_description += f" with {self.headers} headers"
if self.content is not None:
matcher_description += f" and {self.content} body"
- if self.json_content is not None:
- matcher_description += f" and {self.json_content} json body"
+ elif self.json is not None:
+ matcher_description += f" and {self.json} json body"
elif self.content is not None:
matcher_description += f" with {self.content} body"
- elif self.json_content is not None:
- matcher_description += f" with {self.json_content} json body"
+ elif self.json is not None:
+ matcher_description += f" with {self.json} json body"
return matcher_description
@@ -151,7 +155,7 @@ def add_response(
:param method: HTTP method identifying the request(s) to match.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
- :param match_json_content: Any object that should match json.loads(request.body.decode(encoding))
+ :param match_json: Any object that should match json.loads(request.body.decode(encoding))
"""
json = copy.deepcopy(json) if json is not None else None
@@ -188,7 +192,7 @@ def add_callback(
:param method: HTTP method identifying the request(s) to match.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
- :param match_json_content: Any object that should match json.loads(request.body.decode(encoding))
+ :param match_json: Any object that should match json.loads(request.body.decode(encoding))
"""
self._callbacks.append((_RequestMatcher(**matchers), callback))
@@ -202,7 +206,7 @@ def add_exception(self, exception: Exception, **matchers: Any) -> None:
:param method: HTTP method identifying the request(s) to match.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
- :param match_json_content: Any object that should match json.loads(request.body.decode(encoding))
+ :param match_json: Any object that should match json.loads(request.body.decode(encoding))
"""
def exception_callback(request: httpx.Request) -> None:
@@ -260,8 +264,12 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
for header in matcher.headers
]
)
- expect_body = any([matcher.content is not None for matcher in matchers])
- expect_json = any([matcher.json_content is not None for matcher in matchers])
+ expect_body = any(
+ [
+ matcher.content is not None or matcher.json is not None
+ for matcher in matchers
+ ]
+ )
request_description = f"{request.method} request on {request.url}"
if expect_headers:
@@ -278,12 +286,8 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:
request_description += f" with {present_headers} headers"
if expect_body:
request_description += f" and {request.read()} body"
- elif expect_json:
- request_description += f" and {request.read().decode('utf-8')} body"
elif expect_body:
request_description += f" with {request.read()} body"
- elif expect_json:
- request_description += f" with {request.read().decode('utf-8')} body"
matchers_description = "\n".join([str(matcher) for matcher in matchers])
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index c906f77..b917664 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1246,8 +1246,14 @@ async def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_json_content_matching(httpx_mock: HTTPXMock) -> None:
- httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+async def test_match_json_and_match_content_error(httpx_mock: HTTPXMock) -> None:
+ with pytest.raises(ValueError):
+ httpx_mock.add_response(match_json={"a": 1}, match_content=b"")
+
+
+@pytest.mark.asyncio
+async def test_match_json_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json={"a": 1, "b": 2})
async with httpx.AsyncClient() as client:
response = await client.post("https://test_url", json={"b": 2, "a": 1})
@@ -1255,15 +1261,15 @@ async def test_json_content_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_json_content_not_matching(httpx_mock: HTTPXMock) -> None:
- httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+async def test_match_json_not_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json={"a": 1, "b": 2})
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
assert (
str(exception_info.value)
- == """No response can be found for POST request on https://test_url with {"c": 3, "b": 2, "a": 1} body amongst:
+ == """No response can be found for POST request on https://test_url with b'{"c": 3, "b": 2, "a": 1}' body amongst:
Match all requests with {'a': 1, 'b': 2} json body"""
)
@@ -1272,9 +1278,9 @@ async def test_json_content_not_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_headers_and_json_content_not_matching(httpx_mock: HTTPXMock) -> None:
+async def test_headers_and_match_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
- match_json_content={"a": 1, "b": 2},
+ match_json={"a": 1, "b": 2},
match_headers={"foo": "bar"},
)
@@ -1283,7 +1289,7 @@ async def test_headers_and_json_content_not_matching(httpx_mock: HTTPXMock) -> N
await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
assert (
str(exception_info.value)
- == """No response can be found for POST request on https://test_url with {} headers and {"c": 3, "b": 2, "a": 1} body amongst:
+ == """No response can be found for POST request on https://test_url with {} headers and b'{"c": 3, "b": 2, "a": 1}' body amongst:
Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
)
@@ -1292,15 +1298,15 @@ async def test_headers_and_json_content_not_matching(httpx_mock: HTTPXMock) -> N
@pytest.mark.asyncio
-async def test_json_content_not_matching_invalid_json(httpx_mock: HTTPXMock) -> None:
- httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+async def test_match_json_not_matching_invalid_json(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json={"a": 1, "b": 2})
async with httpx.AsyncClient() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
await client.post("https://test_url", content=b"foobar")
assert (
str(exception_info.value)
- == """No response can be found for POST request on https://test_url with foobar body amongst:
+ == """No response can be found for POST request on https://test_url with b'foobar' body amongst:
Match all requests with {'a': 1, 'b': 2} json body"""
)
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index b17b54e..228641c 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -992,24 +992,29 @@ def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.reset(assert_all_responses_were_requested=False)
+def test_match_json_and_match_content_error(httpx_mock: HTTPXMock) -> None:
+ with pytest.raises(ValueError):
+ httpx_mock.add_response(match_json={"a": 1}, match_content=b"")
+
+
@pytest.mark.parametrize("json", [{"a": 1, "b": 2}, "somestring", "25", 25.3])
-def test_json_content_matching(json: Any, httpx_mock: HTTPXMock) -> None:
- httpx_mock.add_response(match_json_content=json)
+def test_match_json_matching(json: Any, httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json=json)
with httpx.Client() as client:
response = client.post("https://test_url", json=json)
assert response.read() == b""
-def test_json_content_not_matching(httpx_mock: HTTPXMock) -> None:
- httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+def test_match_json_not_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json={"a": 1, "b": 2})
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
assert (
str(exception_info.value)
- == """No response can be found for POST request on https://test_url with {"c": 3, "b": 2, "a": 1} body amongst:
+ == """No response can be found for POST request on https://test_url with b'{"c": 3, "b": 2, "a": 1}' body amongst:
Match all requests with {'a': 1, 'b': 2} json body"""
)
@@ -1017,15 +1022,15 @@ def test_json_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.reset(assert_all_responses_were_requested=False)
-def test_json_content_not_matching_invalid_json(httpx_mock: HTTPXMock) -> None:
- httpx_mock.add_response(match_json_content={"a": 1, "b": 2})
+def test_match_json_not_matching_invalid_json(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json={"a": 1, "b": 2})
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException) as exception_info:
client.post("https://test_url", content=b"foobar")
assert (
str(exception_info.value)
- == """No response can be found for POST request on https://test_url with foobar body amongst:
+ == """No response can be found for POST request on https://test_url with b'foobar' body amongst:
Match all requests with {'a': 1, 'b': 2} json body"""
)
From 7533b457ac3a01ec252c057d2ff14bd042f6389a Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Mon, 4 Sep 2023 11:59:53 +0800
Subject: [PATCH 19/23] Moved json matchong to _content_match method
---
pytest_httpx/_httpx_mock.py | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index a2b383d..3d2d2a5 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -35,7 +35,6 @@ def match(self, request: httpx.Request) -> bool:
and self._method_match(request)
and self._headers_match(request)
and self._content_match(request)
- and self._match_json(request)
)
def _url_match(self, request: httpx.Request) -> bool:
@@ -80,21 +79,17 @@ def _headers_match(self, request: httpx.Request) -> bool:
for header_name, header_value in self.headers.items()
)
- def _match_json(self, request: httpx.Request) -> bool:
- if self.json is None:
+ 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
try:
# httpx._content.encode_json hard codes utf-8 encoding.
return json.loads(request.read().decode("utf-8")) == self.json
except json.decoder.JSONDecodeError:
return False
- def _content_match(self, request: httpx.Request) -> bool:
- if self.content is None:
- return True
-
- return request.read() == self.content
-
def __str__(self) -> str:
matcher_description = f"Match {self.method or 'all'} requests"
if self.url:
From bbbb482400ba8e055ef915feb4d48771b4b2089e Mon Sep 17 00:00:00 2001
From: Dolf Andringa
Date: Mon, 4 Sep 2023 12:06:16 +0800
Subject: [PATCH 20/23] Renamed test functions to be a little better
---
tests/test_httpx_async.py | 8 ++++----
tests/test_httpx_sync.py | 6 +++---
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index b917664..c61bcfa 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1252,7 +1252,7 @@ async def test_match_json_and_match_content_error(httpx_mock: HTTPXMock) -> None
@pytest.mark.asyncio
-async def test_match_json_matching(httpx_mock: HTTPXMock) -> None:
+async def test_json_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
async with httpx.AsyncClient() as client:
@@ -1261,7 +1261,7 @@ async def test_match_json_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_match_json_not_matching(httpx_mock: HTTPXMock) -> None:
+async def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
async with httpx.AsyncClient() as client:
@@ -1278,7 +1278,7 @@ async def test_match_json_not_matching(httpx_mock: HTTPXMock) -> None:
@pytest.mark.asyncio
-async def test_headers_and_match_json_not_matching(httpx_mock: HTTPXMock) -> None:
+async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
match_json={"a": 1, "b": 2},
match_headers={"foo": "bar"},
@@ -1298,7 +1298,7 @@ async def test_headers_and_match_json_not_matching(httpx_mock: HTTPXMock) -> Non
@pytest.mark.asyncio
-async def test_match_json_not_matching_invalid_json(httpx_mock: HTTPXMock) -> None:
+async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
async with httpx.AsyncClient() as client:
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index 228641c..4bf51ce 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -998,7 +998,7 @@ def test_match_json_and_match_content_error(httpx_mock: HTTPXMock) -> None:
@pytest.mark.parametrize("json", [{"a": 1, "b": 2}, "somestring", "25", 25.3])
-def test_match_json_matching(json: Any, httpx_mock: HTTPXMock) -> None:
+def test_json_matching(json: Any, httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json=json)
with httpx.Client() as client:
@@ -1006,7 +1006,7 @@ def test_match_json_matching(json: Any, httpx_mock: HTTPXMock) -> None:
assert response.read() == b""
-def test_match_json_not_matching(httpx_mock: HTTPXMock) -> None:
+def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
with httpx.Client() as client:
@@ -1022,7 +1022,7 @@ def test_match_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.reset(assert_all_responses_were_requested=False)
-def test_match_json_not_matching_invalid_json(httpx_mock: HTTPXMock) -> None:
+def test_json_invalid_json(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
with httpx.Client() as client:
From 9d5276222113e74d6c8f6f1ebdf03cf92a4d161e Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 4 Sep 2023 20:27:55 +0200
Subject: [PATCH 21/23] Cleanup json matching documentation
---
CHANGELOG.md | 2 +-
README.md | 10 +++++-----
pytest_httpx/_httpx_mock.py | 14 ++++++++------
tests/test_httpx_async.py | 6 ------
tests/test_httpx_sync.py | 35 +++++++++++++++++++++++++++++------
5 files changed, 43 insertions(+), 24 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aceb028..ebdb8ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
-- Added `match_json` which allows matching a json decoded body against an arbitrary python object for equality.
+- Added `match_json` parameter which allows matching on JSON decoded body (matching against python representation instead of bytes).
### Changed
- Even if it was never documented as a feature, the `match_headers` parameter was not considering header names case when matching.
diff --git a/README.md b/README.md
index 728c728..303bdc1 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -182,12 +182,11 @@ def test_content_matching(httpx_mock: HTTPXMock):
response = client.post("https://test_url", content=b"This is the body")
```
-#### Matching on HTTP json body
+##### Matching on HTTP JSON body
-Use `match_json` parameter to specify the json that will be matched with the json decoded HTTP body to reply to.
-Only one of `match_json` and `match_content` should be used.
+Use `match_json` parameter to specify the JSON decoded HTTP body to reply to.
-Maching is performed on equality after json decoding the body.
+Matching is performed on equality.
```python
import httpx
@@ -200,6 +199,7 @@ def test_json_matching(httpx_mock: HTTPXMock):
response = client.post("https://test_url", json={"a": "json", "b": 2})
```
+Note that `match_content` cannot be provided if `match_json` is also provided.
### Add JSON response
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 3d2d2a5..975c531 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -24,7 +24,7 @@ def __init__(
self.headers = match_headers
if match_content is not None and match_json is not None:
raise ValueError(
- "Only one parameter of match_json or match_content can be supplied."
+ "Only one way of matching against the body can be provided. If you want to match against the JSON decoded representation, use match_json. Otherwise, use match_content."
)
self.content = match_content
self.json = match_json
@@ -150,7 +150,7 @@ def add_response(
:param method: HTTP method identifying the request(s) to match.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
- :param match_json: Any object that should match json.loads(request.body.decode(encoding))
+ :param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable.
"""
json = copy.deepcopy(json) if json is not None else None
@@ -187,7 +187,7 @@ def add_callback(
:param method: HTTP method identifying the request(s) to match.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
- :param match_json: Any object that should match json.loads(request.body.decode(encoding))
+ :param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable.
"""
self._callbacks.append((_RequestMatcher(**matchers), callback))
@@ -201,7 +201,7 @@ def add_exception(self, exception: Exception, **matchers: Any) -> None:
:param method: HTTP method identifying the request(s) to match.
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
- :param match_json: Any object that should match json.loads(request.body.decode(encoding))
+ :param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable.
"""
def exception_callback(request: httpx.Request) -> None:
@@ -327,9 +327,10 @@ def get_requests(self, **matchers: Any) -> List[httpx.Request]:
:param url: Full URL identifying the requests to retrieve.
Can be a str, a re.Pattern instance or a httpx.URL instance.
- :param method: HTTP method identifying the requests to retrieve. Must be a upper cased string value.
+ :param method: HTTP method identifying the requests to retrieve. Must be an upper-cased string value.
:param match_headers: HTTP headers identifying the requests to retrieve. Must be a dictionary.
:param match_content: Full HTTP body identifying the requests to retrieve. Must be bytes.
+ :param match_json: JSON decoded HTTP body identifying the requests to retrieve. Must be JSON encodable.
"""
matcher = _RequestMatcher(**matchers)
return [request for request in self._requests if matcher.match(request)]
@@ -340,9 +341,10 @@ def get_request(self, **matchers: Any) -> Optional[httpx.Request]:
:param url: Full URL identifying the request to retrieve.
Can be a str, a re.Pattern instance or a httpx.URL instance.
- :param method: HTTP method identifying the request to retrieve. Must be a upper cased string value.
+ :param method: HTTP method identifying the request to retrieve. Must be an upper-cased string value.
:param match_headers: HTTP headers identifying the request to retrieve. Must be a dictionary.
:param match_content: Full HTTP body identifying the request to retrieve. Must be bytes.
+ :param match_json: JSON decoded HTTP body identifying the request to retrieve. Must be JSON encodable.
:raises AssertionError: in case more than one request match.
"""
requests = self.get_requests(**matchers)
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index c61bcfa..e3fda17 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -1245,12 +1245,6 @@ async def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.reset(assert_all_responses_were_requested=False)
-@pytest.mark.asyncio
-async def test_match_json_and_match_content_error(httpx_mock: HTTPXMock) -> None:
- with pytest.raises(ValueError):
- httpx_mock.add_response(match_json={"a": 1}, match_content=b"")
-
-
@pytest.mark.asyncio
async def test_json_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index 4bf51ce..ef7e579 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -993,16 +993,20 @@ def test_content_not_matching(httpx_mock: HTTPXMock) -> None:
def test_match_json_and_match_content_error(httpx_mock: HTTPXMock) -> None:
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError) as exception_info:
httpx_mock.add_response(match_json={"a": 1}, match_content=b"")
+ assert (
+ str(exception_info.value)
+ == "Only one way of matching against the body can be provided. If you want to match against the JSON decoded representation, use match_json. Otherwise, use match_content."
+ )
-@pytest.mark.parametrize("json", [{"a": 1, "b": 2}, "somestring", "25", 25.3])
-def test_json_matching(json: Any, httpx_mock: HTTPXMock) -> None:
- httpx_mock.add_response(match_json=json)
+
+def test_json_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json={"a": 1, "b": 2})
with httpx.Client() as client:
- response = client.post("https://test_url", json=json)
+ response = client.post("https://test_url", json={"b": 2, "a": 1})
assert response.read() == b""
@@ -1022,7 +1026,26 @@ def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.reset(assert_all_responses_were_requested=False)
-def test_json_invalid_json(httpx_mock: HTTPXMock) -> None:
+def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(
+ match_json={"a": 1, "b": 2},
+ match_headers={"foo": "bar"},
+ )
+
+ with httpx.Client() as client:
+ with pytest.raises(httpx.TimeoutException) as exception_info:
+ client.post("https://test_url", json={"c": 3, "b": 2, "a": 1})
+ assert (
+ str(exception_info.value)
+ == """No response can be found for POST request on https://test_url with {} headers and b'{"c": 3, "b": 2, "a": 1}' body amongst:
+Match all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body"""
+ )
+
+ # Clean up responses to avoid assertion failure
+ httpx_mock.reset(assert_all_responses_were_requested=False)
+
+
+def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
with httpx.Client() as client:
From e9b0b397e53196d9c818d2b7510c29d31a077ad1 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 4 Sep 2023 20:36:00 +0200
Subject: [PATCH 22/23] Document partial matching
---
README.md | 12 ++++++++++--
tests/test_httpx_async.py | 10 ++++++++++
tests/test_httpx_sync.py | 9 +++++++++
3 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 303bdc1..3343571 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -186,17 +186,25 @@ def test_content_matching(httpx_mock: HTTPXMock):
Use `match_json` parameter to specify the JSON decoded HTTP body to reply to.
-Matching is performed on equality.
+Matching is performed on equality. You can however use `unittest.mock.ANY` to do partial matching.
```python
import httpx
from pytest_httpx import HTTPXMock
+from unittest.mock import ANY
def test_json_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(match_json={"a": "json", "b": 2})
with httpx.Client() as client:
response = client.post("https://test_url", json={"a": "json", "b": 2})
+
+
+def test_partial_json_matching(httpx_mock: HTTPXMock):
+ httpx_mock.add_response(match_json={"a": "json", "b": ANY})
+
+ with httpx.Client() as client:
+ response = client.post("https://test_url", json={"a": "json", "b": 2})
```
Note that `match_content` cannot be provided if `match_json` is also provided.
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index e3fda17..1f1ab99 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -6,6 +6,7 @@
import httpx
import pytest
from pytest import Testdir
+from unittest.mock import ANY
import pytest_httpx
from pytest_httpx import HTTPXMock
@@ -1254,6 +1255,15 @@ async def test_json_matching(httpx_mock: HTTPXMock) -> None:
assert response.read() == b""
+@pytest.mark.asyncio
+async def test_json_partial_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json={"a": 1, "b": ANY})
+
+ async with httpx.AsyncClient() as client:
+ response = await client.post("https://test_url", json={"b": 2, "a": 1})
+ assert response.read() == b""
+
+
@pytest.mark.asyncio
async def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index ef7e579..5e63bbc 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -1,5 +1,6 @@
import re
from typing import Any
+from unittest.mock import ANY
import httpx
import pytest
@@ -1010,6 +1011,14 @@ def test_json_matching(httpx_mock: HTTPXMock) -> None:
assert response.read() == b""
+def test_json_partial_matching(httpx_mock: HTTPXMock) -> None:
+ httpx_mock.add_response(match_json={"a": 1, "b": ANY})
+
+ with httpx.Client() as client:
+ response = client.post("https://test_url", json={"b": 2, "a": 1})
+ assert response.read() == b""
+
+
def test_json_not_matching(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(match_json={"a": 1, "b": 2})
From 2397fbe1f4102e287190da5ce332d831db25d244 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 4 Sep 2023 20:39:36 +0200
Subject: [PATCH 23/23] Release version 0.24.0 today
---
CHANGELOG.md | 5 ++++-
pytest_httpx/version.py | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebdb8ce..b9d5f0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+
+## [0.24.0] - 2023-09-04
### Added
- Added `match_json` parameter which allows matching on JSON decoded body (matching against python representation instead of bytes).
@@ -274,7 +276,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.23.1...HEAD
+[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.24.0...HEAD
+[0.24.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.23.1...v0.24.0
[0.23.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.23.0...v0.23.1
[0.23.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.22.0...v0.23.0
[0.22.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.3...v0.22.0
diff --git a/pytest_httpx/version.py b/pytest_httpx/version.py
index 3e3aba8..6eb5d9a 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.23.1"
+__version__ = "0.24.0"