Enable case sensitive matching
When matching URLs both strings are always lowercased to provide case insensitive matching. Whilst this makes sense for the protocol and the host names it does not necessarily hold true for paths and query strings. A byproduct of this is that the lowercased strings are being reported in request_history which makes it harder to verify requests you made. We enable globally and per adapter setting case sensitive matching. This is intended to become the default in future releases. Change-Id: I7bde70a52995ecf31a0eaeff96f2823a1a6682b2 Closes-Bug: #1584008
This commit is contained in:
parent
e42b9828dc
commit
1b08dcc705
@ -11,6 +11,7 @@ Contents:
|
||||
mocker
|
||||
matching
|
||||
response
|
||||
knownissues
|
||||
history
|
||||
adapter
|
||||
contrib
|
||||
|
34
docs/knownissues.rst
Normal file
34
docs/knownissues.rst
Normal file
@ -0,0 +1,34 @@
|
||||
============
|
||||
Known Issues
|
||||
============
|
||||
|
||||
.. _case_insensitive:
|
||||
|
||||
Case Insensitivity
|
||||
------------------
|
||||
|
||||
By default matching is done in a completely case insensitive way. This makes
|
||||
sense for the protocol and host components which are defined as insensitive by
|
||||
RFCs however it does not make sense for path.
|
||||
|
||||
A byproduct of this is that when using request history the values for path, qs
|
||||
etc are all lowercased as this was what was used to do the matching.
|
||||
|
||||
To work around this when building an Adapter or Mocker you do
|
||||
|
||||
.. code:: python
|
||||
|
||||
with requests_mock.mock(case_sensitive=True) as m:
|
||||
...
|
||||
|
||||
or you can override the default globally by
|
||||
|
||||
.. code:: python
|
||||
|
||||
requests_mock.mock.case_sensitive = True
|
||||
|
||||
It is recommended to run the global fix as it is intended that case sensitivity
|
||||
will become the default in future releases.
|
||||
|
||||
Note that even with case_sensitive enabled the protocol and netloc of a mock
|
||||
are still matched in a case insensitive way.
|
@ -38,6 +38,18 @@ The examples in this file are loaded with:
|
||||
>>> session = requests.Session()
|
||||
>>> session.mount('mock', adapter)
|
||||
|
||||
.. note::
|
||||
|
||||
By default all matching is case insensitive. This can be adjusted by
|
||||
passing case_sensitive=True when creating a mocker or adapter or globally
|
||||
by doing:
|
||||
|
||||
.. code:: python
|
||||
|
||||
requests_mock.mock.case_sensitive = True
|
||||
|
||||
for more see: :ref:`case_insensitive`
|
||||
|
||||
Simple
|
||||
======
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
---
|
||||
prelude: >
|
||||
It is now possible to make URL matching and request history not lowercase
|
||||
the provided URLs.
|
||||
features:
|
||||
- You can pass case_sensitive=True to an adapter or set
|
||||
`requests_mock.mock.case_sensitive = True` globally to enable case
|
||||
sensitive matching.
|
||||
upgrade:
|
||||
- It is recommended you add `requests_mock.mock.case_sensitive = True` to
|
||||
your base test file to globally turn on case sensitive matching as this
|
||||
will become the default in a 2.X release.
|
||||
fixes:
|
||||
- Reported in bug \#1584008 all request matching is done in a case
|
||||
insensitive way, as a byproduct of this request history is handled in a
|
||||
case insensitive way. This can now be controlled by setting case_sensitive
|
||||
to True when creating an adapter or globally.
|
@ -46,13 +46,22 @@ class _RequestObjectProxy(object):
|
||||
self._cert = kwargs.pop('cert', None)
|
||||
self._proxies = copy.deepcopy(kwargs.pop('proxies', {}))
|
||||
|
||||
# FIXME(jamielennox): This is part of bug #1584008 and should default
|
||||
# to True (or simply removed) in a major version bump.
|
||||
self._case_sensitive = kwargs.pop('case_sensitive', False)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._request, name)
|
||||
|
||||
@property
|
||||
def _url_parts(self):
|
||||
if self._url_parts_ is None:
|
||||
self._url_parts_ = urlparse.urlparse(self._request.url.lower())
|
||||
url = self._request.url
|
||||
|
||||
if not self._case_sensitive:
|
||||
url = url.lower()
|
||||
|
||||
self._url_parts_ = urlparse.urlparse(url)
|
||||
|
||||
return self._url_parts_
|
||||
|
||||
@ -165,7 +174,7 @@ class _Matcher(_RequestHistoryTracker):
|
||||
"""Contains all the information about a provided URL to match."""
|
||||
|
||||
def __init__(self, method, url, responses, complete_qs, request_headers,
|
||||
real_http):
|
||||
real_http, case_sensitive):
|
||||
"""
|
||||
:param bool complete_qs: Match the entire query string. By default URLs
|
||||
match if all the provided matcher query arguments are matched and
|
||||
@ -176,15 +185,29 @@ class _Matcher(_RequestHistoryTracker):
|
||||
|
||||
self._method = method
|
||||
self._url = url
|
||||
try:
|
||||
self._url_parts = urlparse.urlparse(url.lower())
|
||||
except:
|
||||
self._url_parts = None
|
||||
self._responses = responses
|
||||
self._complete_qs = complete_qs
|
||||
self._request_headers = request_headers
|
||||
self._real_http = real_http
|
||||
|
||||
# url can be a regex object or ANY so don't always run urlparse
|
||||
if isinstance(url, six.string_types):
|
||||
url_parts = urlparse.urlparse(url)
|
||||
self._scheme = url_parts.scheme.lower()
|
||||
self._netloc = url_parts.netloc.lower()
|
||||
self._path = url_parts.path or '/'
|
||||
self._query = url_parts.query
|
||||
|
||||
if not case_sensitive:
|
||||
self._path = self._path.lower()
|
||||
self._query = self._query.lower()
|
||||
|
||||
else:
|
||||
self._scheme = None
|
||||
self._netloc = None
|
||||
self._path = None
|
||||
self._query = None
|
||||
|
||||
def _match_method(self, request):
|
||||
if self._method is ANY:
|
||||
return True
|
||||
@ -202,18 +225,20 @@ class _Matcher(_RequestHistoryTracker):
|
||||
if hasattr(self._url, 'search'):
|
||||
return self._url.search(request.url) is not None
|
||||
|
||||
if self._url_parts.scheme and request.scheme != self._url_parts.scheme:
|
||||
# scheme is always matched case insensitive
|
||||
if self._scheme and request.scheme.lower() != self._scheme:
|
||||
return False
|
||||
|
||||
if self._url_parts.netloc and request.netloc != self._url_parts.netloc:
|
||||
# netloc is always matched case insensitive
|
||||
if self._netloc and request.netloc.lower() != self._netloc:
|
||||
return False
|
||||
|
||||
if (request.path or '/') != (self._url_parts.path or '/'):
|
||||
if (request.path or '/') != self._path:
|
||||
return False
|
||||
|
||||
# construct our own qs structure as we remove items from it below
|
||||
request_qs = urlparse.parse_qs(request.query)
|
||||
matcher_qs = urlparse.parse_qs(self._url_parts.query)
|
||||
matcher_qs = urlparse.parse_qs(self._query)
|
||||
|
||||
for k, vals in six.iteritems(matcher_qs):
|
||||
for v in vals:
|
||||
@ -279,12 +304,15 @@ class Adapter(BaseAdapter, _RequestHistoryTracker):
|
||||
"""A fake adapter than can return predefined responses.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
def __init__(self, case_sensitive=False):
|
||||
super(Adapter, self).__init__()
|
||||
self._case_sensitive = case_sensitive
|
||||
self._matchers = []
|
||||
|
||||
def send(self, request, **kwargs):
|
||||
request = _RequestObjectProxy(request, **kwargs)
|
||||
request = _RequestObjectProxy(request,
|
||||
case_sensitive=self._case_sensitive,
|
||||
**kwargs)
|
||||
self._add_to_history(request)
|
||||
|
||||
for matcher in reversed(self._matchers):
|
||||
@ -323,10 +351,17 @@ class Adapter(BaseAdapter, _RequestHistoryTracker):
|
||||
elif not response_list:
|
||||
response_list = [] if real_http else [kwargs]
|
||||
|
||||
# NOTE(jamielennox): case_sensitive is not present as a kwarg because i
|
||||
# think there would be an edge case where the adapter and register_uri
|
||||
# had different values.
|
||||
# Ideally case_sensitive would be a value passed to match() however
|
||||
# this would change the contract of matchers so we pass ito to the
|
||||
# proxy and the matcher seperately.
|
||||
responses = [response._MatcherResponse(**k) for k in response_list]
|
||||
matcher = _Matcher(method,
|
||||
url,
|
||||
responses,
|
||||
case_sensitive=self._case_sensitive,
|
||||
complete_qs=complete_qs,
|
||||
request_headers=request_headers,
|
||||
real_http=real_http)
|
||||
|
@ -39,8 +39,28 @@ class MockerCore(object):
|
||||
'called',
|
||||
'call_count'])
|
||||
|
||||
case_sensitive = False
|
||||
"""case_sensitive handles a backwards incompatible bug. The URL used to
|
||||
match against our matches and that is saved in request_history is always
|
||||
lowercased. This is incorrect as it reports incorrect history to the user
|
||||
and doesn't allow case sensitive path matching.
|
||||
|
||||
Unfortunately fixing this change is backwards incompatible in the 1.X
|
||||
series as people may rely on this behaviour. To work around this you can
|
||||
globally set:
|
||||
|
||||
requests_mock.mock.case_sensitive = True
|
||||
|
||||
which will prevent the lowercase being executed and return case sensitive
|
||||
url and query information.
|
||||
|
||||
This will become the default in a 2.X release. See bug: #1584008.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._adapter = adapter.Adapter()
|
||||
case_sensitive = kwargs.pop('case_sensitive', self.case_sensitive)
|
||||
self._adapter = adapter.Adapter(case_sensitive=case_sensitive)
|
||||
|
||||
self._real_http = kwargs.pop('real_http', False)
|
||||
self._real_send = None
|
||||
|
||||
|
@ -676,3 +676,41 @@ class SessionAdapterTests(base.TestCase):
|
||||
|
||||
self.assertIsInstance(data, six.binary_type)
|
||||
self.assertEqual(0, len(data))
|
||||
|
||||
def test_case_sensitive_headers(self):
|
||||
data = 'testdata'
|
||||
headers = {'aBcDe': 'FgHiJ'}
|
||||
|
||||
self.adapter.register_uri('GET', self.url, text=data)
|
||||
resp = self.session.get(self.url, headers=headers)
|
||||
|
||||
self.assertEqual('GET', self.adapter.last_request.method)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual(data, resp.text)
|
||||
|
||||
for k, v in headers.items():
|
||||
self.assertEqual(v, self.adapter.last_request.headers[k])
|
||||
|
||||
def test_case_sensitive_history(self):
|
||||
self.adapter._case_sensitive = True
|
||||
|
||||
data = 'testdata'
|
||||
netloc = 'examPlE.CoM'
|
||||
path = '/TesTER'
|
||||
query = 'aBC=deF'
|
||||
|
||||
mock_url = '%s://%s%s' % (self.PREFIX, netloc.lower(), path)
|
||||
request_url = '%s://%s%s?%s' % (self.PREFIX, netloc, path, query)
|
||||
|
||||
# test that the netloc is ignored when actually making the request
|
||||
self.adapter.register_uri('GET', mock_url, text=data)
|
||||
resp = self.session.get(request_url)
|
||||
|
||||
self.assertEqual('GET', self.adapter.last_request.method)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual(data, resp.text)
|
||||
|
||||
# but even still the mixed case parameters come out in history
|
||||
self.assertEqual(netloc, self.adapter.last_request.netloc)
|
||||
self.assertEqual(path, self.adapter.last_request.path)
|
||||
self.assertEqual(query, self.adapter.last_request.query)
|
||||
|
@ -28,13 +28,15 @@ class TestMatcher(base.TestCase):
|
||||
complete_qs=False,
|
||||
headers=None,
|
||||
request_headers={},
|
||||
real_http=False):
|
||||
real_http=False,
|
||||
case_sensitive=False):
|
||||
matcher = adapter._Matcher(matcher_method,
|
||||
target,
|
||||
[],
|
||||
complete_qs,
|
||||
request_headers,
|
||||
real_http=real_http)
|
||||
real_http=real_http,
|
||||
case_sensitive=case_sensitive)
|
||||
request = adapter._RequestObjectProxy._create(request_method,
|
||||
url,
|
||||
headers)
|
||||
@ -222,3 +224,44 @@ class TestMatcher(base.TestCase):
|
||||
'http://www.test.com/path',
|
||||
headers={'A': 'abc', 'b': 'def'},
|
||||
request_headers={'c': 'ghi'})
|
||||
|
||||
# headers should be key insensitive and value sensitive, we have no
|
||||
# choice here because they go into an insensitive dict.
|
||||
self.assertMatch('/path',
|
||||
'http://www.test.com/path',
|
||||
headers={'aBc': 'abc', 'DEF': 'def'},
|
||||
request_headers={'abC': 'abc'})
|
||||
|
||||
self.assertNoMatch('/path',
|
||||
'http://www.test.com/path',
|
||||
headers={'abc': 'aBC', 'DEF': 'def'},
|
||||
request_headers={'abc': 'Abc'})
|
||||
|
||||
def test_case_sensitive_ignored_for_netloc_and_protocol(self):
|
||||
for case_sensitive in (True, False):
|
||||
self.assertMatch('http://AbC.CoM',
|
||||
'http://aBc.CoM',
|
||||
case_sensitive=case_sensitive)
|
||||
|
||||
self.assertMatch('htTP://abc.com',
|
||||
'hTTp://abc.com',
|
||||
case_sensitive=case_sensitive)
|
||||
|
||||
self.assertMatch('htTP://aBC.cOm',
|
||||
'hTTp://AbC.Com',
|
||||
case_sensitive=case_sensitive)
|
||||
|
||||
def assertSensitiveMatch(self, target, url, **kwargs):
|
||||
self.assertMatch(target, url, case_sensitive=False, **kwargs)
|
||||
self.assertNoMatch(target, url, case_sensitive=True, **kwargs)
|
||||
|
||||
def test_case_sensitive_paths(self):
|
||||
self.assertSensitiveMatch('http://abc.com/pAtH', 'http://abc.com/path')
|
||||
self.assertSensitiveMatch('/pAtH', 'http://abc.com/path')
|
||||
|
||||
def test_case_sensitive_query(self):
|
||||
self.assertSensitiveMatch('http://abc.com/path?abCD=efGH',
|
||||
'http://abc.com/path?abCd=eFGH')
|
||||
|
||||
self.assertSensitiveMatch('http://abc.com/path?abcd=efGH',
|
||||
'http://abc.com/path?abcd=eFGH')
|
||||
|
@ -279,3 +279,34 @@ class MockerHttpMethodsTests(base.TestCase):
|
||||
|
||||
# do it again to make sure the mock is still in place
|
||||
self.assertEqual(data, requests.get(uri1).text)
|
||||
|
||||
@requests_mock.Mocker(case_sensitive=True)
|
||||
def test_case_sensitive_query(self, m):
|
||||
data = 'testdata'
|
||||
query = {'aBcDe': 'FgHiJ'}
|
||||
|
||||
m.get(self.URL, text=data)
|
||||
resp = requests.get(self.URL, params=query)
|
||||
|
||||
self.assertEqual('GET', m.last_request.method)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual(data, resp.text)
|
||||
|
||||
for k, v in query.items():
|
||||
self.assertEqual([v], m.last_request.qs[k])
|
||||
|
||||
@mock.patch.object(requests_mock.Mocker, 'case_sensitive', True)
|
||||
def test_global_case_sensitive(self):
|
||||
with requests_mock.mock() as m:
|
||||
data = 'testdata'
|
||||
query = {'aBcDe': 'FgHiJ'}
|
||||
|
||||
m.get(self.URL, text=data)
|
||||
resp = requests.get(self.URL, params=query)
|
||||
|
||||
self.assertEqual('GET', m.last_request.method)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual(data, resp.text)
|
||||
|
||||
for k, v in query.items():
|
||||
self.assertEqual([v], m.last_request.qs[k])
|
||||
|
Loading…
x
Reference in New Issue
Block a user