Repository: getsentry/responses
Pull request #427 (2021-11-01 10:00:53)

mbeliaev/identifier
same as #108 but,
since by default identifier is ignored by `responses` we just use existing mechanism with `matchers`

this helps us not to expose additional argument in `Response` and follows logic of the rest of the library


Patch:
diff --git a/CHANGES b/CHANGES
index 5f885bf..f401a5c 100644
--- a/CHANGES
+++ b/CHANGES
@@ -3,6 +3,8 @@
 
 * Fixed regression with ``stream`` parameter deprecation, requests.session() and cookie handling.
 * Replaced adhoc URL parsing with `urllib.parse`.
+* Added `responses.matchers.fragment_identifier_matcher`. This matcher allows you
+  to match request URL fragment identifier.
 
 0.15.0
 ------
diff --git a/README.rst b/README.rst
index 801016d..f04dfb3 100644
--- a/README.rst
+++ b/README.rst
@@ -4,9 +4,6 @@ Responses
 .. image:: https://img.shields.io/pypi/v/responses.svg
     :target: https://pypi.python.org/pypi/responses/
 
-..  image:: https://travis-ci.org/getsentry/responses.svg?branch=master
-    :target: https://travis-ci.org/getsentry/responses
-
 .. image:: https://img.shields.io/pypi/pyversions/responses.svg
     :target: https://pypi.org/project/responses/
 
@@ -139,6 +136,7 @@ match (``list``)
     * kwargs provided to request e.g. ``stream``, ``verify``
     * 'multipart/form-data' content and headers in request
     * request headers
+    * request fragment identifier
 
     Alternatively user can create custom matcher.
     Read more `Matching Requests`_
@@ -273,6 +271,33 @@ to the request:
     my_func()
     # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs.
 
+
+To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``.
+The matcher takes fragment string (everything after ``#`` sign) as input for comparison:
+
+.. code-block:: python
+
+    import requests
+    import responses
+    from responses.matchers import fragment_identifier_matcher
+
+    @responses.activate
+    def run():
+        url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
+        responses.add(
+            responses.GET,
+            url,
+            match_querystring=True,
+            match=[fragment_identifier_matcher("test=1&foo=bar")],
+            body=b"test",
+        )
+
+        # two requests to check reversed order of fragment identifier
+        resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
+        resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")
+
+    run()
+
 Matching Request Headers
 ------------------------
 
diff --git a/responses/__init__.py b/responses/__init__.py
index 365ff80..7de3b64 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -163,8 +163,6 @@ def get_wrapped(func, responses):
 
         # Preserve the argspec for the wrapped function so that testing
         # tools such as pytest can continue to use their fixture injection.
-        if hasattr(func, "__self__"):
-            args = args[1:]  # Omit 'self'
         func_args = inspect.formatargspec(args, a, kw, None)
     else:
         signature = inspect.signature(func)
diff --git a/responses/matchers.py b/responses/matchers.py
index fe4ceb2..f90edef 100644
--- a/responses/matchers.py
+++ b/responses/matchers.py
@@ -4,9 +4,10 @@ import json as json_module
 from requests import PreparedRequest
 
 if six.PY2:
-    from urlparse import parse_qsl
+    from urlparse import parse_qsl, urlparse
 else:
-    from urllib.parse import parse_qsl
+    from urllib.parse import parse_qsl, urlparse
+
 
 try:
     from requests.packages.urllib3.util.url import parse_url
@@ -117,6 +118,26 @@ def json_params_matcher(params):
     return match
 
 
+def fragment_identifier_matcher(identifier):
+    def match(request):
+        reason = ""
+        url_fragment = urlparse(request.url).fragment
+        if identifier:
+            url_fragment_qsl = sorted(parse_qsl(url_fragment))
+            identifier_qsl = sorted(parse_qsl(identifier))
+            valid = identifier_qsl == url_fragment_qsl
+        else:
+            valid = not url_fragment
+
+        if not valid:
+            reason = "URL fragment identifier is different: {} doesn't match {}".format(
+                identifier, url_fragment
+            )
+        return valid, reason
+
+    return match
+
+
 def query_param_matcher(params):
     """
     Matcher to match 'params' argument in request
diff --git a/responses/matchers.pyi b/responses/matchers.pyi
index ea692ad..188de2e 100644
--- a/responses/matchers.pyi
+++ b/responses/matchers.pyi
@@ -38,3 +38,7 @@ def header_matcher(
     headers: Dict[str, str],
     strict_match: bool = ...
 ) -> Callable[..., Any]: ...
+
+def fragment_identifier_matcher(
+    identifier: Optional[str]
+) -> Callable[..., Any]: ...


Test patch:
diff --git a/responses/test_responses.py b/responses/test_responses.py
index 1f6a51f..9c02fe6 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -710,6 +710,12 @@ def test_regular_expression_url():
     assert_reset()
 
 
+def test_base_response_get_response():
+    resp = BaseResponse("GET", ".com")
+    with pytest.raises(NotImplementedError):
+        resp.get_response(requests.PreparedRequest())
+
+
 def test_custom_adapter():
     @responses.activate
     def run():
@@ -777,6 +783,29 @@ def test_activate_doesnt_change_signature():
     assert decorated_test_function(3) == test_function(3)
 
 
+@pytest.fixture
+def my_fruit():
+    return "apple"
+
+
+@pytest.fixture
+def fruit_basket(my_fruit):
+    return ["banana", my_fruit]
+
+
+@pytest.mark.usefixtures("my_fruit", "fruit_basket")
+class TestFixtures(object):
+    """
+    Test that pytest fixtures work well with 'activate' decorator
+    """
+
+    def test_function(self, my_fruit, fruit_basket):
+        assert my_fruit in fruit_basket
+        assert my_fruit == "apple"
+
+    test_function_decorated = responses.activate(test_function)
+
+
 def test_activate_mock_interaction():
     @patch("sys.stdout")
     def test_function(mock_stdout):
@@ -802,7 +831,7 @@ def test_activate_mock_interaction():
 @pytest.mark.skipif(six.PY2, reason="Cannot run in python2")
 def test_activate_doesnt_change_signature_with_return_type():
     def test_function(a, b=None):
-        return (a, b)
+        return a, b
 
     # Add type annotations as they are syntax errors in py2.
     # Use a class to test for import errors in evaled code.
@@ -810,14 +839,10 @@ def test_activate_doesnt_change_signature_with_return_type():
     test_function.__annotations__["a"] = Mock
 
     decorated_test_function = responses.activate(test_function)
-    if hasattr(inspect, "signature"):
-        assert inspect.signature(test_function) == inspect.signature(
-            decorated_test_function
-        )
-    else:
-        assert inspect.getargspec(test_function) == inspect.getargspec(
-            decorated_test_function
-        )
+    assert inspect.signature(test_function) == inspect.signature(
+        decorated_test_function
+    )
+
     assert decorated_test_function(1, 2) == test_function(1, 2)
     assert decorated_test_function(3) == test_function(3)
 
@@ -2081,6 +2106,74 @@ def test_request_matches_headers_strict_match():
     assert_reset()
 
 
+def test_fragment_identifier_matcher():
+    @responses.activate
+    def run():
+        responses.add(
+            responses.GET,
+            "http://example.com",
+            match=[matchers.fragment_identifier_matcher("test=1&foo=bar")],
+            body=b"test",
+        )
+
+        resp = requests.get("http://example.com#test=1&foo=bar")
+        assert_response(resp, "test")
+
+    run()
+    assert_reset()
+
+
+def test_fragment_identifier_matcher_error():
+    @responses.activate
+    def run():
+        responses.add(
+            responses.GET,
+            "http://example.com/",
+            match=[matchers.fragment_identifier_matcher("test=1")],
+        )
+        responses.add(
+            responses.GET,
+            "http://example.com/",
+            match=[matchers.fragment_identifier_matcher(None)],
+        )
+
+        with pytest.raises(ConnectionError) as excinfo:
+            requests.get("http://example.com/#test=2")
+
+        msg = str(excinfo.value)
+        assert (
+            "URL fragment identifier is different: test=1 doesn't match test=2"
+        ) in msg
+        assert (
+            "URL fragment identifier is different: None doesn't match test=2"
+        ) in msg
+
+    run()
+    assert_reset()
+
+
+def test_fragment_identifier_matcher_and_match_querystring():
+    @responses.activate
+    def run():
+        url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
+        responses.add(
+            responses.GET,
+            url,
+            match_querystring=True,
+            match=[matchers.fragment_identifier_matcher("test=1&foo=bar")],
+            body=b"test",
+        )
+
+        # two requests to check reversed order of fragment identifier
+        resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
+        assert_response(resp, "test")
+        resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")
+        assert_response(resp, "test")
+
+    run()
+    assert_reset()
+
+
 @pytest.mark.parametrize(
     "response_params, expected_representation",
     [