Merge "Enable case sensitive matching"
This commit is contained in:
commit
c105f1cae2
@ -11,6 +11,7 @@ Contents:
|
|||||||
mocker
|
mocker
|
||||||
matching
|
matching
|
||||||
response
|
response
|
||||||
|
knownissues
|
||||||
history
|
history
|
||||||
adapter
|
adapter
|
||||||
contrib
|
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 = requests.Session()
|
||||||
>>> session.mount('mock', adapter)
|
>>> 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
|
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._cert = kwargs.pop('cert', None)
|
||||||
self._proxies = copy.deepcopy(kwargs.pop('proxies', {}))
|
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):
|
def __getattr__(self, name):
|
||||||
return getattr(self._request, name)
|
return getattr(self._request, name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _url_parts(self):
|
def _url_parts(self):
|
||||||
if self._url_parts_ is None:
|
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_
|
return self._url_parts_
|
||||||
|
|
||||||
@ -169,7 +178,7 @@ class _Matcher(_RequestHistoryTracker):
|
|||||||
"""Contains all the information about a provided URL to match."""
|
"""Contains all the information about a provided URL to match."""
|
||||||
|
|
||||||
def __init__(self, method, url, responses, complete_qs, request_headers,
|
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
|
:param bool complete_qs: Match the entire query string. By default URLs
|
||||||
match if all the provided matcher query arguments are matched and
|
match if all the provided matcher query arguments are matched and
|
||||||
@ -180,15 +189,29 @@ class _Matcher(_RequestHistoryTracker):
|
|||||||
|
|
||||||
self._method = method
|
self._method = method
|
||||||
self._url = url
|
self._url = url
|
||||||
try:
|
|
||||||
self._url_parts = urlparse.urlparse(url.lower())
|
|
||||||
except:
|
|
||||||
self._url_parts = None
|
|
||||||
self._responses = responses
|
self._responses = responses
|
||||||
self._complete_qs = complete_qs
|
self._complete_qs = complete_qs
|
||||||
self._request_headers = request_headers
|
self._request_headers = request_headers
|
||||||
self._real_http = real_http
|
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):
|
def _match_method(self, request):
|
||||||
if self._method is ANY:
|
if self._method is ANY:
|
||||||
return True
|
return True
|
||||||
@ -206,18 +229,20 @@ class _Matcher(_RequestHistoryTracker):
|
|||||||
if hasattr(self._url, 'search'):
|
if hasattr(self._url, 'search'):
|
||||||
return self._url.search(request.url) is not None
|
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
|
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
|
return False
|
||||||
|
|
||||||
if (request.path or '/') != (self._url_parts.path or '/'):
|
if (request.path or '/') != self._path:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# construct our own qs structure as we remove items from it below
|
# construct our own qs structure as we remove items from it below
|
||||||
request_qs = urlparse.parse_qs(request.query)
|
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 k, vals in six.iteritems(matcher_qs):
|
||||||
for v in vals:
|
for v in vals:
|
||||||
@ -283,12 +308,15 @@ class Adapter(BaseAdapter, _RequestHistoryTracker):
|
|||||||
"""A fake adapter than can return predefined responses.
|
"""A fake adapter than can return predefined responses.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self, case_sensitive=False):
|
||||||
super(Adapter, self).__init__()
|
super(Adapter, self).__init__()
|
||||||
|
self._case_sensitive = case_sensitive
|
||||||
self._matchers = []
|
self._matchers = []
|
||||||
|
|
||||||
def send(self, request, **kwargs):
|
def send(self, request, **kwargs):
|
||||||
request = _RequestObjectProxy(request, **kwargs)
|
request = _RequestObjectProxy(request,
|
||||||
|
case_sensitive=self._case_sensitive,
|
||||||
|
**kwargs)
|
||||||
self._add_to_history(request)
|
self._add_to_history(request)
|
||||||
|
|
||||||
for matcher in reversed(self._matchers):
|
for matcher in reversed(self._matchers):
|
||||||
@ -327,10 +355,17 @@ class Adapter(BaseAdapter, _RequestHistoryTracker):
|
|||||||
elif not response_list:
|
elif not response_list:
|
||||||
response_list = [] if real_http else [kwargs]
|
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]
|
responses = [response._MatcherResponse(**k) for k in response_list]
|
||||||
matcher = _Matcher(method,
|
matcher = _Matcher(method,
|
||||||
url,
|
url,
|
||||||
responses,
|
responses,
|
||||||
|
case_sensitive=self._case_sensitive,
|
||||||
complete_qs=complete_qs,
|
complete_qs=complete_qs,
|
||||||
request_headers=request_headers,
|
request_headers=request_headers,
|
||||||
real_http=real_http)
|
real_http=real_http)
|
||||||
|
@ -39,8 +39,28 @@ class MockerCore(object):
|
|||||||
'called',
|
'called',
|
||||||
'call_count'])
|
'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):
|
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_http = kwargs.pop('real_http', False)
|
||||||
self._real_send = None
|
self._real_send = None
|
||||||
|
|
||||||
|
@ -683,3 +683,41 @@ class SessionAdapterTests(base.TestCase):
|
|||||||
|
|
||||||
self.assertIsInstance(data, six.binary_type)
|
self.assertIsInstance(data, six.binary_type)
|
||||||
self.assertEqual(0, len(data))
|
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,
|
complete_qs=False,
|
||||||
headers=None,
|
headers=None,
|
||||||
request_headers={},
|
request_headers={},
|
||||||
real_http=False):
|
real_http=False,
|
||||||
|
case_sensitive=False):
|
||||||
matcher = adapter._Matcher(matcher_method,
|
matcher = adapter._Matcher(matcher_method,
|
||||||
target,
|
target,
|
||||||
[],
|
[],
|
||||||
complete_qs,
|
complete_qs,
|
||||||
request_headers,
|
request_headers,
|
||||||
real_http=real_http)
|
real_http=real_http,
|
||||||
|
case_sensitive=case_sensitive)
|
||||||
request = adapter._RequestObjectProxy._create(request_method,
|
request = adapter._RequestObjectProxy._create(request_method,
|
||||||
url,
|
url,
|
||||||
headers)
|
headers)
|
||||||
@ -222,3 +224,44 @@ class TestMatcher(base.TestCase):
|
|||||||
'http://www.test.com/path',
|
'http://www.test.com/path',
|
||||||
headers={'A': 'abc', 'b': 'def'},
|
headers={'A': 'abc', 'b': 'def'},
|
||||||
request_headers={'c': 'ghi'})
|
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')
|
||||||
|
@ -288,3 +288,34 @@ class MockerHttpMethodsTests(base.TestCase):
|
|||||||
|
|
||||||
# do it again to make sure the mock is still in place
|
# do it again to make sure the mock is still in place
|
||||||
self.assertEqual(data, requests.get(uri1).text)
|
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