Skip to content
Merged
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 ``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
* Match response ``Content-Type`` header names case-insensitively so lowercase
headers do not produce duplicate content types or incorrect response decoding.
Expand Down
6 changes: 4 additions & 2 deletions responses/matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,10 @@ def match(request: PreparedRequest) -> Tuple[bool, str]:
if identifier:
if "=" in identifier:
# Query-string-style fragment: compare order-insensitively.
url_fragment_qsl = sorted(parse_qsl(url_fragment)) # type: ignore[type-var]
identifier_qsl = sorted(parse_qsl(identifier))
url_fragment_qsl = sorted(
parse_qsl(url_fragment, keep_blank_values=True) # type: ignore[type-var]
)
identifier_qsl = sorted(parse_qsl(identifier, keep_blank_values=True))
valid = identifier_qsl == url_fragment_qsl
else:
# Opaque fragment (e.g. "/users/5"): parse_qsl() yields no
Expand Down
38 changes: 38 additions & 0 deletions responses/tests/test_matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,44 @@ def run():
assert_reset()


@pytest.mark.parametrize(
"expected, actual, matches",
[
("flag=", "flag=", True),
("flag=", "other=", False),
("flag=", "", False),
("flag=&value=1", "value=1&flag=", True),
("flag=&value=1", "value=1", False),
("flag=&value=1", "flag=present&value=1", False),
("flag=&flag=1", "flag=1&flag=", True),
("flag=&flag=1", "flag=1", False),
],
)
def test_fragment_identifier_matcher_blank_values(
expected: str, actual: str, matches: bool
) -> None:
request = requests.Request("GET", f"http://example.com/#{actual}").prepare()
valid, reason = matchers.fragment_identifier_matcher(expected)(request)
assert valid is matches
if matches:
assert reason == ""
else:
assert "URL fragment identifier is different" in reason


@responses.activate
def test_fragment_identifier_matcher_blank_values_in_requests() -> None:
responses.add(
responses.GET,
"http://example.com/",
match=[matchers.fragment_identifier_matcher("flag=&value=1")],
body=b"test",
)
with pytest.raises(ConnectionError, match="URL fragment identifier is different"):
requests.get("http://example.com/#value=1")
assert_response(requests.get("http://example.com/#value=1&flag="), "test")


def test_fragment_identifier_matcher_opaque():
"""Opaque (non key=value) fragments must compare verbatim.

Expand Down
Loading