Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Use `match_params` to partially match query parameters without having to provide

If this parameter is provided, `url` parameter must not contain any query parameter.

All query parameters have to be provided (as `str`). You can however use `unittest.mock.ANY` to do partial matching.
All query parameters have to be provided as strings (`str`). However, **boolean values (True and False) are automatically converted to "true" and "false"** in the query string, so you can use them directly in `match_params`. You can also use `unittest.mock.ANY` for partial matching.

```python
import httpx
Expand Down
17 changes: 17 additions & 0 deletions pytest_httpx/_httpx_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
Sequence[tuple[bytes, bytes]],
]

PrimitiveData = Optional[Union[str, int, float, bool]]


class IteratorStream(AsyncIteratorByteStream, IteratorByteStream):
def __init__(self, stream: Iterable[bytes]):
Expand Down Expand Up @@ -58,3 +60,18 @@ def _proxy_url(
real_pool := real_transport._pool, (httpcore.HTTPProxy, httpcore.AsyncHTTPProxy)
):
return _to_httpx_url(real_pool._proxy_url, real_pool._proxy_headers)


def _primitive_value_to_str(value: PrimitiveData) -> str:
"""
Coerce a primitive data type into a string value.

Note that we prefer JSON-style 'true'/'false' for boolean values here.
"""
if value is True:
return "true"
elif value is False:
return "false"
elif value is None:
return ""
return str(value)
19 changes: 16 additions & 3 deletions pytest_httpx/_request_matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
import httpx
from httpx import QueryParams

from pytest_httpx._httpx_internals import _proxy_url
from pytest_httpx._httpx_internals import _proxy_url, _primitive_value_to_str
from pytest_httpx._options import _HTTPXMockOptions


def _normalize_bool(value: Union[str | bool]) -> str:
return _primitive_value_to_str(value) if isinstance(value, bool) else value


def _url_match(
url_to_match: Union[Pattern[str], httpx.URL],
received: httpx.URL,
params: Optional[dict[str, Union[str | list[str]]]],
params: Optional[dict[str, Union[str | list[str] | bool]]],
) -> bool:
if isinstance(url_to_match, re.Pattern):
return url_to_match.match(str(received)) is not None
Expand All @@ -22,6 +26,15 @@ def _url_match(
received_params = to_params_dict(received.params)
if params is None:
params = to_params_dict(url_to_match.params)
else:
params = {
k: (
[_normalize_bool(x) for x in v]
if isinstance(v, list)
else _normalize_bool(v)
)
for k, v in params.items()
}

# Remove the query parameters from the original URL to compare everything besides query parameters
received_url = received.copy_with(query=None)
Expand Down Expand Up @@ -52,7 +65,7 @@ def __init__(
match_data: Optional[dict[str, Any]] = None,
match_files: Optional[Any] = None,
match_extensions: Optional[dict[str, Any]] = None,
match_params: Optional[dict[str, Union[str | list[str]]]] = None,
match_params: Optional[dict[str, Union[str | list[str] | bool]]] = None,
is_optional: Optional[bool] = None,
is_reusable: Optional[bool] = None,
):
Expand Down
41 changes: 41 additions & 0 deletions tests/test_httpx_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
import time
from collections.abc import AsyncIterable
from typing import Union, Any

import httpx
import pytest
Expand Down Expand Up @@ -184,6 +185,46 @@ async def test_url_query_params_not_matching(httpx_mock: HTTPXMock) -> None:
)


@pytest.mark.asyncio
@pytest.mark.parametrize(
("match_params", "request_params"),
[
({"a": True, "b": False}, {"a": True, "b": False}),
({"a": [True, "1", "2"]}, {"a": [True, 1, "2"]}),
],
)
async def test_url_query_params_stringification_for_matching_with_params(
httpx_mock: HTTPXMock,
match_params: dict[str, Any],
request_params: dict[str, Any],
) -> None:
httpx_mock.add_response(url="https://test_url", match_params=match_params)

async with httpx.AsyncClient() as client:
response = await client.get("https://test_url", params=request_params)
assert response.content == b""


@pytest.mark.asyncio
@pytest.mark.parametrize(
("match_params", "url"),
[
({"a": True, "b": False}, "https://test_url?a=true&b=false"),
({"a": [True, "1", "2"]}, "https://test_url?a=true&a=1&a=2"),
],
)
async def test_url_query_params_stringification_for_matching_in_url(
httpx_mock: HTTPXMock,
match_params: dict[str, Any],
url: str,
) -> None:
httpx_mock.add_response(url="https://test_url", match_params=match_params)

async with httpx.AsyncClient() as client:
response = await client.get(url)
assert response.content == b""


@pytest.mark.asyncio
async def test_url_matching_with_more_than_one_value_on_same_param(
httpx_mock: HTTPXMock,
Expand Down
39 changes: 39 additions & 0 deletions tests/test_httpx_sync.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import re
from collections.abc import Iterable
from typing import Union, Any
from unittest.mock import ANY

import httpx
Expand Down Expand Up @@ -199,6 +200,44 @@ def test_url_query_params_not_matching(httpx_mock: HTTPXMock) -> None:
)


@pytest.mark.parametrize(
("match_params", "request_params"),
[
({"a": True, "b": False}, {"a": True, "b": False}),
({"a": [True, "1", "2"]}, {"a": [True, 1, "2"]}),
],
)
def test_url_query_params_stringification_for_matching_with_params(
httpx_mock: HTTPXMock,
match_params: dict[str, Any],
request_params: dict[str, Any],
) -> None:
httpx_mock.add_response(url="https://test_url", match_params=match_params)

with httpx.Client() as client:
response = client.get("https://test_url", params=request_params)
assert response.content == b""


@pytest.mark.parametrize(
("match_params", "url"),
[
({"a": True, "b": False}, "https://test_url?a=true&b=false"),
({"a": [True, "1", "2"]}, "https://test_url?a=true&a=1&a=2"),
],
)
def test_url_query_params_stringification_for_matching_in_url(
httpx_mock: HTTPXMock,
match_params: dict[str, Any],
url: str,
) -> None:
httpx_mock.add_response(url="https://test_url", match_params=match_params)

with httpx.Client() as client:
response = client.get(url)
assert response.content == b""


def test_url_matching_with_more_than_one_value_on_same_param(
httpx_mock: HTTPXMock,
) -> None:
Expand Down
Loading