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
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Unreleased
This also applies to headers returned by callbacks.
* Fixed ``urlencoded_params_matcher`` rejecting matching form data supplied
as bytes or a binary file-like object.
* Fixed ``urlencoded_params_matcher`` accepting, under ``strict_match``, a
request body with repeated keys that the params dict cannot represent (for
example ``a=1&a=2`` against ``{"a": "2"}``).
* Added ``responses.CallbackRequest`` so callback and matcher annotations can
include the ``params`` and ``req_kwargs`` attributes added by ``responses``.
* Fixed non-strict `json_params_matcher` raising `TypeError` when an actual
Expand Down
19 changes: 18 additions & 1 deletion responses/matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,28 @@ def match(request: PreparedRequest) -> Tuple[bool, str]:
if not params and request_body:
valid = False

# `dict(parse_qsl(...))` above collapses repeated keys (last value wins),
# so a strict match would otherwise accept a body carrying data the dict
# params cannot represent (e.g. body "a=1&a=2" against {"a": "2"}).
repeated_keys = bool(
strict_match
and request_body
and len(parse_qsl(request_body, keep_blank_values=allow_blank))
!= len(qsl_body)
)
if repeated_keys:
valid = False

if not valid:
reason = (
f"request.body doesn't match: {qsl_body} doesn't match {match_params}"
)
if strict_match:
if repeated_keys:
reason += (
"\nNote: the request body has repeated keys that a strict "
"dict match cannot represent."
)
elif strict_match:
reason += (
"\nNote: You're using strict parameter check. "
"To try a partial match, use strict_match=False"
Expand Down
27 changes: 27 additions & 0 deletions responses/tests/test_matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,33 @@ def run():
assert_reset()


def test_urlencoded_params_matcher_strict_repeated_keys():
"""Strict matching must reject a body with repeated keys the params dict
cannot represent (e.g. body "a=1&a=2" against {"a": "2"})."""

@responses.activate
def run():
responses.add(
method=responses.POST,
url="http://example.com/",
body="body1",
match=[matchers.urlencoded_params_matcher({"a": "2"})],
)

# data as a list of pairs encodes to "a=1&a=2"; the stray a=1 must fail
with pytest.raises(ConnectionError) as excinfo:
requests.request(
"POST",
"http://example.com/",
headers={"Content-Type": "x-www-form-urlencoded"},
data=[("a", "1"), ("a", "2")],
)
assert "repeated keys" in str(excinfo.value)

run()
assert_reset()


def test_query_params_numbers():
@responses.activate
def run():
Expand Down