respx
respx copied to clipboard
`respx_mock` doesn't handle `//` in the path-section of URLs.
Hi,
I just stumbled upon the issue described in the title, which can be reproduced with the following pytest file.
import httpx
import pytest
from pydantic import AnyHttpUrl
from respx import MockRouter
@pytest.mark.parametrize(
"url",
[
"http://localhost", # OK
"http://localhost/", # OK
"http://localhost//", # Fails
"http://localhost///", # Fails
"http://localhost/%2F", # Fails
"http://localhost/%2F/", # Fails
"http://localhost/%2F%2F", # Fails
],
)
async def test_respx_targeting(respx_mock: MockRouter, url: AnyHttpUrl) -> None:
route = respx_mock.get(url=url).respond(status_code=200)
result = httpx.get(url)
assert result.status_code == 200
assert route.called
The fails all take the form of:
respx.models.AllMockedAssertionError: RESPX: <Request('GET', 'http://localhost//')> not mocked!
Having // in the path sections of URLs is valid according to RFC 3986 (see section '3: Syntax Components' and section '3.3. Path'), Stack Overflow seems to concur.
I noticed the issue as I am using hypothesis to run property-based tests on my code, in particular using their provisional.urls strategy.
The above parametrized tests can instead be run under hypothesis using the following code:
import httpx
from hypothesis import HealthCheck
from hypothesis import given
from hypothesis import settings
from hypothesis.provisional import urls
from hypothesis.strategies import register_type_strategy
from pydantic import AnyHttpUrl
from respx import MockRouter
register_type_strategy(AnyHttpUrl, urls())
@given(...)
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
async def test_respx_targeting(respx_mock: MockRouter, url: AnyHttpUrl) -> None:
# Reset the function scoped fixture as it is used with hypothesis
respx_mock.reset()
route = respx_mock.get(url=url).respond(status_code=200)
result = httpx.get(url)
assert result.status_code == 200
assert route.called
To ensure that the property holds for "all" urls.
Thanks for reporting @Skeen. I've narrowed down the problem to the Path pattern here.
Both urljoin("/", "///") and httpx.URL("///") turns e.g. /// into a single / path 🤔
Looked a bit more, and regarding urljoin, we'll need to change to a simple conditional slash prepend.
Regarding httpx.URL("///"), this unfortunately is treated as first two first // as scheme/host delimiter, and a single / path 😅 . Could be fixed by simply adding that, e.g. httpx.URL("scheme://host" + path).path