mostr-zulip-bot/zulip_bots/zulip_bots/request_test_lib.py
Robert Hönig 60c3919deb bot tests: Allow raw responses in fixtures.
Previously, the responses set in bot test fixtures
where handled as JSON objects. This works fine for
most bot tests, because most of the APIs that bots
are calling return a JSON-formatted response object.
However, some, like Trello, do return raw data.
This hasn't been noticed so far, because the respective
Trello test needed internet access. Tests shouldn't
need internet access.
This commit makes that Trello test use a fixture. To
work properly, it also adds a way to make http_mock_config
parse the response object as raw data.
This can now be done by modifying the "is_raw_response"
property in a newly introduced "meta" object that can
be used to specify how a fixture should be handled.
2018-06-04 10:13:38 -07:00

104 lines
3.7 KiB
Python

import json
import requests
from contextlib import contextmanager
from unittest.mock import patch
from typing import Any, Dict, List
@contextmanager
def mock_http_conversation(http_data):
# type: (Dict[str, Any]) -> Any
"""
Use this context manager to mock and verify a bot's HTTP
requests to the third-party API (and provide the correct
third-party API response. This allows us to test things
that would require the Internet without it).
http_data should be fixtures data formatted like the data
in zulip_bots/zulip_bots/bots/giphy/fixtures/test_normal.json
"""
def get_response(http_response, http_headers, is_raw_response):
# type: (Dict[str, Any], Dict[str, Any], bool) -> Any
"""Creates a fake `requests` Response with a desired HTTP response and
response headers.
"""
mock_result = requests.Response()
if is_raw_response:
mock_result._content = http_response.encode() # type: ignore # This modifies a "hidden" attribute.
else:
mock_result._content = json.dumps(http_response).encode() # type: ignore # See above.
mock_result.status_code = http_headers.get('status', 200)
return mock_result
def assert_called_with_fields(mock_result, http_request, fields, meta):
# type: (Any, Dict[str, Any], List[str], Dict[str, Any]) -> None
"""Calls `assert_called_with` on a mock object using an HTTP request.
Uses `fields` to determine which keys to look for in HTTP request and
to test; if a key is in `fields`, e.g., 'headers', it will be used in
the assertion.
"""
args = {}
for field in fields:
if field in http_request:
args[field] = http_request[field]
mock_result.assert_called_with(http_request['api_url'], **args)
try:
http_request = http_data['request']
http_response = http_data['response']
http_headers = http_data['response-headers']
except KeyError:
print("ERROR: Failed to find 'request', 'response' or 'response-headers' fields in fixture")
raise
meta = http_data.get('meta', dict())
is_raw_response = meta.get('is_raw_response', False)
http_method = http_request.get('method', 'GET')
if http_method == 'GET':
with patch('requests.get') as mock_get:
mock_get.return_value = get_response(http_response, http_headers, is_raw_response)
yield
assert_called_with_fields(
mock_get,
http_request,
['params', 'headers'],
meta
)
elif http_method == 'PATCH':
with patch('requests.patch') as mock_patch:
mock_patch.return_value = get_response(http_response, http_headers, is_raw_response)
yield
assert_called_with_fields(
mock_patch,
http_request,
['params', 'headers', 'json', 'data'],
meta
)
else:
with patch('requests.post') as mock_post:
mock_post.return_value = get_response(http_response, http_headers, is_raw_response)
yield
assert_called_with_fields(
mock_post,
http_request,
['params', 'headers', 'json', 'data'],
meta
)
@contextmanager
def mock_request_exception():
# type: () -> Any
def assert_mock_called(mock_result):
# type: (Any) -> None
assert mock_result.called
with patch('requests.get') as mock_get:
mock_get.return_value = True
mock_get.side_effect = requests.exceptions.RequestException
yield
assert_mock_called(mock_get)