2016-10-24 06:48:01 +00:00
|
|
|
"""Aiohttp test utils."""
|
|
|
|
import asyncio
|
|
|
|
from contextlib import contextmanager
|
|
|
|
import functools
|
|
|
|
import json as _json
|
|
|
|
from unittest import mock
|
2016-11-03 02:34:12 +00:00
|
|
|
from urllib.parse import urlparse, parse_qs
|
2016-12-01 06:20:21 +00:00
|
|
|
import yarl
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
class AiohttpClientMocker:
|
|
|
|
"""Mock Aiohttp client requests."""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
"""Initialize the request mocker."""
|
|
|
|
self._mocks = []
|
2017-01-09 16:08:37 +00:00
|
|
|
self._cookies = {}
|
2016-10-24 06:48:01 +00:00
|
|
|
self.mock_calls = []
|
|
|
|
|
|
|
|
def request(self, method, url, *,
|
2016-10-28 04:45:35 +00:00
|
|
|
auth=None,
|
2016-10-24 06:48:01 +00:00
|
|
|
status=200,
|
|
|
|
text=None,
|
2016-12-13 16:55:13 +00:00
|
|
|
data=None,
|
2016-10-24 06:48:01 +00:00
|
|
|
content=None,
|
2016-12-01 06:20:21 +00:00
|
|
|
json=None,
|
2016-12-02 02:30:41 +00:00
|
|
|
params=None,
|
2017-01-30 00:15:40 +00:00
|
|
|
headers={},
|
2017-01-09 16:08:37 +00:00
|
|
|
exc=None,
|
|
|
|
cookies=None):
|
2016-10-24 06:48:01 +00:00
|
|
|
"""Mock a request."""
|
|
|
|
if json:
|
|
|
|
text = _json.dumps(json)
|
|
|
|
if text:
|
|
|
|
content = text.encode('utf-8')
|
|
|
|
if content is None:
|
|
|
|
content = b''
|
2016-12-01 06:20:21 +00:00
|
|
|
if params:
|
|
|
|
url = str(yarl.URL(url).with_query(params))
|
2016-12-02 02:30:41 +00:00
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
self._mocks.append(AiohttpClientMockResponse(
|
2017-01-30 00:15:40 +00:00
|
|
|
method, url, status, content, cookies, exc, headers))
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
def get(self, *args, **kwargs):
|
|
|
|
"""Register a mock get request."""
|
|
|
|
self.request('get', *args, **kwargs)
|
|
|
|
|
|
|
|
def put(self, *args, **kwargs):
|
|
|
|
"""Register a mock put request."""
|
|
|
|
self.request('put', *args, **kwargs)
|
|
|
|
|
|
|
|
def post(self, *args, **kwargs):
|
|
|
|
"""Register a mock post request."""
|
|
|
|
self.request('post', *args, **kwargs)
|
|
|
|
|
|
|
|
def delete(self, *args, **kwargs):
|
|
|
|
"""Register a mock delete request."""
|
|
|
|
self.request('delete', *args, **kwargs)
|
|
|
|
|
|
|
|
def options(self, *args, **kwargs):
|
|
|
|
"""Register a mock options request."""
|
|
|
|
self.request('options', *args, **kwargs)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def call_count(self):
|
|
|
|
"""Number of requests made."""
|
|
|
|
return len(self.mock_calls)
|
|
|
|
|
2017-01-09 16:08:37 +00:00
|
|
|
def clear_requests(self):
|
|
|
|
"""Reset mock calls."""
|
|
|
|
self._mocks.clear()
|
|
|
|
self._cookies.clear()
|
|
|
|
self.mock_calls.clear()
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@asyncio.coroutine
|
2017-02-19 22:25:45 +00:00
|
|
|
# pylint: disable=unused-variable
|
2016-12-13 16:55:13 +00:00
|
|
|
def match_request(self, method, url, *, data=None, auth=None, params=None,
|
2017-02-19 22:25:45 +00:00
|
|
|
headers=None, allow_redirects=None):
|
2016-10-24 06:48:01 +00:00
|
|
|
"""Match a request against pre-registered requests."""
|
|
|
|
for response in self._mocks:
|
2016-12-01 06:20:21 +00:00
|
|
|
if response.match_request(method, url, params):
|
2016-12-13 16:55:13 +00:00
|
|
|
self.mock_calls.append((method, url, data))
|
2016-12-02 02:30:41 +00:00
|
|
|
|
2017-01-09 16:08:37 +00:00
|
|
|
if response.exc:
|
|
|
|
raise response.exc
|
2016-10-24 06:48:01 +00:00
|
|
|
return response
|
|
|
|
|
2016-12-13 07:23:08 +00:00
|
|
|
assert False, "No mock registered for {} {} {}".format(method.upper(),
|
|
|
|
url, params)
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
class AiohttpClientMockResponse:
|
|
|
|
"""Mock Aiohttp client response."""
|
|
|
|
|
2017-01-30 00:15:40 +00:00
|
|
|
def __init__(self, method, url, status, response, cookies=None, exc=None,
|
|
|
|
headers={}):
|
2016-10-24 06:48:01 +00:00
|
|
|
"""Initialize a fake response."""
|
|
|
|
self.method = method
|
2016-11-03 02:34:12 +00:00
|
|
|
self._url = url
|
|
|
|
self._url_parts = (None if hasattr(url, 'search')
|
|
|
|
else urlparse(url.lower()))
|
2016-10-24 06:48:01 +00:00
|
|
|
self.status = status
|
|
|
|
self.response = response
|
2017-01-09 16:08:37 +00:00
|
|
|
self.exc = exc
|
2016-10-24 06:48:01 +00:00
|
|
|
|
2017-01-30 00:15:40 +00:00
|
|
|
self._headers = headers
|
2017-01-16 10:06:23 +00:00
|
|
|
self._cookies = {}
|
|
|
|
|
|
|
|
if cookies:
|
|
|
|
for name, data in cookies.items():
|
|
|
|
cookie = mock.MagicMock()
|
|
|
|
cookie.value = data
|
|
|
|
self._cookies[name] = cookie
|
|
|
|
|
2017-01-30 00:15:40 +00:00
|
|
|
if isinstance(response, list):
|
|
|
|
self.content = mock.MagicMock()
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def read(*argc, **kwargs):
|
|
|
|
"""Read content stream mock."""
|
|
|
|
if self.response:
|
|
|
|
return self.response.pop()
|
|
|
|
return None
|
|
|
|
|
|
|
|
self.content.read = read
|
|
|
|
|
2016-12-01 06:20:21 +00:00
|
|
|
def match_request(self, method, url, params=None):
|
2016-10-24 06:48:01 +00:00
|
|
|
"""Test if response answers request."""
|
2016-11-03 02:34:12 +00:00
|
|
|
if method.lower() != self.method.lower():
|
|
|
|
return False
|
|
|
|
|
2016-12-01 06:20:21 +00:00
|
|
|
if params:
|
|
|
|
url = str(yarl.URL(url).with_query(params))
|
|
|
|
|
2016-11-03 02:34:12 +00:00
|
|
|
# regular expression matching
|
|
|
|
if self._url_parts is None:
|
|
|
|
return self._url.search(url) is not None
|
|
|
|
|
|
|
|
req = urlparse(url.lower())
|
|
|
|
|
|
|
|
if self._url_parts.scheme and req.scheme != self._url_parts.scheme:
|
|
|
|
return False
|
|
|
|
if self._url_parts.netloc and req.netloc != self._url_parts.netloc:
|
|
|
|
return False
|
|
|
|
if (req.path or '/') != (self._url_parts.path or '/'):
|
|
|
|
return False
|
|
|
|
|
|
|
|
# Ensure all query components in matcher are present in the request
|
|
|
|
request_qs = parse_qs(req.query)
|
|
|
|
matcher_qs = parse_qs(self._url_parts.query)
|
|
|
|
for key, vals in matcher_qs.items():
|
|
|
|
for val in vals:
|
|
|
|
try:
|
|
|
|
request_qs.get(key, []).remove(val)
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
2016-10-24 06:48:01 +00:00
|
|
|
|
2017-01-30 00:15:40 +00:00
|
|
|
@property
|
|
|
|
def headers(self):
|
|
|
|
"""Return content_type."""
|
|
|
|
return self._headers
|
|
|
|
|
2017-01-16 10:06:23 +00:00
|
|
|
@property
|
|
|
|
def cookies(self):
|
|
|
|
"""Return dict of cookies."""
|
|
|
|
return self._cookies
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def read(self):
|
|
|
|
"""Return mock response."""
|
|
|
|
return self.response
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def text(self, encoding='utf-8'):
|
|
|
|
"""Return mock response as a string."""
|
|
|
|
return self.response.decode(encoding)
|
|
|
|
|
2017-01-14 07:18:03 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def json(self, encoding='utf-8'):
|
|
|
|
"""Return mock response as a json."""
|
|
|
|
return _json.loads(self.response.decode(encoding))
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def release(self):
|
|
|
|
"""Mock release."""
|
|
|
|
pass
|
|
|
|
|
2017-01-16 10:06:23 +00:00
|
|
|
def close(self):
|
|
|
|
"""Mock close."""
|
|
|
|
pass
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
def mock_aiohttp_client():
|
|
|
|
"""Context manager to mock aiohttp client."""
|
|
|
|
mocker = AiohttpClientMocker()
|
|
|
|
|
|
|
|
with mock.patch('aiohttp.ClientSession') as mock_session:
|
|
|
|
instance = mock_session()
|
|
|
|
|
|
|
|
for method in ('get', 'post', 'put', 'options', 'delete'):
|
|
|
|
setattr(instance, method,
|
|
|
|
functools.partial(mocker.match_request, method))
|
|
|
|
|
|
|
|
yield mocker
|