From 8732145a5c06b89ffe4af3a4b587d53bc78b2cb3 Mon Sep 17 00:00:00 2001 From: Tyler Turner Date: Thu, 19 Jan 2023 11:15:28 -0600 Subject: [PATCH 1/2] Add before_record_interaction Closes #556 #527 Co-authored-by: Edward Delaporte (@edthedev) Co-authored-by: David Riddle (@ddriddle) Co-authored-by: Michelle Pitcel (@mpitcel) Co-authored-by: Zach Carrington (@zdc217) --- docs/advanced.rst | 29 +++++++++++++++++++++++++++-- tests/integration/test_filter.py | 30 ++++++++++++++++++++++++++++++ tests/unit/test_cassettes.py | 10 ++++++++++ vcr/cassette.py | 7 ++++++- vcr/config.py | 4 ++++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index fb287fa3..cd9d2960 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -287,6 +287,30 @@ sensitive data from the response body: with my_vcr.use_cassette('test.yml'): # your http code here +Custom Request & Response Filtering +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also do request and response filtering with the +``before_record_interaction`` configuration option. Its usage is +similar to the above ``before_record_request`` and +``before_record_response`` - you can mutate the response or the request, +or return ``None`` to avoid recording the request and response +altogether. For example to hide sensitive data from the response body: + +.. code:: python + + def scrub_string(string, replacement='', path): + def before_record_interaction(request, response): + if request.path == path: + response['body']['string'] = response['body']['string'].replace(string, replacement) + return request, response + return before_record_interaction + + my_vcr = vcr.VCR( + before_record_interaction=scrub_string(settings.PASSWORD, 'password', '/auth'), + ) + with my_vcr.use_cassette('test.yml'): + # your http code here Decode compressed response --------------------------- @@ -313,8 +337,9 @@ in a few ways: or 0.0.0.0. - Set the ``ignore_hosts`` configuration option to a list of hosts to ignore -- Add a ``before_record_request`` or ``before_record_response`` callback - that returns ``None`` for requests you want to ignore (see above). +- Add a ``before_record_request``, ``before_record_response``, or + ``before_record_interaction`` callback that returns ``None`` for + requests you want to ignore (see above). Requests that are ignored by VCR will not be saved in a cassette, nor played back from a cassette. VCR will completely ignore those requests diff --git a/tests/integration/test_filter.py b/tests/integration/test_filter.py index 5823b8b2..ab3b6177 100644 --- a/tests/integration/test_filter.py +++ b/tests/integration/test_filter.py @@ -96,6 +96,36 @@ def before_record_cb(request): assert len(cass) == 0 +def test_before_record_interaction(tmpdir, httpbin): + url = httpbin.url + "/get" + cass_file = str(tmpdir.join("basic_auth_filter1.yaml")) + + def before_record_interaction_cb(request, response): + if request.path == "/get": + return + return request, response + + my_vcr = vcr.VCR(before_record_interaction=before_record_interaction_cb) + with my_vcr.use_cassette(cass_file, filter_headers=["authorization"]) as cass: + urlopen(url) + assert len(cass) == 0 + + +def test_before_record_interaction_override(tmpdir, httpbin): + url = httpbin.url + "/get" + cass_file = str(tmpdir.join("basic_auth_filter2.yaml")) + + def before_record_interaction_cb(request, response): + if request.path == "/get": + return + return request, response + + my_vcr = vcr.VCR() + with my_vcr.use_cassette(cass_file, before_record_interaction=before_record_interaction_cb) as cass: + urlopen(url) + assert len(cass) == 0 + + def test_decompress_gzip(tmpdir, httpbin): url = httpbin.url + "/gzip" request = Request(url, headers={"Accept-Encoding": ["gzip, deflate"]}) diff --git a/tests/unit/test_cassettes.py b/tests/unit/test_cassettes.py index 41e3df53..07a39864 100644 --- a/tests/unit/test_cassettes.py +++ b/tests/unit/test_cassettes.py @@ -183,6 +183,16 @@ def test_before_record_response(): assert cassette.responses[0] == "mutated" +def test_before_record_interaction(): + before_record_interaction = mock.Mock(return_value=("mutated", "twice")) + cassette = Cassette("test", before_record_interaction=before_record_interaction) + cassette.append("req", "res") + + before_record_interaction.assert_called_once_with("req", "res") + assert cassette.requests[0] == "mutated" + assert cassette.responses[0] == "twice" + + def assert_get_response_body_is(value): conn = httplib.HTTPConnection("www.python.org") conn.request("GET", "/index.html") diff --git a/vcr/cassette.py b/vcr/cassette.py index 5822afac..bce58fd1 100644 --- a/vcr/cassette.py +++ b/vcr/cassette.py @@ -194,6 +194,7 @@ def __init__( match_on=(uri, method), before_record_request=None, before_record_response=None, + before_record_interaction=None, custom_patches=(), inject=False, allow_playback_repeats=False, @@ -205,6 +206,7 @@ def __init__( self._before_record_request = before_record_request or (lambda x: x) log.info(self._before_record_request) self._before_record_response = before_record_response or (lambda x: x) + self._before_record_interaction = before_record_interaction or (lambda x, y: (x, y)) self.inject = inject self.record_mode = record_mode self.custom_patches = custom_patches @@ -249,7 +251,10 @@ def append(self, request, response): response = self._before_record_response(response) if response is None: return - self.data.append((request, response)) + interaction = self._before_record_interaction(request, response) + if interaction is None: + return + self.data.append(interaction) self.dirty = True def filter_request(self, request): diff --git a/vcr/config.py b/vcr/config.py index a991c958..18daa6fa 100644 --- a/vcr/config.py +++ b/vcr/config.py @@ -41,6 +41,7 @@ def __init__( ignore_localhost=False, filter_headers=(), before_record_response=None, + before_record_interaction=None, filter_post_data_parameters=(), match_on=("method", "scheme", "host", "port", "path", "query"), before_record=None, @@ -75,6 +76,7 @@ def __init__( self.filter_post_data_parameters = filter_post_data_parameters self.before_record_request = before_record_request or before_record self.before_record_response = before_record_response + self.before_record_interaction = before_record_interaction self.ignore_hosts = ignore_hosts self.ignore_localhost = ignore_localhost self.inject_cassette = inject_cassette @@ -126,6 +128,7 @@ def get_merged_config(self, **kwargs): cassette_library_dir = kwargs.get("cassette_library_dir", self.cassette_library_dir) additional_matchers = kwargs.get("additional_matchers", ()) record_on_exception = kwargs.get("record_on_exception", self.record_on_exception) + before_record_interaction = kwargs.get("before_record_interaction", self.before_record_interaction) if cassette_library_dir: @@ -147,6 +150,7 @@ def add_cassette_library_dir(path): "record_mode": kwargs.get("record_mode", self.record_mode), "before_record_request": self._build_before_record_request(kwargs), "before_record_response": self._build_before_record_response(kwargs), + "before_record_interaction": before_record_interaction, "custom_patches": self._custom_patches + kwargs.get("custom_patches", ()), "inject": kwargs.get("inject_cassette", self.inject_cassette), "path_transformer": path_transformer, From 1af2a274bfd32713d0b64d5e66866202316b8268 Mon Sep 17 00:00:00 2001 From: Tyler Turner Date: Thu, 19 Jan 2023 15:23:20 -0600 Subject: [PATCH 2/2] Update changelog.rst --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index ad475e8a..25c28101 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,8 @@ For a full list of triaged issues, bugs and PRs and what release they are target All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out. +- 4.3.0 + - Add `before_record_interaction` (thanks @edthedev, @ddriddle, @mpitcel, @zdc217, @tzturner) - 4.2.1 - Fix a bug where the first request in a redirect chain was not being recorded with aiohttp - Various typos and small fixes, thanks @jairhenrique, @timgates42