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/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 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,