Add DEBUG_API_REQUESTS Anymail setting to dump API communications.

Optionally dump API requests and responses to stdout, to simplify
debugging of the raw API communications. Currently implemented only
for Requests-based backends.

This (undocumented) setting can log things like API keys, so is not
appropriate for use in production.
This commit is contained in:
medmunds
2018-10-09 15:07:22 -07:00
parent 0794617750
commit ddafac9fbd
5 changed files with 132 additions and 4 deletions

View File

@@ -1,7 +1,8 @@
from django.test import override_settings
from django.test import override_settings, SimpleTestCase
from anymail.backends.base_requests import AnymailRequestsBackend, RequestsPayload
from anymail.message import AnymailMessage, AnymailRecipientStatus
from tests.utils import AnymailTestMixin
from .mock_requests_backend import RequestsBackendMockAPITestCase
@@ -10,12 +11,14 @@ class MinimalRequestsBackend(AnymailRequestsBackend):
"""(useful only for these tests)"""
esp_name = "Example"
api_url = "https://httpbin.org/post" # helpful echoback endpoint for live testing
def __init__(self, **kwargs):
super(MinimalRequestsBackend, self).__init__("https://esp.example.com/api/", **kwargs)
super(MinimalRequestsBackend, self).__init__(self.api_url, **kwargs)
def build_message_payload(self, message, defaults):
return MinimalRequestsPayload(message, defaults, self)
_payload_init = getattr(message, "_payload_init", {})
return MinimalRequestsPayload(message, defaults, self, **_payload_init)
def parse_recipient_status(self, response, payload, message):
return {'to@example.com': AnymailRecipientStatus('message-id', 'sent')}
@@ -49,7 +52,7 @@ class RequestsBackendBaseTestCase(RequestsBackendMockAPITestCase):
def test_minimal_requests_backend(self):
"""Make sure the testing backend defined above actually works"""
self.message.send()
self.assert_esp_called("https://esp.example.com/api/")
self.assert_esp_called("https://httpbin.org/post")
def test_timeout_default(self):
"""All requests have a 30 second default timeout"""
@@ -63,3 +66,44 @@ class RequestsBackendBaseTestCase(RequestsBackendMockAPITestCase):
self.message.send()
timeout = self.get_api_call_arg('timeout')
self.assertEqual(timeout, 5)
@override_settings(EMAIL_BACKEND='tests.test_base_backends.MinimalRequestsBackend')
class RequestsBackendLiveTestCase(SimpleTestCase, AnymailTestMixin):
@override_settings(ANYMAIL_DEBUG_API_REQUESTS=True)
def test_debug_logging(self):
message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
message._payload_init = dict(
data="Request body",
headers={
"Content-Type": "text/plain",
"Accept": "application/json",
},
)
with self.assertPrints("===== Anymail API request") as outbuf:
message.send()
# Header order and response data vary to much to do a full comparison, but make sure
# that the output contains some expected pieces of the request and the response"
output = outbuf.getvalue()
self.assertIn("\nPOST https://httpbin.org/post\n", output)
self.assertIn("\nUser-Agent: django-anymail/", output)
self.assertIn("\nAccept: application/json\n", output)
self.assertIn("\nContent-Type: text/plain\n", output) # request
self.assertIn("\n\nRequest body\n", output)
self.assertIn("\n----- Response\n", output)
self.assertIn("\nHTTP 200 OK\n", output)
self.assertIn("\nContent-Type: application/json\n", output) # response
def test_no_debug_logging(self):
# Make sure it doesn't output anything when DEBUG_API_REQUESTS is not set
message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
message._payload_init = dict(
data="Request body",
headers={
"Content-Type": "text/plain",
"Accept": "application/json",
},
)
with self.assertPrints("", match="equal"):
message.send()

View File

@@ -12,6 +12,7 @@ from distutils.util import strtobool
import six
from django.test import Client
from six.moves import StringIO
def envbool(var, default=False):
@@ -173,6 +174,41 @@ class AnymailTestMixin:
raise self.failureException(
msg or "%r is not a valid UUID" % uuid_str)
@contextmanager
def assertPrints(self, expected, match="contain", msg=None):
"""Use as a context manager; checks that code writes `expected` to stdout.
`match` can be "contain", "equal", "start", "end", or the name of any str
method that takes one str argument and returns a boolean, or None to simply
capture stdout without checking it. Default is "contain".
Returns StringIO buffer; the output text is available as cm.getvalue().
>>> with self.assertPrints("foo") as cm:
... print("foo")
>>> self.assertNotIn("bar", cm.getvalue())
"""
matchfn = {
"contain": "__contains__",
"equal": "__eq__",
"start": "startswith",
"end": "endswith",
}.get(match, match)
old_stdout = sys.stdout
buffer = StringIO()
try:
sys.stdout = buffer
yield buffer
if matchfn:
actual = buffer.getvalue()
bound_matchfn = getattr(actual, matchfn)
if not bound_matchfn(expected):
raise self.failureException(
msg or "Stdout {actual!r} does not {match} {expected!r}".format(
actual=actual, match=match, expected=expected))
finally:
sys.stdout = old_stdout
# Backported from Python 3.4
class _AssertLogsContext(object):