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: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Unreleased
----------

* Fixed matching and passthrough registration for Unicode hostnames with ports
or userinfo, preserving credentials and ports during hostname encoding.
* Fixed ``fragment_identifier_matcher`` ignoring blank-valued fragment
parameters, which allowed missing or different parameters to match.
* Added Python 3.14 support (classifiers, tox envlist, and CI test matrix). See #810
Expand Down
7 changes: 5 additions & 2 deletions responses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,15 @@ def _clean_unicode(url: str) -> str:
urllist = list(urlsplit(url))
netloc = urllist[1]
if _has_unicode(netloc):
domains = netloc.split(".")
# Encode only the hostname, keeping userinfo and the port out of punycode.
userinfo, at, hostport = netloc.rpartition("@")
host, colon, port = hostport.partition(":")
domains = host.split(".")
for i, d in enumerate(domains):
if _has_unicode(d):
d = "xn--" + d.encode("punycode").decode("ascii")
domains[i] = d
urllist[1] = ".".join(domains)
urllist[1] = userinfo + at + ".".join(domains) + colon + port
url = urlunsplit(urllist)

# Clean up path/query/params, which use url-encoding to handle unicode chars
Expand Down
71 changes: 71 additions & 0 deletions responses/tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,77 @@ def run():
assert_reset()


@pytest.mark.parametrize(
"url",
[
"https://example.рф:8443/path",
"https://пример.рф:8443/path",
"https://user:pass@пример.рф/path",
"https://user:pass@пример.рф:8443/path",
"https://üser:päss@example.com:8443/path",
"https://üser:päss@пример.рф:8443/path",
"https://пример.рф:8443/путь?key=значение",
"https://пример.рф/path",
"https://user:pass@example.com:8443/path",
"https://üser:päss@[::1]:8443/path",
],
)
def test_unicode_url_authority(url): # type: ignore[misc]
with responses.RequestsMock() as mock:
mock.get(url, body="matched")

response = requests.get(url)

assert_response(response, "matched")
assert len(mock.calls) == 1


@pytest.mark.parametrize(
("prefix", "expected"),
[
("https://пример.рф:8443", "https://xn--e1afmkfd.xn--p1ai:8443"),
(
"https://user:pass@пример.рф:8443/path",
"https://user:pass@xn--e1afmkfd.xn--p1ai:8443/path",
),
(
"https://üser:päss@example.com:8443/path",
"https://%C3%BCser:p%C3%A4ss@example.com:8443/path",
),
],
)
def test_unicode_authority_passthru(prefix, expected): # type: ignore[misc]
real_send = Mock(return_value=requests.Response())
with responses.RequestsMock(real_adapter_send=real_send) as mock:
mock.add_passthru(prefix)
assert mock.passthru_prefixes == (expected,)

requests.get(prefix + "/child")

real_send.assert_called_once()
assert real_send.call_args[0][1].url == expected + "/child"


@pytest.mark.parametrize(
"url",
[
"https://user:pass@пример.рф:8444/path",
"https://other:pass@пример.рф:8443/path",
"https://user:other@пример.рф:8443/path",
"https://user:pass@пример.рф:8443/other",
],
)
def test_unicode_authority_keeps_url_components_significant(url): # type: ignore[misc]
response = responses.Response(
responses.GET, "https://user:pass@пример.рф:8443/path"
)

matched, reason = response.matches(requests.Request("GET", url).prepare())

assert not matched
assert reason == "URL does not match"


def test_handles_unicode_body():
url = "http://example.com/test"

Expand Down