diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index 4c4b2f5..c34033b 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -15,7 +15,6 @@ def _url_match( received: httpx.URL, params: Optional[dict[str, Union[str | list[str]]]], ) -> bool: - # TODO Allow to provide a regex in URL and params as a dict if isinstance(url_to_match, re.Pattern): return url_to_match.match(str(received)) is not None @@ -90,14 +89,23 @@ def __init__( "If you want to match against the multipart representation, use match_files (and match_data). " "Otherwise, use match_content." ) + if self.params and not self.url: + raise ValueError( + "URL must be provided when match_params is used." + ) + if self.params and isinstance(self.url, re.Pattern): + raise ValueError( + "match_params cannot be used in addition to regex URL. Request this feature via https://github.com/Colin-b/pytest_httpx/issues/new?title=Regex%20URL%20should%20allow%20match_params&body=Hi,%20I%20need%20a%20regex%20to%20match%20the%20non%20query%20part%20of%20the%20URL%20only" + ) + if self._is_matching_params_more_than_one_way(): + raise ValueError( + "Provided URL must not contain any query parameter when match_params is used." + ) if self.data and not self.files: raise ValueError( "match_data is meant to be used for multipart matching (in conjunction with match_files)." "Use match_content to match url encoded data." ) - # TODO Prevent match_params and params in URL - # TODO Prevent match_params with non str values / keys - # TODO Prevent match_params with list values of size < 2 def expect_body(self) -> bool: matching_ways = [ @@ -115,6 +123,14 @@ def _is_matching_body_more_than_one_way(self) -> bool: ] return sum(matching_ways) > 1 + def _is_matching_params_more_than_one_way(self) -> bool: + url_has_params = bool(self.url.params) if (self.url and isinstance(self.url, httpx.URL)) else False + matching_ways = [ + self.params is not None, + url_has_params, + ] + return sum(matching_ways) > 1 + def match( self, real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport], diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index 663ae52..ac4b3fb 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -89,6 +89,155 @@ async def test_url_query_params_partial_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +async def test_url_query_params_with_single_value_list(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={"a": ["1"]}, + is_optional=True, + ) + + async with httpx.AsyncClient() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + await client.post("https://test_url?a=1") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?a=1 amongst: +- Match any request on https://test_url with {'a': ['1']} query parameters""" + ) + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +async def test_url_query_params_with_non_str_value(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={"a": 1}, + is_optional=True, + ) + + async with httpx.AsyncClient() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + await client.post("https://test_url?a=1") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?a=1 amongst: +- Match any request on https://test_url with {'a': 1} query parameters""" + ) + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +async def test_url_query_params_with_non_str_list_value(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={"a": [1, "2"]}, + is_optional=True, + ) + + async with httpx.AsyncClient() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + await client.post("https://test_url?a=1&a=2") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?a=1&a=2 amongst: +- Match any request on https://test_url with {'a': [1, '2']} query parameters""" + ) + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +async def test_url_query_params_with_non_str_name(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={1: "1"}, + is_optional=True, + ) + + async with httpx.AsyncClient() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + await client.post("https://test_url?1=1") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?1=1 amongst: +- Match any request on https://test_url with {1: '1'} query parameters""" + ) + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +async def test_url_query_params_not_matching(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={"a": "1"}, + is_optional=True, + ) + + async with httpx.AsyncClient() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + await client.post("https://test_url?a=2") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?a=2 amongst: +- Match any request on https://test_url with {'a': '1'} query parameters""" + ) + + +@pytest.mark.asyncio +async def test_url_matching_with_more_than_one_value_on_same_param(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(url="https://test_url?a=1&a=3", is_optional=True) + + async with httpx.AsyncClient() as client: + response = await client.get("https://test_url", params={"a": [1, 3]}) + assert response.content == b"" + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +async def test_url_not_matching_with_more_than_one_value_on_same_param_and_diff_value(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(url="https://test_url?a=2&a=3", is_optional=True) + + async with httpx.AsyncClient() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + await client.get("https://test_url", params={"a": [1, 3]}) + assert ( + str(exception_info.value) + == """No response can be found for GET request on https://test_url?a=1&a=3 amongst: +- Match any request on https://test_url?a=2&a=3""" + ) + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +async def test_url_not_matching_with_more_than_one_value_on_same_param_and_more_values(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(url="https://test_url?a=1&a=3", is_optional=True) + + async with httpx.AsyncClient() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + await client.get("https://test_url", params={"a": [1, 3, 4]}) + assert ( + str(exception_info.value) + == """No response can be found for GET request on https://test_url?a=1&a=3&a=4 amongst: +- Match any request on https://test_url?a=1&a=3""" + ) + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +async def test_url_not_matching_with_more_than_one_value_on_same_param_and_less_values(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(url="https://test_url?a=1&a=3&a=4", is_optional=True) + + async with httpx.AsyncClient() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + await client.get("https://test_url", params={"a": [1, 3]}) + assert ( + str(exception_info.value) + == """No response can be found for GET request on https://test_url?a=1&a=3 amongst: +- Match any request on https://test_url?a=1&a=3&a=4""" + ) + + @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_not_matching(httpx_mock: HTTPXMock) -> None: diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py index e1f5610..60e1d64 100644 --- a/tests/test_httpx_sync.py +++ b/tests/test_httpx_sync.py @@ -80,7 +80,165 @@ def test_url_query_params_partial_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" -# TODO Test URL matching with more than one value on the same param (sucess and failure) +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +def test_url_query_params_with_single_value_list(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={"a": ["1"]}, + is_optional=True, + ) + + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + client.post("https://test_url?a=1") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?a=1 amongst: +- Match any request on https://test_url with {'a': ['1']} query parameters""" + ) + + +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +def test_url_query_params_with_non_str_value(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={"a": 1}, + is_optional=True, + ) + + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + client.post("https://test_url?a=1") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?a=1 amongst: +- Match any request on https://test_url with {'a': 1} query parameters""" + ) + + +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +def test_url_query_params_with_non_str_list_value(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={"a": [1, "2"]}, + is_optional=True, + ) + + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + client.post("https://test_url?a=1&a=2") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?a=1&a=2 amongst: +- Match any request on https://test_url with {'a': [1, '2']} query parameters""" + ) + + +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +def test_url_query_params_with_non_str_name(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={1: "1"}, + is_optional=True, + ) + + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + client.post("https://test_url?1=1") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?1=1 amongst: +- Match any request on https://test_url with {1: '1'} query parameters""" + ) + + +def test_match_params_without_url(httpx_mock: HTTPXMock) -> None: + with pytest.raises(ValueError) as exception_info: + httpx_mock.add_response(match_params={"a": "1"}) + + assert str(exception_info.value) == "URL must be provided when match_params is used." + + +def test_query_params_in_both_url_and_match_params(httpx_mock: HTTPXMock) -> None: + with pytest.raises(ValueError) as exception_info: + httpx_mock.add_response(url="https://test_url?a=1", match_params={"a": "1"}) + + assert str(exception_info.value) == "Provided URL must not contain any query parameter when match_params is used." + + +def test_regex_url_and_match_params(httpx_mock: HTTPXMock) -> None: + with pytest.raises(ValueError) as exception_info: + httpx_mock.add_response(url=re.compile(".*test.*"), match_params={"a": "1"}) + + assert str(exception_info.value) == "match_params cannot be used in addition to regex URL. Request this feature via https://github.com/Colin-b/pytest_httpx/issues/new?title=Regex%20URL%20should%20allow%20match_params&body=Hi,%20I%20need%20a%20regex%20to%20match%20the%20non%20query%20part%20of%20the%20URL%20only" + + +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +def test_url_query_params_not_matching(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( + url="https://test_url", + match_params={"a": "1"}, + is_optional=True, + ) + + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + client.post("https://test_url?a=2") + assert ( + str(exception_info.value) + == """No response can be found for POST request on https://test_url?a=2 amongst: +- Match any request on https://test_url with {'a': '1'} query parameters""" + ) + + +def test_url_matching_with_more_than_one_value_on_same_param(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(url="https://test_url?a=1&a=3", is_optional=True) + + with httpx.Client() as client: + response = client.get("https://test_url", params={"a": [1, 3]}) + assert response.content == b"" + + +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +def test_url_not_matching_with_more_than_one_value_on_same_param_and_diff_value(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(url="https://test_url?a=2&a=3", is_optional=True) + + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + client.get("https://test_url", params={"a": [1, 3]}) + assert ( + str(exception_info.value) + == """No response can be found for GET request on https://test_url?a=1&a=3 amongst: +- Match any request on https://test_url?a=2&a=3""" + ) + + +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +def test_url_not_matching_with_more_than_one_value_on_same_param_and_more_values(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(url="https://test_url?a=1&a=3", is_optional=True) + + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + client.get("https://test_url", params={"a": [1, 3, 4]}) + assert ( + str(exception_info.value) + == """No response can be found for GET request on https://test_url?a=1&a=3&a=4 amongst: +- Match any request on https://test_url?a=1&a=3""" + ) + + +@pytest.mark.httpx_mock(assert_all_requests_were_expected=False) +def test_url_not_matching_with_more_than_one_value_on_same_param_and_less_values(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(url="https://test_url?a=1&a=3&a=4", is_optional=True) + + with httpx.Client() as client: + with pytest.raises(httpx.TimeoutException) as exception_info: + client.get("https://test_url", params={"a": [1, 3]}) + assert ( + str(exception_info.value) + == """No response can be found for GET request on https://test_url?a=1&a=3 amongst: +- Match any request on https://test_url?a=1&a=3&a=4""" + ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False)