Skip to content

Commit

Permalink
fix(matchers): Don't sort failed matches when printing error message (#…
Browse files Browse the repository at this point in the history
…711)

Delete `_create_key_val_str` which was introduced for the compatibility
between python2 (not supported anymore) and python3.

Fixes GH-704
  • Loading branch information
mgaligniana committed May 8, 2024
1 parent 2bc3496 commit 6791922
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 111 deletions.
5 changes: 5 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
0.25.2
------

* Fixed error messages when matches fail: inputs are not sorted or reformatted. See #704

0.25.1
------

Expand Down
71 changes: 12 additions & 59 deletions responses/matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,6 @@
from urllib3.util.url import parse_url


def _create_key_val_str(input_dict: Union[Mapping[Any, Any], Any]) -> str:
"""
Returns string of format {'key': val, 'key2': val2}
Function is called recursively for nested dictionaries
:param input_dict: dictionary to transform
:return: (str) reformatted string
"""

def list_to_str(input_list: List[str]) -> str:
"""
Convert all list items to string.
Function is called recursively for nested lists
"""
converted_list = []
for item in sorted(input_list, key=lambda x: str(x)):
if isinstance(item, dict):
item = _create_key_val_str(item)
elif isinstance(item, list):
item = list_to_str(item)

converted_list.append(str(item))
list_str = ", ".join(converted_list)
return "[" + list_str + "]"

items_list = []
for key in sorted(input_dict.keys(), key=lambda x: str(x)):
val = input_dict[key]
if isinstance(val, dict):
val = _create_key_val_str(val)
elif isinstance(val, list):
val = list_to_str(input_list=val)

items_list.append(f"{key}: {val}")

key_val_str = "{{{}}}".format(", ".join(items_list))
return key_val_str


def _filter_dict_recursively(
dict1: Mapping[Any, Any], dict2: Mapping[Any, Any]
) -> Mapping[Any, Any]:
Expand Down Expand Up @@ -91,8 +52,8 @@ def match(request: PreparedRequest) -> Tuple[bool, str]:
params_dict = params or {}
valid = params is None if request_body is None else params_dict == qsl_body
if not valid:
reason = "request.body doesn't match: {} doesn't match {}".format(
_create_key_val_str(qsl_body), _create_key_val_str(params_dict)
reason = (
f"request.body doesn't match: {qsl_body} doesn't match {params_dict}"
)

return valid, reason
Expand Down Expand Up @@ -146,13 +107,7 @@ def match(request: PreparedRequest) -> Tuple[bool, str]:
valid = params is None if request_body is None else json_params == json_body

if not valid:
if isinstance(json_body, dict) and isinstance(json_params, dict):
reason = "request.body doesn't match: {} doesn't match {}".format(
_create_key_val_str(json_body), _create_key_val_str(json_params)
)
else:
reason = f"request.body doesn't match: {json_body} doesn't match {json_params}"

reason = f"request.body doesn't match: {json_body} doesn't match {json_params}"
if not strict_match:
reason += (
"\nNote: You use non-strict parameters check, "
Expand Down Expand Up @@ -234,10 +189,7 @@ def match(request: PreparedRequest) -> Tuple[bool, str]:
valid = sorted(params_dict.items()) == sorted(request_params_dict.items())

if not valid:
reason = "Parameters do not match. {} doesn't match {}".format(
_create_key_val_str(request_params_dict),
_create_key_val_str(params_dict),
)
reason = f"Parameters do not match. {request_params_dict} doesn't match {params_dict}"
if not strict_match:
reason += (
"\nYou can use `strict_match=True` to do a strict parameters check."
Expand Down Expand Up @@ -267,9 +219,9 @@ def match(request: PreparedRequest) -> Tuple[bool, str]:
valid = not query if request_query is None else request_qsl == matcher_qsl

if not valid:
reason = "Query string doesn't match. {} doesn't match {}".format(
_create_key_val_str(dict(request_qsl)),
_create_key_val_str(dict(matcher_qsl)),
reason = (
"Query string doesn't match. "
f"{dict(request_qsl)} doesn't match {dict(matcher_qsl)}"
)

return valid, reason
Expand Down Expand Up @@ -299,8 +251,8 @@ def match(request: PreparedRequest) -> Tuple[bool, str]:
)

if not valid:
reason = "Arguments don't match: {} doesn't match {}".format(
_create_key_val_str(request_kwargs), _create_key_val_str(kwargs_dict)
reason = (
f"Arguments don't match: {request_kwargs} doesn't match {kwargs_dict}"
)

return valid, reason
Expand Down Expand Up @@ -436,8 +388,9 @@ def match(request: PreparedRequest) -> Tuple[bool, str]:
valid = _compare_with_regex(request_headers)

if not valid:
return False, "Headers do not match: {} doesn't match {}".format(
_create_key_val_str(request_headers), _create_key_val_str(headers)
return (
False,
f"Headers do not match: {request_headers} doesn't match {headers}",
)

return valid, ""
Expand Down
96 changes: 44 additions & 52 deletions responses/tests/test_matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,27 @@ def run():
)
assert (
"- POST http://example.com/ request.body doesn't match: "
"{page: {type: json}} doesn't match {page: {diff: value, type: json}}"
"{'page': {'type': 'json'}} doesn't match {'page': {'type': 'json', 'diff': 'value'}}"
) in str(exc.value)

run()
assert_reset()


def test_failed_matchers_dont_modify_inputs_order_in_error_message():
json_a = {"array": ["C", "B", "A"]}
json_b = '{"array" : ["B", "A", "C"]}'
mock_request = Mock(body=json_b)
result = matchers.json_params_matcher(json_a)(mock_request)
assert result == (
False,
(
"request.body doesn't match: {'array': ['B', 'A', 'C']} "
"doesn't match {'array': ['C', 'B', 'A']}"
),
)


def test_json_params_matcher_json_list():
json_a = [{"a": "b"}]
json_b = '[{"a": "b", "c": "d"}]'
Expand Down Expand Up @@ -250,7 +264,7 @@ def run():

assert (
"- GET https://example.com/ Parameters do not match. {} doesn't"
" match {does_not_exist: test}\n"
" match {'does_not_exist': 'test'}\n"
"You can use `strict_match=True` to do a strict parameters check."
) in str(exc.value)

Expand Down Expand Up @@ -304,7 +318,7 @@ def run():
)

msg = str(excinfo.value)
assert "request.body doesn't match: {my: data} doesn't match {}" in msg
assert "request.body doesn't match: {'my': 'data'} doesn't match {}" in msg

with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
rsps.add(
Expand All @@ -322,9 +336,9 @@ def run():
)
msg = str(excinfo.value)
assert (
"request.body doesn't match: {page: second, type: urlencoded} doesn't match {}"
in msg
)
"request.body doesn't match: {'page': 'second', "
"'type': 'urlencoded'} doesn't match {}"
) in msg

run()
assert_reset()
Expand Down Expand Up @@ -389,7 +403,8 @@ def run():

msg = str(excinfo.value)
assert (
"request.body doesn't match: {id: bad} doesn't match {foo: bar}" in msg
"request.body doesn't match: {'id': 'bad'} doesn't match {'foo': 'bar'}"
in msg
)

assert (
Expand Down Expand Up @@ -418,10 +433,11 @@ def run():

msg = str(excinfo.value)
assert (
"Parameters do not match. {id: bad} doesn't match {my: params}" in msg
"Parameters do not match. {'id': 'bad'} doesn't match {'my': 'params'}"
in msg
)
assert (
"request.body doesn't match: {page: two} doesn't match {page: one}"
"request.body doesn't match: {'page': 'two'} doesn't match {'page': 'one'}"
in msg
)

Expand All @@ -442,7 +458,7 @@ def run():
msg = str(excinfo.value)
assert (
"Arguments don't match: "
"{stream: True, verify: True} doesn't match {stream: True, verify: False}"
"{'stream': True, 'verify': True} doesn't match {'stream': True, 'verify': False}"
) in msg

run()
Expand Down Expand Up @@ -587,9 +603,9 @@ def run():

msg = str(excinfo.value)
assert (
"Query string doesn't match. {didi: pro, test: 1} doesn't match {didi: pro}"
in msg
)
"Query string doesn't match. {'didi': 'pro', 'test': '1'} "
"doesn't match {'didi': 'pro'}"
) in msg

run()
assert_reset()
Expand Down Expand Up @@ -642,8 +658,8 @@ def run():

msg = str(excinfo.value)
assert (
"Headers do not match: {Accept: application/xml} doesn't match "
"{Accept: application/json}"
"Headers do not match: {'Accept': 'application/xml'} doesn't match "
"{'Accept': 'application/json'}"
) in msg

run()
Expand All @@ -665,7 +681,9 @@ def run():
requests.get(url, headers={})

msg = str(excinfo.value)
assert ("Headers do not match: {} doesn't match {x-custom-header: foo}") in msg
assert (
"Headers do not match: {} doesn't match {'x-custom-header': 'foo'}"
) in msg

run()
assert_reset()
Expand Down Expand Up @@ -716,8 +734,8 @@ def run():

msg = str(excinfo.value)
assert (
"Headers do not match: {Accept: text/plain, Accept-Charset: utf-8} "
"doesn't match {Accept: text/plain}"
"Headers do not match: {'Accept': 'text/plain', 'Accept-Charset': 'utf-8'} "
"doesn't match {'Accept': 'text/plain'}"
) in msg

run()
Expand Down Expand Up @@ -792,31 +810,6 @@ def run():
assert_reset()


def test_matchers_create_key_val_str():
"""
Test that matchers._create_key_val_str does recursive conversion
"""
data = {
"my_list": [
1,
2,
"a",
{"key1": "val1", "key2": 2, 3: "test"},
"!",
[["list", "nested"], {"nested": "dict"}],
],
1: 4,
"test": "val",
"high": {"nested": "nested_dict"},
}
conv_str = matchers._create_key_val_str(data)
reference = (
"{1: 4, high: {nested: nested_dict}, my_list: [!, 1, 2, [[list, nested], {nested: dict}], "
"a, {3: test, key1: val1, key2: 2}], test: val}"
)
assert conv_str == reference


class TestHeaderWithRegex:
@property
def url(self): # type: ignore[misc]
Expand Down Expand Up @@ -892,9 +885,9 @@ def run():
session.send(prepped)
msg = str(excinfo.value)
assert (
"Headers do not match: {Accept: text/plain, Message-Signature: "
'signature="123",created=abc} '
"doesn't match {Accept: text/plain, Message-Signature: "
"Headers do not match: {'Accept': 'text/plain', 'Message-Signature': "
"""'signature="123",created=abc'} """
"doesn't match {'Accept': 'text/plain', 'Message-Signature': "
"re.compile('signature=\"\\\\S+\",created=\\\\d+')}"
) in msg

Expand All @@ -921,8 +914,8 @@ def run():
session.send(prepped)
msg = str(excinfo.value)
assert (
"Headers do not match: {Accept: text/plain, Accept-Charset: utf-8} "
"doesn't match {Accept: text/plain, Message-Signature: "
"Headers do not match: {'Accept': 'text/plain', 'Accept-Charset': 'utf-8'} "
"doesn't match {'Accept': 'text/plain', 'Message-Signature': "
"re.compile('signature=\"\\\\S+\",created=\\\\d+')}"
) in msg

Expand Down Expand Up @@ -950,10 +943,9 @@ def run():
session.send(prepped)
msg = str(excinfo.value)
assert (
"Headers do not match: {Accept: text/plain, Accept-Charset: utf-8, "
'Message-Signature: signature="abc",'
"created=1243} "
"doesn't match {Accept: text/plain, Message-Signature: "
"Headers do not match: {'Accept': 'text/plain', 'Accept-Charset': 'utf-8', "
"""'Message-Signature': 'signature="abc",created=1243'} """
"doesn't match {'Accept': 'text/plain', 'Message-Signature': "
"re.compile('signature=\"\\\\S+\",created=\\\\d+')}"
) in msg

Expand Down

0 comments on commit 6791922

Please sign in to comment.