diff --git a/README.md b/README.md index 91315d3..71c68e5 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,15 @@ with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']): requests.get('http://api.com/getdata?api_key=secretstring') ``` +### Filter information from HTTP post data +Use the `filter_post_data_parameters` configuration option with a list of query +parameters to filter. + +```python +with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['client_secret']): + requests.post('http://api.com/postdata', data={'api_key': 'secretstring'}) +``` + ### Custom Request filtering If neither of these covers your request filtering needs, you can register a callback diff --git a/tests/integration/test_filter.py b/tests/integration/test_filter.py index c3e6582..2c5bae3 100644 --- a/tests/integration/test_filter.py +++ b/tests/integration/test_filter.py @@ -1,6 +1,7 @@ import base64 import pytest from six.moves.urllib.request import urlopen, Request +from six.moves.urllib.parse import urlencode from six.moves.urllib.error import HTTPError import vcr @@ -55,6 +56,16 @@ def test_filter_querystring(tmpdir): assert 'foo' not in cass.requests[0].url +def test_filter_post_data(tmpdir): + url = 'http://httpbin.org/post' + data = urlencode({'id': 'secret', 'foo': 'bar'}).encode('utf-8') + cass_file = str(tmpdir.join('filter_pd.yaml')) + with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']): + urlopen(url, data) + with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']) as cass: + assert b'id=secret' not in cass.requests[0].body + + def test_filter_callback(tmpdir): url = 'http://httpbin.org/get' cass_file = str(tmpdir.join('basic_auth_filter.yaml')) diff --git a/tests/unit/test_filters.py b/tests/unit/test_filters.py index 2629c5e..546579a 100644 --- a/tests/unit/test_filters.py +++ b/tests/unit/test_filters.py @@ -1,4 +1,8 @@ -from vcr.filters import remove_headers, remove_query_parameters +from vcr.filters import ( + remove_headers, + remove_query_parameters, + remove_post_data_parameters +) from vcr.request import Request @@ -35,3 +39,31 @@ def test_remove_nonexistent_query_parameters(): request = Request('GET', uri, '', {}) remove_query_parameters(request, ['w', 'q']) assert request.uri == 'http://g.com/' + + +def test_remove_post_data_parameters(): + body = b'id=secret&foo=bar' + request = Request('POST', 'http://google.com', body, {}) + remove_post_data_parameters(request, ['id']) + assert request.body == b'foo=bar' + + +def test_preserve_multiple_post_data_parameters(): + body = b'id=secret&foo=bar&foo=baz' + request = Request('POST', 'http://google.com', body, {}) + remove_post_data_parameters(request, ['id']) + assert request.body == b'foo=bar&foo=baz' + + +def test_remove_all_post_data_parameters(): + body = b'id=secret&foo=bar' + request = Request('POST', 'http://google.com', body, {}) + remove_post_data_parameters(request, ['id', 'foo']) + assert request.body == b'' + + +def test_remove_nonexistent_post_data_parameters(): + body = b'' + request = Request('POST', 'http://google.com', body, {}) + remove_post_data_parameters(request, ['id']) + assert request.body == b'' diff --git a/vcr/config.py b/vcr/config.py index 88ae535..47f7540 100644 --- a/vcr/config.py +++ b/vcr/config.py @@ -13,8 +13,8 @@ class VCR(object): def __init__(self, serializer='yaml', cassette_library_dir=None, record_mode="once", filter_headers=(), custom_patches=(), - filter_query_parameters=(), before_record_request=None, - before_record_response=None, ignore_hosts=(), + filter_query_parameters=(), filter_post_data_parameters=(), + before_record_request=None, before_record_response=None, ignore_hosts=(), match_on=('method', 'scheme', 'host', 'port', 'path', 'query',), ignore_localhost=False, before_record=None): self.serializer = serializer @@ -39,6 +39,7 @@ class VCR(object): self.record_mode = record_mode self.filter_headers = filter_headers self.filter_query_parameters = filter_query_parameters + 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.ignore_hosts = ignore_hosts @@ -121,6 +122,9 @@ class VCR(object): filter_query_parameters = options.get( 'filter_query_parameters', self.filter_query_parameters ) + filter_post_data_parameters = options.get( + 'filter_post_data_parameters', self.filter_post_data_parameters + ) before_record_request = options.get( "before_record_request", options.get("before_record", self.before_record_request) ) @@ -137,6 +141,10 @@ class VCR(object): filter_functions.append(functools.partial(filters.remove_query_parameters, query_parameters_to_remove=filter_query_parameters)) + if filter_post_data_parameters: + filter_functions.append(functools.partial(filters.remove_post_data_parameters, + post_data_parameters_to_remove=filter_post_data_parameters)) + hosts_to_ignore = list(ignore_hosts) if ignore_localhost: hosts_to_ignore.extend(('localhost', '0.0.0.0', '127.0.0.1')) diff --git a/vcr/filters.py b/vcr/filters.py index 7e79451..5293a09 100644 --- a/vcr/filters.py +++ b/vcr/filters.py @@ -1,4 +1,6 @@ +from six import BytesIO from six.moves.urllib.parse import urlparse, urlencode, urlunparse +from collections import OrderedDict import copy @@ -22,3 +24,17 @@ def remove_query_parameters(request, query_parameters_to_remove): uri_parts[4] = urlencode(new_query) request.uri = urlunparse(uri_parts) return request + + +def remove_post_data_parameters(request, post_data_parameters_to_remove): + if request.method == 'POST' and not isinstance(request.body, BytesIO): + post_data = OrderedDict() + for k, sep, v in [p.partition(b'=') for p in request.body.split(b'&')]: + if k in post_data: + post_data[k].append(v) + elif len(k) > 0 and k.decode('utf-8') not in post_data_parameters_to_remove: + post_data[k] = [v] + request.body = b'&'.join( + b'='.join([k, v]) + for k, vals in post_data.items() for v in vals) + return request