Drop Python 2 and Django 1.11 support

Minimum supported versions are now Django 2.0, Python 3.5.

This touches a lot of code, to:
* Remove obsolete portability code and workarounds
  (six, backports of email parsers, test utils, etc.)
* Use Python 3 syntax (class defs, raise ... from, etc.)
* Correct inheritance for mixin classes
* Fix outdated docs content and links
* Suppress Python 3 "unclosed SSLSocket" ResourceWarnings
  that are beyond our control (in integration tests due to boto3, 
  python-sparkpost)
This commit is contained in:
Mike Edmunds
2020-08-01 14:53:10 -07:00
committed by GitHub
parent c803108481
commit 85cec5e9dc
87 changed files with 672 additions and 1278 deletions

View File

@@ -1,9 +1,9 @@
import json
from io import BytesIO
from django.core import mail
from django.test import SimpleTestCase
import requests
import six
from mock import patch
from anymail.exceptions import AnymailAPIError
@@ -13,7 +13,7 @@ from .utils import AnymailTestMixin
UNSET = object()
class RequestsBackendMockAPITestCase(SimpleTestCase, AnymailTestMixin):
class RequestsBackendMockAPITestCase(AnymailTestMixin, SimpleTestCase):
"""TestCase that mocks API calls through requests"""
DEFAULT_RAW_RESPONSE = b"""{"subclass": "should override"}"""
@@ -22,15 +22,14 @@ class RequestsBackendMockAPITestCase(SimpleTestCase, AnymailTestMixin):
class MockResponse(requests.Response):
"""requests.request return value mock sufficient for testing"""
def __init__(self, status_code=200, raw=b"RESPONSE", encoding='utf-8', reason=None):
super(RequestsBackendMockAPITestCase.MockResponse, self).__init__()
super().__init__()
self.status_code = status_code
self.encoding = encoding
self.reason = reason or ("OK" if 200 <= status_code < 300 else "ERROR")
# six.BytesIO(None) returns b'None' in PY2 (rather than b'')
self.raw = six.BytesIO(raw) if raw is not None else six.BytesIO()
self.raw = BytesIO(raw)
def setUp(self):
super(RequestsBackendMockAPITestCase, self).setUp()
super().setUp()
self.patch_request = patch('requests.Session.request', autospec=True)
self.mock_request = self.patch_request.start()
self.addCleanup(self.patch_request.stop)
@@ -127,17 +126,25 @@ class RequestsBackendMockAPITestCase(SimpleTestCase, AnymailTestMixin):
raise AssertionError(msg or "ESP API was called and shouldn't have been")
# noinspection PyUnresolvedReferences
class SessionSharingTestCasesMixin(object):
"""Mixin that tests connection sharing in any RequestsBackendMockAPITestCase
class SessionSharingTestCases(RequestsBackendMockAPITestCase):
"""Common test cases for requests backend connection sharing.
(Contains actual test cases, so can't be included in RequestsBackendMockAPITestCase
itself, as that would re-run these tests several times for each backend, in
each TestCase for the backend.)
Instantiate for each ESP by:
- subclassing
- adding or overriding any tests as appropriate
"""
def __init__(self, methodName='runTest'):
if self.__class__ is SessionSharingTestCases:
# don't run these tests on the abstract base implementation
methodName = 'runNoTestsInBaseClass'
super().__init__(methodName)
def runNoTestsInBaseClass(self):
pass
def setUp(self):
super(SessionSharingTestCasesMixin, self).setUp()
super().setUp()
self.patch_close = patch('requests.Session.close', autospec=True)
self.mock_close = self.patch_close.start()
self.addCleanup(self.patch_close.stop)

View File

@@ -1,11 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import json
from datetime import datetime
from email.mime.application import MIMEApplication
import six
from django.core import mail
from django.core.mail import BadHeaderError
from django.test import SimpleTestCase, override_settings, tag
@@ -19,11 +15,11 @@ from .utils import AnymailTestMixin, SAMPLE_IMAGE_FILENAME, sample_image_content
@tag('amazon_ses')
@override_settings(EMAIL_BACKEND='anymail.backends.amazon_ses.EmailBackend')
class AmazonSESBackendMockAPITestCase(SimpleTestCase, AnymailTestMixin):
class AmazonSESBackendMockAPITestCase(AnymailTestMixin, SimpleTestCase):
"""TestCase that uses the Amazon SES EmailBackend with a mocked boto3 client"""
def setUp(self):
super(AmazonSESBackendMockAPITestCase, self).setUp()
super().setUp()
# Mock boto3.session.Session().client('ses').send_raw_email (and any other client operations)
# (We could also use botocore.stub.Stubber, but mock works well with our test structure)
@@ -122,7 +118,7 @@ class AmazonSESBackendStandardEmailTests(AmazonSESBackendMockAPITestCase):
# send_raw_email takes a fully-formatted MIME message.
# This is a simple (if inexact) way to check for expected headers and body:
raw_mime = params['RawMessage']['Data']
self.assertIsInstance(raw_mime, six.binary_type) # SendRawEmail expects Data as bytes
self.assertIsInstance(raw_mime, bytes) # SendRawEmail expects Data as bytes
self.assertIn(b"\nFrom: from@example.com\n", raw_mime)
self.assertIn(b"\nTo: to@example.com\n", raw_mime)
self.assertIn(b"\nSubject: Subject here\n", raw_mime)
@@ -259,7 +255,7 @@ class AmazonSESBackendStandardEmailTests(AmazonSESBackendMockAPITestCase):
self.assertEqual(params['Source'], "from1@example.com")
def test_commas_in_subject(self):
"""Anymail works around a Python 2 email header bug that adds unwanted spaces after commas in long subjects"""
"""There used to be a Python email header bug that added unwanted spaces after commas in long subjects"""
self.message.subject = "100,000,000 isn't a number you'd really want to break up in this email subject, right?"
self.message.send()
sent_message = self.get_sent_message()

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
import json
from base64 import b64encode
from datetime import datetime
@@ -22,7 +20,7 @@ from .webhook_cases import WebhookTestCase
class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
def setUp(self):
super(AmazonSESInboundTests, self).setUp()
super().setUp()
# Mock boto3.session.Session().client('s3').download_fileobj
# (We could also use botocore.stub.Stubber, but mock works well with our test structure)
self.patch_boto3_session = patch('anymail.webhooks.amazon_ses.boto3.session.Session', autospec=True)
@@ -263,9 +261,8 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
self.assertEqual([str(to) for to in message.to],
['Recipient <inbound@example.com>', 'someone-else@example.org'])
self.assertEqual(message.subject, 'Test inbound message')
# rstrip below because the Python 3 EmailBytesParser converts \r\n to \n, but the Python 2 version doesn't
self.assertEqual(message.text.rstrip(), "It's a body\N{HORIZONTAL ELLIPSIS}")
self.assertEqual(message.html.rstrip(), """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>""")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertIsNone(message.spam_detected)
def test_inbound_s3_failure_message(self):

View File

@@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import unittest
import warnings
@@ -12,11 +9,6 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin, sample_image_path
try:
ResourceWarning
except NameError:
ResourceWarning = Warning # Python 2
AMAZON_SES_TEST_ACCESS_KEY_ID = os.getenv("AMAZON_SES_TEST_ACCESS_KEY_ID")
AMAZON_SES_TEST_SECRET_ACCESS_KEY = os.getenv("AMAZON_SES_TEST_SECRET_ACCESS_KEY")
@@ -42,7 +34,7 @@ AMAZON_SES_TEST_REGION_NAME = os.getenv("AMAZON_SES_TEST_REGION_NAME", "us-east-
"AMAZON_SES_CONFIGURATION_SET_NAME": "TestConfigurationSet", # actual config set in Anymail test account
})
@tag('amazon_ses', 'live')
class AmazonSESBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
class AmazonSESBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Amazon SES API integration tests
These tests run against the **live** Amazon SES API, using the environment
@@ -63,14 +55,14 @@ class AmazonSESBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
"""
def setUp(self):
super(AmazonSESBackendIntegrationTests, self).setUp()
super().setUp()
self.message = AnymailMessage('Anymail Amazon SES integration test', 'Text content',
'test@test-ses.anymail.info', ['success@simulator.amazonses.com'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
# boto3 relies on GC to close connections. Python 3 warns about unclosed ssl.SSLSocket during cleanup.
# We don't care. (It might not be a real problem worth warning, but in any case it's not our problem.)
# https://www.google.com/search?q=unittest+boto3+ResourceWarning+unclosed+ssl.SSLSocket
# We don't care. (It may be a false positive, or it may be a botocore problem, but it's not *our* problem.)
# https://github.com/boto/boto3/issues/454#issuecomment-586033745
# Filter in TestCase.setUp because unittest resets the warning filters for each test.
# https://stackoverflow.com/a/26620811/647002
warnings.filterwarnings("ignore", message=r"unclosed <ssl\.SSLSocket", category=ResourceWarning)
@@ -154,8 +146,9 @@ class AmazonSESBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
}
})
def test_invalid_aws_credentials(self):
with self.assertRaises(AnymailAPIError) as cm:
self.message.send()
err = cm.exception
# Make sure the exception message includes AWS's response:
self.assertIn("The security token included in the request is invalid", str(err))
with self.assertRaisesMessage(
AnymailAPIError,
"The security token included in the request is invalid"
):
self.message.send()

View File

@@ -2,7 +2,7 @@ import json
import warnings
from datetime import datetime
from django.test import override_settings, tag
from django.test import SimpleTestCase, override_settings, tag
from django.utils.timezone import utc
from mock import ANY, patch
@@ -10,12 +10,11 @@ from anymail.exceptions import AnymailConfigurationError, AnymailInsecureWebhook
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.amazon_ses import AmazonSESTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestsMixin, WebhookTestCase
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
class AmazonSESWebhookTestsMixin(object):
class AmazonSESWebhookTestsMixin(SimpleTestCase):
def post_from_sns(self, path, raw_sns_message, **kwargs):
# noinspection PyUnresolvedReferences
return self.client.post(
path,
content_type='text/plain; charset=UTF-8', # SNS posts JSON as text/plain
@@ -27,12 +26,12 @@ class AmazonSESWebhookTestsMixin(object):
@tag('amazon_ses')
class AmazonSESWebhookSecurityTests(WebhookTestCase, AmazonSESWebhookTestsMixin, WebhookBasicAuthTestsMixin):
class AmazonSESWebhookSecurityTests(AmazonSESWebhookTestsMixin, WebhookBasicAuthTestCase):
def call_webhook(self):
return self.post_from_sns('/anymail/amazon_ses/tracking/',
{"Type": "Notification", "MessageId": "123", "Message": "{}"})
# Most actual tests are in WebhookBasicAuthTestsMixin
# Most actual tests are in WebhookBasicAuthTestCase
def test_verifies_missing_auth(self):
# Must handle missing auth header slightly differently from Anymail default 400 SuspiciousOperation:
@@ -412,7 +411,7 @@ class AmazonSESSubscriptionManagementTests(WebhookTestCase, AmazonSESWebhookTest
# (Note that WebhookTestCase sets up ANYMAIL WEBHOOK_SECRET.)
def setUp(self):
super(AmazonSESSubscriptionManagementTests, self).setUp()
super().setUp()
# Mock boto3.session.Session().client('sns').confirm_subscription (and any other client operations)
# (We could also use botocore.stub.Stubber, but mock works well with our test structure)
self.patch_boto3_session = patch('anymail.webhooks.amazon_ses.boto3.session.Session', autospec=True)

View File

@@ -14,7 +14,7 @@ class MinimalRequestsBackend(AnymailRequestsBackend):
api_url = "https://httpbin.org/post" # helpful echoback endpoint for live testing
def __init__(self, **kwargs):
super(MinimalRequestsBackend, self).__init__(self.api_url, **kwargs)
super().__init__(self.api_url, **kwargs)
def build_message_payload(self, message, defaults):
_payload_init = getattr(message, "_payload_init", {})
@@ -46,7 +46,7 @@ class RequestsBackendBaseTestCase(RequestsBackendMockAPITestCase):
"""Test common functionality in AnymailRequestsBackend"""
def setUp(self):
super(RequestsBackendBaseTestCase, self).setUp()
super().setUp()
self.message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
def test_minimal_requests_backend(self):
@@ -70,7 +70,7 @@ class RequestsBackendBaseTestCase(RequestsBackendMockAPITestCase):
@tag('live')
@override_settings(EMAIL_BACKEND='tests.test_base_backends.MinimalRequestsBackend')
class RequestsBackendLiveTestCase(SimpleTestCase, AnymailTestMixin):
class RequestsBackendLiveTestCase(AnymailTestMixin, SimpleTestCase):
@override_settings(ANYMAIL_DEBUG_API_REQUESTS=True)
def test_debug_logging(self):
message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])

View File

@@ -7,7 +7,7 @@ from anymail.checks import check_deprecated_settings, check_insecure_settings
from .utils import AnymailTestMixin
class DeprecatedSettingsTests(SimpleTestCase, AnymailTestMixin):
class DeprecatedSettingsTests(AnymailTestMixin, SimpleTestCase):
@override_settings(ANYMAIL={"WEBHOOK_AUTHORIZATION": "abcde:12345"})
def test_webhook_authorization(self):
errors = check_deprecated_settings(None)
@@ -27,7 +27,7 @@ class DeprecatedSettingsTests(SimpleTestCase, AnymailTestMixin):
)])
class InsecureSettingsTests(SimpleTestCase, AnymailTestMixin):
class InsecureSettingsTests(AnymailTestMixin, SimpleTestCase):
@override_settings(ANYMAIL={"DEBUG_API_REQUESTS": True})
def test_debug_api_requests_deployed(self):
errors = check_insecure_settings(None)

View File

@@ -1,7 +1,6 @@
from datetime import datetime
from email.mime.text import MIMEText
import six
from django.core import mail
from django.core.exceptions import ImproperlyConfigured
from django.core.mail import get_connection, send_mail
@@ -9,7 +8,7 @@ from django.test import SimpleTestCase
from django.test.utils import override_settings
from django.utils.functional import Promise
from django.utils.timezone import utc
from django.utils.translation import ugettext_lazy
from django.utils.translation import gettext_lazy
from anymail.backends.test import EmailBackend as TestBackend, TestPayload
from anymail.exceptions import AnymailConfigurationError, AnymailInvalidAddress, AnymailUnsupportedFeature
@@ -29,15 +28,15 @@ class SettingsTestBackend(TestBackend):
default=None, allow_bare=True)
self.password = get_anymail_setting('password', esp_name=esp_name, kwargs=kwargs,
default=None, allow_bare=True)
super(SettingsTestBackend, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
@override_settings(EMAIL_BACKEND='anymail.backends.test.EmailBackend')
class TestBackendTestCase(SimpleTestCase, AnymailTestMixin):
class TestBackendTestCase(AnymailTestMixin, SimpleTestCase):
"""Base TestCase using Anymail's Test EmailBackend"""
def setUp(self):
super(TestBackendTestCase, self).setUp()
super().setUp()
# Simple message useful for many tests
self.message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
@@ -237,7 +236,7 @@ class SendDefaultsTests(TestBackendTestCase):
class LazyStringsTest(TestBackendTestCase):
"""
Tests ugettext_lazy strings forced real before passing to ESP transport.
Tests gettext_lazy strings forced real before passing to ESP transport.
Docs notwithstanding, Django lazy strings *don't* work anywhere regular
strings would. In particular, they aren't instances of unicode/str.
@@ -251,38 +250,38 @@ class LazyStringsTest(TestBackendTestCase):
def assertNotLazy(self, s, msg=None):
self.assertNotIsInstance(s, Promise,
msg=msg or "String %r is lazy" % six.text_type(s))
msg=msg or "String %r is lazy" % str(s))
def test_lazy_from(self):
# This sometimes ends up lazy when settings.DEFAULT_FROM_EMAIL is meant to be localized
self.message.from_email = ugettext_lazy(u'"Global Sales" <sales@example.com>')
self.message.from_email = gettext_lazy('"Global Sales" <sales@example.com>')
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['from'].address)
def test_lazy_subject(self):
self.message.subject = ugettext_lazy("subject")
self.message.subject = gettext_lazy("subject")
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['subject'])
def test_lazy_body(self):
self.message.body = ugettext_lazy("text body")
self.message.attach_alternative(ugettext_lazy("html body"), "text/html")
self.message.body = gettext_lazy("text body")
self.message.attach_alternative(gettext_lazy("html body"), "text/html")
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['text_body'])
self.assertNotLazy(params['html_body'])
def test_lazy_headers(self):
self.message.extra_headers['X-Test'] = ugettext_lazy("Test Header")
self.message.extra_headers['X-Test'] = gettext_lazy("Test Header")
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['extra_headers']['X-Test'])
def test_lazy_attachments(self):
self.message.attach(ugettext_lazy("test.csv"), ugettext_lazy("test,csv,data"), "text/csv")
self.message.attach(MIMEText(ugettext_lazy("contact info")))
self.message.attach(gettext_lazy("test.csv"), gettext_lazy("test,csv,data"), "text/csv")
self.message.attach(MIMEText(gettext_lazy("contact info")))
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['attachments'][0].name)
@@ -290,22 +289,22 @@ class LazyStringsTest(TestBackendTestCase):
self.assertNotLazy(params['attachments'][1].content)
def test_lazy_tags(self):
self.message.tags = [ugettext_lazy("Shipping"), ugettext_lazy("Sales")]
self.message.tags = [gettext_lazy("Shipping"), gettext_lazy("Sales")]
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['tags'][0])
self.assertNotLazy(params['tags'][1])
def test_lazy_metadata(self):
self.message.metadata = {'order_type': ugettext_lazy("Subscription")}
self.message.metadata = {'order_type': gettext_lazy("Subscription")}
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['metadata']['order_type'])
def test_lazy_merge_data(self):
self.message.merge_data = {
'to@example.com': {'duration': ugettext_lazy("One Month")}}
self.message.merge_global_data = {'order_type': ugettext_lazy("Subscription")}
'to@example.com': {'duration': gettext_lazy("One Month")}}
self.message.merge_global_data = {'order_type': gettext_lazy("Subscription")}
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['merge_data']['to@example.com']['duration'])
@@ -329,7 +328,7 @@ class CatchCommonErrorsTests(TestBackendTestCase):
def test_explains_reply_to_must_be_list_lazy(self):
"""Same as previous tests, with lazy strings"""
# Lazy strings can fool string/iterable detection
self.message.reply_to = ugettext_lazy("single-reply-to@example.com")
self.message.reply_to = gettext_lazy("single-reply-to@example.com")
with self.assertRaisesMessage(TypeError, '"reply_to" attribute must be a list or other iterable'):
self.message.send()
@@ -431,7 +430,7 @@ class BatchSendDetectionTestCase(TestBackendTestCase):
"""Tests shared code to consistently determine whether to use batch send"""
def setUp(self):
super(BatchSendDetectionTestCase, self).setUp()
super().setUp()
self.backend = TestBackend()
def test_default_is_not_batch(self):
@@ -460,7 +459,7 @@ class BatchSendDetectionTestCase(TestBackendTestCase):
def set_cc(self, emails):
if self.is_batch(): # this won't work here!
self.unsupported_feature("cc with batch send")
super(ImproperlyImplementedPayload, self).set_cc(emails)
super().set_cc(emails)
connection = mail.get_connection('anymail.backends.test.EmailBackend',
payload_class=ImproperlyImplementedPayload)

View File

@@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import quopri
from base64 import b64encode
from email.utils import collapse_rfc2231_value
@@ -169,10 +166,9 @@ class AnymailInboundMessageConstructionTests(SimpleTestCase):
def test_parse_raw_mime_8bit_utf8(self):
# In come cases, the message below ends up with 'Content-Transfer-Encoding: 8bit',
# so needs to be parsed as bytes, not text (see https://bugs.python.org/issue18271).
# Message.as_string() returns str, which is is bytes on Python 2 and text on Python 3.
# Message.as_string() returns str (text), not bytes.
# (This might be a Django bug; plain old MIMEText avoids the problem by using
# 'Content-Transfer-Encoding: base64', which parses fine as text or bytes.
# Django <1.11 on Python 3 also used base64.)
# 'Content-Transfer-Encoding: base64', which parses fine as text or bytes.)
# Either way, AnymailInboundMessage should try to sidestep the whole issue.
raw = SafeMIMEText("Unicode ✓", "plain", "utf-8").as_string()
msg = AnymailInboundMessage.parse_raw_mime(raw)
@@ -495,9 +491,11 @@ class AnymailInboundMessageAttachedMessageTests(SimpleTestCase):
self.assertEqual(orig_msg.get_content_type(), "multipart/related")
class EmailParserWorkaroundTests(SimpleTestCase):
# Anymail includes workarounds for (some of) the more problematic bugs
# in the Python 2 email.parser.Parser.
class EmailParserBehaviorTests(SimpleTestCase):
# Python 3.5+'s EmailParser should handle all of these, so long as it's not
# invoked with its default policy=compat32. This double checks we're using it
# properly. (Also, older versions of Anymail included workarounds for these
# in older, broken versions of the EmailParser.)
def test_parse_folded_headers(self):
raw = dedent("""\
@@ -540,16 +538,11 @@ class EmailParserWorkaroundTests(SimpleTestCase):
self.assertEqual(msg.from_email.display_name, "Keith Moore")
self.assertEqual(msg.from_email.addr_spec, "moore@example.com")
# When an RFC2047 encoded-word abuts an RFC5322 quoted-word in a *structured* header,
# Python 3's parser nicely recombines them into a single quoted word. That's way too
# complicated for our Python 2 workaround ...
self.assertIn(msg["To"], [ # `To` header will decode to one of these:
'Keld Jørn Simonsen <keld@example.com>, "André Pirard, Jr." <PIRARD@example.com>', # Python 3
'Keld Jørn Simonsen <keld@example.com>, André "Pirard, Jr." <PIRARD@example.com>', # workaround version
])
# ... but the two forms are equivalent, and de-structure the same:
self.assertEqual(msg["To"],
'Keld Jørn Simonsen <keld@example.com>, '
'"André Pirard, Jr." <PIRARD@example.com>')
self.assertEqual(msg.to[0].display_name, "Keld Jørn Simonsen")
self.assertEqual(msg.to[1].display_name, "André Pirard, Jr.") # correct in Python 3 *and* workaround!
self.assertEqual(msg.to[1].display_name, "André Pirard, Jr.")
# Note: Like email.headerregistry.Address, Anymail decodes an RFC2047-encoded display_name,
# but does not decode a punycode domain. (Use `idna.decode(domain)` if you need that.)

View File

@@ -1,16 +1,7 @@
# -*- coding: utf-8 -*-
from datetime import date, datetime
from textwrap import dedent
try:
from email import message_from_bytes
except ImportError:
from email import message_from_string
def message_from_bytes(s):
return message_from_string(s.decode('utf-8'))
from email import message_from_bytes
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
@@ -24,7 +15,7 @@ from anymail.exceptions import (
AnymailRequestsAPIError, AnymailUnsupportedFeature)
from anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCases
from .utils import (AnymailTestMixin, sample_email_content,
sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME)
@@ -39,7 +30,7 @@ class MailgunBackendMockAPITestCase(RequestsBackendMockAPITestCase):
}"""
def setUp(self):
super(MailgunBackendMockAPITestCase, self).setUp()
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
@@ -178,7 +169,7 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase):
self.assertEqual(len(inlines), 0)
def test_unicode_attachment_correctly_decoded(self):
self.message.attach(u"Une pièce jointe.html", u'<p>\u2019</p>', mimetype='text/html')
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.send()
# Verify the RFC 7578 compliance workaround has kicked in:
@@ -191,7 +182,7 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase):
workaround = True
data = data.decode("utf-8").replace("\r\n", "\n")
self.assertNotIn("filename*=", data) # No RFC 2231 encoding
self.assertIn(u'Content-Disposition: form-data; name="attachment"; filename="Une pièce jointe.html"', data)
self.assertIn('Content-Disposition: form-data; name="attachment"; filename="Une pièce jointe.html"', data)
if workaround:
files = self.get_api_call_files(required=False)
@@ -199,8 +190,8 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase):
def test_rfc_7578_compliance(self):
# Check some corner cases in the workaround that undoes RFC 2231 multipart/form-data encoding...
self.message.subject = u"Testing for filename*=utf-8''problems"
self.message.body = u"The attached message should have an attachment named 'vedhæftet fil.txt'"
self.message.subject = "Testing for filename*=utf-8''problems"
self.message.body = "The attached message should have an attachment named 'vedhæftet fil.txt'"
# A forwarded message with its own attachment:
forwarded_message = dedent("""\
MIME-Version: 1.0
@@ -219,7 +210,7 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase):
This is an attachment.
--boundary--
""")
self.message.attach(u"besked med vedhæftede filer", forwarded_message, "message/rfc822")
self.message.attach("besked med vedhæftede filer", forwarded_message, "message/rfc822")
self.message.send()
data = self.get_api_call_data()
@@ -230,13 +221,13 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase):
# Top-level attachment (in form-data) should have RFC 7578 filename (raw Unicode):
self.assertIn(
u'Content-Disposition: form-data; name="attachment"; filename="besked med vedhæftede filer"', data)
'Content-Disposition: form-data; name="attachment"; filename="besked med vedhæftede filer"', data)
# Embedded message/rfc822 attachment should retain its RFC 2231 encoded filename:
self.assertIn("Content-Type: text/plain; name*=utf-8''vedh%C3%A6ftet%20fil.txt", data)
self.assertIn("Content-Disposition: attachment; filename*=utf-8''vedh%C3%A6ftet%20fil.txt", data)
# References to RFC 2231 in message text should remain intact:
self.assertIn("Testing for filename*=utf-8''problems", data)
self.assertIn(u"The attached message should have an attachment named 'vedhæftet fil.txt'", data)
self.assertIn("The attached message should have an attachment named 'vedhæftet fil.txt'", data)
def test_attachment_missing_filename(self):
"""Mailgun silently drops attachments without filenames, so warn the caller"""
@@ -767,14 +758,14 @@ class MailgunBackendRecipientsRefusedTests(MailgunBackendMockAPITestCase):
@tag('mailgun')
class MailgunBackendSessionSharingTestCase(SessionSharingTestCasesMixin, MailgunBackendMockAPITestCase):
class MailgunBackendSessionSharingTestCase(SessionSharingTestCases, MailgunBackendMockAPITestCase):
"""Requests session sharing tests"""
pass # tests are defined in the mixin
pass # tests are defined in SessionSharingTestCases
@tag('mailgun')
@override_settings(EMAIL_BACKEND="anymail.backends.mailgun.EmailBackend")
class MailgunBackendImproperlyConfiguredTests(SimpleTestCase, AnymailTestMixin):
class MailgunBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""
def test_missing_api_key(self):

View File

@@ -1,8 +1,8 @@
import json
from datetime import datetime
from io import BytesIO
from textwrap import dedent
import six
from django.test import override_settings, tag
from django.utils.timezone import utc
from mock import ANY
@@ -97,13 +97,13 @@ class MailgunInboundTestCase(WebhookTestCase):
])
def test_attachments(self):
att1 = six.BytesIO('test attachment'.encode('utf-8'))
att1 = BytesIO('test attachment'.encode('utf-8'))
att1.name = 'test.txt'
image_content = sample_image_content()
att2 = six.BytesIO(image_content)
att2 = BytesIO(image_content)
att2.name = 'image.png'
email_content = sample_email_content()
att3 = six.BytesIO(email_content)
att3 = BytesIO(email_content)
att3.content_type = 'message/rfc822; charset="us-ascii"'
raw_event = mailgun_sign_legacy_payload({
'message-headers': '[]',
@@ -124,7 +124,7 @@ class MailgunInboundTestCase(WebhookTestCase):
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), u'test attachment')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)
@@ -176,8 +176,8 @@ class MailgunInboundTestCase(WebhookTestCase):
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertEqual(message.subject, 'Raw MIME test')
self.assertEqual(message.text, u"It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, u"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
def test_misconfigured_tracking(self):
raw_event = mailgun_sign_payload({

View File

@@ -1,21 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import logging
import os
import unittest
from datetime import datetime, timedelta
from time import mktime, sleep
from time import sleep
import requests
from django.test import SimpleTestCase, override_settings, tag
from anymail.exceptions import AnymailAPIError
from anymail.message import AnymailMessage
from .utils import AnymailTestMixin, sample_image_path
MAILGUN_TEST_API_KEY = os.getenv('MAILGUN_TEST_API_KEY')
MAILGUN_TEST_DOMAIN = os.getenv('MAILGUN_TEST_DOMAIN')
@@ -28,7 +24,7 @@ MAILGUN_TEST_DOMAIN = os.getenv('MAILGUN_TEST_DOMAIN')
'MAILGUN_SENDER_DOMAIN': MAILGUN_TEST_DOMAIN,
'MAILGUN_SEND_DEFAULTS': {'esp_extra': {'o:testmode': 'yes'}}},
EMAIL_BACKEND="anymail.backends.mailgun.EmailBackend")
class MailgunBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Mailgun API integration tests
These tests run against the **live** Mailgun API, using the
@@ -39,7 +35,7 @@ class MailgunBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
"""
def setUp(self):
super(MailgunBackendIntegrationTests, self).setUp()
super().setUp()
self.message = AnymailMessage('Anymail Mailgun integration test', 'Text content',
'from@example.com', ['test+to1@anymail.info'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
@@ -101,7 +97,7 @@ class MailgunBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
def test_all_options(self):
send_at = datetime.now().replace(microsecond=0) + timedelta(minutes=2)
send_at_timestamp = mktime(send_at.timetuple()) # python3: send_at.timestamp()
send_at_timestamp = send_at.timestamp()
message = AnymailMessage(
subject="Anymail Mailgun all-options integration test",
body="This is the text body",

View File

@@ -12,7 +12,7 @@ from anymail.exceptions import AnymailConfigurationError
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.mailgun import MailgunTrackingWebhookView
from .webhook_cases import WebhookTestCase, WebhookBasicAuthTestsMixin
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
TEST_WEBHOOK_SIGNING_KEY = 'TEST_WEBHOOK_SIGNING_KEY'
@@ -99,14 +99,14 @@ class MailgunWebhookSettingsTestCase(WebhookTestCase):
@tag('mailgun')
@override_settings(ANYMAIL_MAILGUN_WEBHOOK_SIGNING_KEY=TEST_WEBHOOK_SIGNING_KEY)
class MailgunWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
class MailgunWebhookSecurityTestCase(WebhookBasicAuthTestCase):
should_warn_if_no_auth = False # because we check webhook signature
def call_webhook(self):
return self.client.post('/anymail/mailgun/tracking/', content_type="application/json",
data=json.dumps(mailgun_sign_payload({'event-data': {'event': 'delivered'}})))
# Additional tests are in WebhookBasicAuthTestsMixin
# Additional tests are in WebhookBasicAuthTestCase
def test_verifies_correct_signature(self):
response = self.client.post('/anymail/mailgun/tracking/', content_type="application/json",

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from base64 import b64encode
from decimal import Decimal
from email.mime.base import MIMEBase
@@ -14,7 +12,7 @@ from anymail.exceptions import (AnymailAPIError, AnymailSerializationError,
AnymailRequestsAPIError)
from anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCases
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin, decode_att
@@ -49,7 +47,7 @@ class MailjetBackendMockAPITestCase(RequestsBackendMockAPITestCase):
}"""
def setUp(self):
super(MailjetBackendMockAPITestCase, self).setUp()
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
@@ -222,13 +220,13 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
self.assertNotIn('ContentID', attachments[2])
def test_unicode_attachment_correctly_decoded(self):
self.message.attach(u"Une pièce jointe.html", u'<p>\u2019</p>', mimetype='text/html')
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Attachments'], [{
'Filename': u'Une pièce jointe.html',
'Filename': 'Une pièce jointe.html',
'Content-type': 'text/html',
'content': b64encode(u'<p>\u2019</p>'.encode('utf-8')).decode('ascii')
'content': b64encode('<p>\u2019</p>'.encode('utf-8')).decode('ascii')
}])
def test_embedded_images(self):
@@ -656,14 +654,14 @@ class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
@tag('mailjet')
class MailjetBackendSessionSharingTestCase(SessionSharingTestCasesMixin, MailjetBackendMockAPITestCase):
class MailjetBackendSessionSharingTestCase(SessionSharingTestCases, MailjetBackendMockAPITestCase):
"""Requests session sharing tests"""
pass # tests are defined in the mixin
pass # tests are defined in SessionSharingTestCases
@tag('mailjet')
@override_settings(EMAIL_BACKEND="anymail.backends.mailjet.EmailBackend")
class MailjetBackendImproperlyConfiguredTests(SimpleTestCase, AnymailTestMixin):
class MailjetBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""
def test_missing_api_key(self):

View File

@@ -160,7 +160,7 @@ class MailjetInboundTestCase(WebhookTestCase):
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), u'test attachment')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)

View File

@@ -19,7 +19,7 @@ MAILJET_TEST_SECRET_KEY = os.getenv('MAILJET_TEST_SECRET_KEY')
@override_settings(ANYMAIL_MAILJET_API_KEY=MAILJET_TEST_API_KEY,
ANYMAIL_MAILJET_SECRET_KEY=MAILJET_TEST_SECRET_KEY,
EMAIL_BACKEND="anymail.backends.mailjet.EmailBackend")
class MailjetBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
class MailjetBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Mailjet API integration tests
These tests run against the **live** Mailjet API, using the
@@ -36,7 +36,7 @@ class MailjetBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
"""
def setUp(self):
super(MailjetBackendIntegrationTests, self).setUp()
super().setUp()
self.message = AnymailMessage('Anymail Mailjet integration test', 'Text content',
'test@test-mj.anymail.info', ['test+to1@anymail.info'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")

View File

@@ -7,16 +7,16 @@ from mock import ANY
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.mailjet import MailjetTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestsMixin, WebhookTestCase
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('mailjet')
class MailjetWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
class MailjetWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps([]))
# Actual tests are in WebhookBasicAuthTestsMixin
# Actual tests are in WebhookBasicAuthTestCase
@tag('mailjet')

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from datetime import date, datetime
from decimal import Decimal
from email.mime.base import MIMEBase
@@ -14,7 +12,7 @@ from anymail.exceptions import (AnymailAPIError, AnymailRecipientsRefused,
AnymailSerializationError, AnymailUnsupportedFeature)
from anymail.message import attach_inline_image
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCases
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin, decode_att
@@ -30,7 +28,7 @@ class MandrillBackendMockAPITestCase(RequestsBackendMockAPITestCase):
}]"""
def setUp(self):
super(MandrillBackendMockAPITestCase, self).setUp()
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
@@ -170,7 +168,7 @@ class MandrillBackendStandardEmailTests(MandrillBackendMockAPITestCase):
self.assertFalse('images' in data['message'])
def test_unicode_attachment_correctly_decoded(self):
self.message.attach(u"Une pièce jointe.html", u'<p>\u2019</p>', mimetype='text/html')
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.send()
data = self.get_api_call_json()
attachments = data['message']['attachments']
@@ -610,14 +608,14 @@ class MandrillBackendRecipientsRefusedTests(MandrillBackendMockAPITestCase):
@tag('mandrill')
class MandrillBackendSessionSharingTestCase(SessionSharingTestCasesMixin, MandrillBackendMockAPITestCase):
class MandrillBackendSessionSharingTestCase(SessionSharingTestCases, MandrillBackendMockAPITestCase):
"""Requests session sharing tests"""
pass # tests are defined in the mixin
pass # tests are defined in SessionSharingTestCases
@tag('mandrill')
@override_settings(EMAIL_BACKEND="anymail.backends.mandrill.EmailBackend")
class MandrillBackendImproperlyConfiguredTests(SimpleTestCase, AnymailTestMixin):
class MandrillBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test backend without required settings"""
def test_missing_api_key(self):

View File

@@ -75,8 +75,8 @@ class MandrillInboundTestCase(WebhookTestCase):
self.assertEqual(message.to[1].addr_spec, 'other@example.com')
self.assertEqual(message.subject, 'Test subject')
self.assertEqual(message.date.isoformat(" "), "2017-10-12 18:03:30-07:00")
self.assertEqual(message.text, u"It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, u"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertIsNone(message.envelope_sender) # Mandrill doesn't provide sender
self.assertEqual(message.envelope_recipient, 'delivered-to@example.com')

View File

@@ -17,7 +17,7 @@ MANDRILL_TEST_API_KEY = os.getenv('MANDRILL_TEST_API_KEY')
"Set MANDRILL_TEST_API_KEY environment variable to run integration tests")
@override_settings(MANDRILL_API_KEY=MANDRILL_TEST_API_KEY,
EMAIL_BACKEND="anymail.backends.mandrill.EmailBackend")
class MandrillBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
class MandrillBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Mandrill API integration tests
These tests run against the **live** Mandrill API, using the
@@ -30,7 +30,7 @@ class MandrillBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
"""
def setUp(self):
super(MandrillBackendIntegrationTests, self).setUp()
super().setUp()
self.message = mail.EmailMultiAlternatives('Anymail Mandrill integration test', 'Text content',
'from@example.com', ['test+to1@anymail.info'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")

View File

@@ -1,10 +1,10 @@
import json
from datetime import datetime
from six.moves.urllib.parse import urljoin
import hashlib
import hmac
import json
from base64 import b64encode
from datetime import datetime
from urllib.parse import urljoin
from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings, tag
from django.utils.timezone import utc
@@ -12,8 +12,7 @@ from mock import ANY
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.mandrill import MandrillCombinedWebhookView, MandrillTrackingWebhookView
from .webhook_cases import WebhookTestCase, WebhookBasicAuthTestsMixin
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
TEST_WEBHOOK_KEY = 'TEST_WEBHOOK_KEY'
@@ -65,14 +64,14 @@ class MandrillWebhookSettingsTestCase(WebhookTestCase):
@tag('mandrill')
@override_settings(ANYMAIL_MANDRILL_WEBHOOK_KEY=TEST_WEBHOOK_KEY)
class MandrillWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
class MandrillWebhookSecurityTestCase(WebhookBasicAuthTestCase):
should_warn_if_no_auth = False # because we check webhook signature
def call_webhook(self):
kwargs = mandrill_args([{'event': 'send'}])
return self.client.post(**kwargs)
# Additional tests are in WebhookBasicAuthTestsMixin
# Additional tests are in WebhookBasicAuthTestCase
def test_verifies_correct_signature(self):
kwargs = mandrill_args([{'event': 'send'}])
@@ -112,7 +111,7 @@ class MandrillWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixi
response = self.client.post(SERVER_NAME="127.0.0.1", **kwargs)
self.assertEqual(response.status_code, 200)
# override WebhookBasicAuthTestsMixin version of this test
# override WebhookBasicAuthTestCase version of this test
@override_settings(ANYMAIL={'WEBHOOK_SECRET': ['cred1:pass1', 'cred2:pass2']})
def test_supports_credential_rotation(self):
"""You can supply a list of basic auth credentials, and any is allowed"""

View File

@@ -10,7 +10,7 @@ from .utils import AnymailTestMixin, sample_image_content
class InlineImageTests(AnymailTestMixin, SimpleTestCase):
def setUp(self):
self.message = EmailMultiAlternatives()
super(InlineImageTests, self).setUp()
super().setUp()
@patch("email.utils.socket.getfqdn")
def test_default_domain(self, mock_getfqdn):

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import json
from base64 import b64encode
from decimal import Decimal
@@ -14,7 +13,7 @@ from anymail.exceptions import (
AnymailUnsupportedFeature, AnymailRecipientsRefused, AnymailInvalidAddress)
from anymail.message import attach_inline_image_file, AnymailMessage
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCases
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin, decode_att
@@ -31,7 +30,7 @@ class PostmarkBackendMockAPITestCase(RequestsBackendMockAPITestCase):
}"""
def setUp(self):
super(PostmarkBackendMockAPITestCase, self).setUp()
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
@@ -182,13 +181,13 @@ class PostmarkBackendStandardEmailTests(PostmarkBackendMockAPITestCase):
self.assertNotIn('ContentID', attachments[2])
def test_unicode_attachment_correctly_decoded(self):
self.message.attach(u"Une pièce jointe.html", u'<p>\u2019</p>', mimetype='text/html')
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Attachments'], [{
'Name': u'Une pièce jointe.html',
'Name': 'Une pièce jointe.html',
'ContentType': 'text/html',
'Content': b64encode(u'<p>\u2019</p>'.encode('utf-8')).decode('ascii')
'Content': b64encode('<p>\u2019</p>'.encode('utf-8')).decode('ascii')
}])
def test_embedded_images(self):
@@ -758,14 +757,14 @@ class PostmarkBackendRecipientsRefusedTests(PostmarkBackendMockAPITestCase):
@tag('postmark')
class PostmarkBackendSessionSharingTestCase(SessionSharingTestCasesMixin, PostmarkBackendMockAPITestCase):
class PostmarkBackendSessionSharingTestCase(SessionSharingTestCases, PostmarkBackendMockAPITestCase):
"""Requests session sharing tests"""
pass # tests are defined in the mixin
pass # tests are defined in SessionSharingTestCases
@tag('postmark')
@override_settings(EMAIL_BACKEND="anymail.backends.postmark.EmailBackend")
class PostmarkBackendImproperlyConfiguredTests(SimpleTestCase, AnymailTestMixin):
class PostmarkBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""
def test_missing_api_key(self):

View File

@@ -163,7 +163,7 @@ class PostmarkInboundTestCase(WebhookTestCase):
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), u'test attachment')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)

View File

@@ -18,7 +18,7 @@ POSTMARK_TEST_TEMPLATE_ID = os.getenv('POSTMARK_TEST_TEMPLATE_ID')
@tag('postmark', 'live')
@override_settings(ANYMAIL_POSTMARK_SERVER_TOKEN="POSTMARK_API_TEST",
EMAIL_BACKEND="anymail.backends.postmark.EmailBackend")
class PostmarkBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
class PostmarkBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Postmark API integration tests
These tests run against the **live** Postmark API, but using a
@@ -26,7 +26,7 @@ class PostmarkBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
"""
def setUp(self):
super(PostmarkBackendIntegrationTests, self).setUp()
super().setUp()
self.message = AnymailMessage('Anymail Postmark integration test', 'Text content',
'from@example.com', ['test+to1@anymail.info'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")

View File

@@ -8,16 +8,16 @@ from mock import ANY
from anymail.exceptions import AnymailConfigurationError
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.postmark import PostmarkTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestsMixin, WebhookTestCase
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('postmark')
class PostmarkWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
class PostmarkWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps({}))
# Actual tests are in WebhookBasicAuthTestsMixin
# Actual tests are in WebhookBasicAuthTestCase
@tag('postmark')

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from base64 import b64encode, b64decode
from calendar import timegm
from datetime import date, datetime
@@ -7,7 +5,6 @@ from decimal import Decimal
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
import six
from django.core import mail
from django.test import SimpleTestCase, override_settings, tag
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone
@@ -17,12 +14,9 @@ from anymail.exceptions import (AnymailAPIError, AnymailConfigurationError, Anym
AnymailUnsupportedFeature, AnymailWarning)
from anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCases
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin
# noinspection PyUnresolvedReferences
longtype = int if six.PY3 else long # NOQA: F821
@tag('sendgrid')
@override_settings(EMAIL_BACKEND='anymail.backends.sendgrid.EmailBackend',
@@ -32,7 +26,7 @@ class SendGridBackendMockAPITestCase(RequestsBackendMockAPITestCase):
DEFAULT_STATUS_CODE = 202 # SendGrid v3 uses '202 Accepted' for success (in most cases)
def setUp(self):
super(SendGridBackendMockAPITestCase, self).setUp()
super().setUp()
# Patch uuid4 to generate predictable anymail_ids for testing
patch_uuid4 = patch('anymail.backends.sendgrid.uuid.uuid4',
@@ -153,13 +147,12 @@ class SendGridBackendStandardEmailTests(SendGridBackendMockAPITestCase):
self.assertEqual(data['content'][0], {'type': "text/html", 'value': html_content})
def test_extra_headers(self):
self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123, 'X-Long': longtype(123),
self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123,
'Reply-To': '"Do Not Reply" <noreply@example.com>'}
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['headers']['X-Custom'], 'string')
self.assertEqual(data['headers']['X-Num'], '123') # converted to string (undoc'd SendGrid requirement)
self.assertEqual(data['headers']['X-Long'], '123') # converted to string (undoc'd SendGrid requirement)
# Reply-To must be moved to separate param
self.assertNotIn('Reply-To', data['headers'])
self.assertEqual(data['reply_to'], {'name': "Do Not Reply", 'email': "noreply@example.com"})
@@ -222,11 +215,11 @@ class SendGridBackendStandardEmailTests(SendGridBackendMockAPITestCase):
'type': "application/pdf"})
def test_unicode_attachment_correctly_decoded(self):
self.message.attach(u"Une pièce jointe.html", u'<p>\u2019</p>', mimetype='text/html')
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.send()
attachment = self.get_api_call_json()['attachments'][0]
self.assertEqual(attachment['filename'], u'Une pièce jointe.html')
self.assertEqual(b64decode(attachment['content']).decode('utf-8'), u'<p>\u2019</p>')
self.assertEqual(attachment['filename'], 'Une pièce jointe.html')
self.assertEqual(b64decode(attachment['content']).decode('utf-8'), '<p>\u2019</p>')
def test_embedded_images(self):
image_filename = SAMPLE_IMAGE_FILENAME
@@ -348,14 +341,14 @@ class SendGridBackendAnymailFeatureTests(SendGridBackendMockAPITestCase):
self.message.send()
def test_metadata(self):
self.message.metadata = {'user_id': "12345", 'items': 6, 'float': 98.6, 'long': longtype(123)}
self.message.metadata = {'user_id': "12345", 'items': 6, 'float': 98.6}
self.message.send()
data = self.get_api_call_json()
data['custom_args'].pop('anymail_id', None) # remove anymail_id we added for tracking
self.assertEqual(data['custom_args'], {'user_id': "12345",
'items': "6", # int converted to a string,
'float': "98.6", # float converted to a string (watch binary rounding!)
'long': "123"}) # long converted to string
})
def test_send_at(self):
utc_plus_6 = get_fixed_timezone(6 * 60)
@@ -879,14 +872,14 @@ class SendGridBackendRecipientsRefusedTests(SendGridBackendMockAPITestCase):
@tag('sendgrid')
class SendGridBackendSessionSharingTestCase(SessionSharingTestCasesMixin, SendGridBackendMockAPITestCase):
class SendGridBackendSessionSharingTestCase(SessionSharingTestCases, SendGridBackendMockAPITestCase):
"""Requests session sharing tests"""
pass # tests are defined in the mixin
pass # tests are defined in SessionSharingTestCases
@tag('sendgrid')
@override_settings(EMAIL_BACKEND="anymail.backends.sendgrid.EmailBackend")
class SendGridBackendImproperlyConfiguredTests(SimpleTestCase, AnymailTestMixin):
class SendGridBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""
def test_missing_auth(self):
@@ -896,7 +889,7 @@ class SendGridBackendImproperlyConfiguredTests(SimpleTestCase, AnymailTestMixin)
@tag('sendgrid')
@override_settings(EMAIL_BACKEND="anymail.backends.sendgrid.EmailBackend")
class SendGridBackendDisallowsV2Tests(SimpleTestCase, AnymailTestMixin):
class SendGridBackendDisallowsV2Tests(AnymailTestMixin, SimpleTestCase):
"""Using v2-API-only features should cause errors with v3 backend"""
@override_settings(ANYMAIL={'SENDGRID_USERNAME': 'sg_username', 'SENDGRID_PASSWORD': 'sg_password'})

View File

@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
import json
from io import BytesIO
from textwrap import dedent
import six
from django.test import tag
from mock import ANY
@@ -91,13 +89,13 @@ class SendgridInboundTestCase(WebhookTestCase):
])
def test_attachments(self):
att1 = six.BytesIO('test attachment'.encode('utf-8'))
att1 = BytesIO('test attachment'.encode('utf-8'))
att1.name = 'test.txt'
image_content = sample_image_content()
att2 = six.BytesIO(image_content)
att2 = BytesIO(image_content)
att2.name = 'image.png'
email_content = sample_email_content()
att3 = six.BytesIO(email_content)
att3 = BytesIO(email_content)
att3.content_type = 'message/rfc822; charset="us-ascii"'
raw_event = {
'headers': '',
@@ -124,7 +122,7 @@ class SendgridInboundTestCase(WebhookTestCase):
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), u'test attachment')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)
@@ -183,8 +181,8 @@ class SendgridInboundTestCase(WebhookTestCase):
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertEqual(message.subject, 'Raw MIME test')
self.assertEqual(message.text, u"It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, u"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
def test_inbound_charsets(self):
# Captured (sanitized) from actual SendGrid inbound webhook payload 7/2020,
@@ -233,11 +231,11 @@ class SendgridInboundTestCase(WebhookTestCase):
event = kwargs['event']
message = event.message
self.assertEqual(message.from_email.display_name, u"Opérateur de test")
self.assertEqual(message.from_email.display_name, "Opérateur de test")
self.assertEqual(message.from_email.addr_spec, "sender@example.com")
self.assertEqual(len(message.to), 1)
self.assertEqual(message.to[0].display_name, u"Récipiendaire précieux")
self.assertEqual(message.to[0].display_name, "Récipiendaire précieux")
self.assertEqual(message.to[0].addr_spec, "inbound@sg.example.com")
self.assertEqual(message.subject, u"Como usted pidió")
self.assertEqual(message.text, u"Test the ESPs inbound charset handling…")
self.assertEqual(message.html, u"<p>¿Esto se ve como esperabas?</p>")
self.assertEqual(message.subject, "Como usted pidió")
self.assertEqual(message.text, "Test the ESPs inbound charset handling…")
self.assertEqual(message.html, "<p>¿Esto se ve como esperabas?</p>")

View File

@@ -22,7 +22,7 @@ SENDGRID_TEST_TEMPLATE_ID = os.getenv('SENDGRID_TEST_TEMPLATE_ID')
"mail_settings": {"sandbox_mode": {"enable": True}},
}},
EMAIL_BACKEND="anymail.backends.sendgrid.EmailBackend")
class SendGridBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
class SendGridBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""SendGrid v3 API integration tests
These tests run against the **live** SendGrid API, using the
@@ -38,7 +38,7 @@ class SendGridBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
"""
def setUp(self):
super(SendGridBackendIntegrationTests, self).setUp()
super().setUp()
self.message = AnymailMessage('Anymail SendGrid integration test', 'Text content',
'from@example.com', ['to@sink.sendgrid.net'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")

View File

@@ -7,16 +7,16 @@ from mock import ANY
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.sendgrid import SendGridTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestsMixin, WebhookTestCase
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('sendgrid')
class SendGridWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
class SendGridWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps([]))
# Actual tests are in WebhookBasicAuthTestsMixin
# Actual tests are in WebhookBasicAuthTestCase
@tag('sendgrid')

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import json
from base64 import b64encode, b64decode
from datetime import datetime
@@ -7,7 +5,6 @@ from decimal import Decimal
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
import six
from django.core import mail
from django.test import SimpleTestCase, override_settings, tag
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone
@@ -15,12 +12,9 @@ from django.utils.timezone import get_fixed_timezone, override as override_curre
from anymail.exceptions import (AnymailAPIError, AnymailConfigurationError, AnymailSerializationError,
AnymailUnsupportedFeature)
from anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCases
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin
# noinspection PyUnresolvedReferences
longtype = int if six.PY3 else long # NOQA: F821
@tag('sendinblue')
@override_settings(EMAIL_BACKEND='anymail.backends.sendinblue.EmailBackend',
@@ -31,7 +25,7 @@ class SendinBlueBackendMockAPITestCase(RequestsBackendMockAPITestCase):
DEFAULT_STATUS_CODE = 201 # SendinBlue v3 uses '201 Created' for success (in most cases)
def setUp(self):
super(SendinBlueBackendMockAPITestCase, self).setUp()
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
@@ -119,13 +113,12 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
self.assertNotIn('textContent', data)
def test_extra_headers(self):
self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123, 'X-Long': longtype(123),
self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123,
'Reply-To': '"Do Not Reply" <noreply@example.com>'}
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['headers']['X-Custom'], 'string')
self.assertEqual(data['headers']['X-Num'], 123)
self.assertEqual(data['headers']['X-Long'], 123)
# Reply-To must be moved to separate param
self.assertNotIn('Reply-To', data['headers'])
self.assertEqual(data['replyTo'], {'name': "Do Not Reply", 'email': "noreply@example.com"})
@@ -185,11 +178,11 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
'content': b64encode(pdf_content).decode('ascii')})
def test_unicode_attachment_correctly_decoded(self):
self.message.attach(u"Une pièce jointe.html", u'<p>\u2019</p>', mimetype='text/html')
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.send()
attachment = self.get_api_call_json()['attachment'][0]
self.assertEqual(attachment['name'], u'Une pièce jointe.html')
self.assertEqual(b64decode(attachment['content']).decode('utf-8'), u'<p>\u2019</p>')
self.assertEqual(attachment['name'], 'Une pièce jointe.html')
self.assertEqual(b64decode(attachment['content']).decode('utf-8'), '<p>\u2019</p>')
def test_embedded_images(self):
# SendinBlue doesn't support inline image
@@ -284,7 +277,7 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
self.message.send()
def test_metadata(self):
self.message.metadata = {'user_id': "12345", 'items': 6, 'float': 98.6, 'long': longtype(123)}
self.message.metadata = {'user_id': "12345", 'items': 6, 'float': 98.6}
self.message.send()
data = self.get_api_call_json()
@@ -293,7 +286,6 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
self.assertEqual(metadata['user_id'], "12345")
self.assertEqual(metadata['items'], 6)
self.assertEqual(metadata['float'], 98.6)
self.assertEqual(metadata['long'], longtype(123))
def test_send_at(self):
utc_plus_6 = get_fixed_timezone(6 * 60)
@@ -451,14 +443,14 @@ class SendinBlueBackendRecipientsRefusedTests(SendinBlueBackendMockAPITestCase):
@tag('sendinblue')
class SendinBlueBackendSessionSharingTestCase(SessionSharingTestCasesMixin, SendinBlueBackendMockAPITestCase):
class SendinBlueBackendSessionSharingTestCase(SessionSharingTestCases, SendinBlueBackendMockAPITestCase):
"""Requests session sharing tests"""
pass # tests are defined in the mixin
pass # tests are defined in SessionSharingTestCases
@tag('sendinblue')
@override_settings(EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend")
class SendinBlueBackendImproperlyConfiguredTests(SimpleTestCase, AnymailTestMixin):
class SendinBlueBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""
def test_missing_auth(self):

View File

@@ -18,7 +18,7 @@ SENDINBLUE_TEST_API_KEY = os.getenv('SENDINBLUE_TEST_API_KEY')
@override_settings(ANYMAIL_SENDINBLUE_API_KEY=SENDINBLUE_TEST_API_KEY,
ANYMAIL_SENDINBLUE_SEND_DEFAULTS=dict(),
EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend")
class SendinBlueBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""SendinBlue v3 API integration tests
SendinBlue doesn't have sandbox so these tests run
@@ -31,7 +31,7 @@ class SendinBlueBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
"""
def setUp(self):
super(SendinBlueBackendIntegrationTests, self).setUp()
super().setUp()
self.message = AnymailMessage('Anymail SendinBlue integration test', 'Text content',
'from@test-sb.anymail.info', ['test+to1@anymail.info'])

View File

@@ -7,16 +7,16 @@ from mock import ANY
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.sendinblue import SendinBlueTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestsMixin, WebhookTestCase
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('sendinblue')
class SendinBlueWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
class SendinBlueWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps({}))
# Actual tests are in WebhookBasicAuthTestsMixin
# Actual tests are in WebhookBasicAuthTestCase
@tag('sendinblue')

View File

@@ -1,121 +0,0 @@
"""
Django settings for Anymail tests.
Generated by 'django-admin startproject' using Django 1.11.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'NOT_FOR_PRODUCTION_USE'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'anymail',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'tests.test_settings.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'tests.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'

View File

@@ -1,5 +1,5 @@
from django.conf.urls import include, url
from django.urls import include, re_path
urlpatterns = [
url(r'^anymail/', include('anymail.urls')),
re_path(r'^anymail/', include('anymail.urls')),
]

View File

@@ -1,32 +1,30 @@
# -*- coding: utf-8 -*-
from datetime import datetime, date
import os
from datetime import date, datetime
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
import os
from io import BytesIO
import requests
import six
from django.core import mail
from django.test import SimpleTestCase, override_settings, tag
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone, utc
from mock import patch
from anymail.exceptions import (AnymailAPIError, AnymailUnsupportedFeature, AnymailRecipientsRefused,
AnymailConfigurationError, AnymailInvalidAddress)
from anymail.exceptions import (
AnymailAPIError, AnymailConfigurationError, AnymailInvalidAddress, AnymailRecipientsRefused,
AnymailUnsupportedFeature)
from anymail.message import attach_inline_image_file
from .utils import AnymailTestMixin, decode_att, SAMPLE_IMAGE_FILENAME, sample_image_path, sample_image_content
from .utils import AnymailTestMixin, SAMPLE_IMAGE_FILENAME, decode_att, sample_image_content, sample_image_path
@tag('sparkpost')
@override_settings(EMAIL_BACKEND='anymail.backends.sparkpost.EmailBackend',
ANYMAIL={'SPARKPOST_API_KEY': 'test_api_key'})
class SparkPostBackendMockAPITestCase(SimpleTestCase, AnymailTestMixin):
class SparkPostBackendMockAPITestCase(AnymailTestMixin, SimpleTestCase):
"""TestCase that uses SparkPostEmailBackend with a mocked transmissions.send API"""
def setUp(self):
super(SparkPostBackendMockAPITestCase, self).setUp()
super().setUp()
self.patch_send = patch('sparkpost.Transmissions.send', autospec=True)
self.mock_send = self.patch_send.start()
self.addCleanup(self.patch_send.stop)
@@ -52,7 +50,7 @@ class SparkPostBackendMockAPITestCase(SimpleTestCase, AnymailTestMixin):
response = requests.Response()
response.status_code = status_code
response.encoding = encoding
response.raw = six.BytesIO(raw)
response.raw = BytesIO(raw)
response.url = "/mock/send"
self.mock_send.side_effect = SparkPostAPIException(response)
@@ -205,7 +203,7 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
def test_unicode_attachment_correctly_decoded(self):
# Slight modification from the Django unicode docs:
# http://django.readthedocs.org/en/latest/ref/unicode.html#email
self.message.attach(u"Une pièce jointe.html", u'<p>\u2019</p>', mimetype='text/html')
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.send()
params = self.get_send_params()
attachments = params['attachments']
@@ -609,7 +607,7 @@ class SparkPostBackendRecipientsRefusedTests(SparkPostBackendMockAPITestCase):
@tag('sparkpost')
@override_settings(EMAIL_BACKEND="anymail.backends.sparkpost.EmailBackend")
class SparkPostBackendConfigurationTests(SimpleTestCase, AnymailTestMixin):
class SparkPostBackendConfigurationTests(AnymailTestMixin, SimpleTestCase):
"""Test various SparkPost client options"""
def test_missing_api_key(self):

View File

@@ -80,8 +80,8 @@ class SparkpostInboundTestCase(WebhookTestCase):
['cc@example.com'])
self.assertEqual(message.subject, 'Test subject')
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
self.assertEqual(message.text, u"It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, u"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
@@ -158,7 +158,7 @@ class SparkpostInboundTestCase(WebhookTestCase):
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), u'test attachment')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)

View File

@@ -1,5 +1,6 @@
import os
import unittest
import warnings
from datetime import datetime, timedelta
from django.test import SimpleTestCase, override_settings, tag
@@ -18,7 +19,7 @@ SPARKPOST_TEST_API_KEY = os.getenv('SPARKPOST_TEST_API_KEY')
"to run SparkPost integration tests")
@override_settings(ANYMAIL_SPARKPOST_API_KEY=SPARKPOST_TEST_API_KEY,
EMAIL_BACKEND="anymail.backends.sparkpost.EmailBackend")
class SparkPostBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
class SparkPostBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""SparkPost API integration tests
These tests run against the **live** SparkPost API, using the
@@ -28,19 +29,31 @@ class SparkPostBackendIntegrationTests(SimpleTestCase, AnymailTestMixin):
SparkPost doesn't offer a test mode -- it tries to send everything
you ask. To avoid stacking up a pile of undeliverable @example.com
emails, the tests use SparkPost's "sink domain" @*.sink.sparkpostmail.com.
https://support.sparkpost.com/customer/en/portal/articles/2361300-how-to-test-integrations
https://www.sparkpost.com/docs/faq/using-sink-server/
SparkPost also doesn't support arbitrary senders (so no from@example.com).
We've set up @test-sp.anymail.info as a validated sending domain for these tests.
"""
def setUp(self):
super(SparkPostBackendIntegrationTests, self).setUp()
super().setUp()
self.message = AnymailMessage('Anymail SparkPost integration test', 'Text content',
'test@test-sp.anymail.info', ['to@test.sink.sparkpostmail.com'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
# The SparkPost Python package uses requests directly, without managing sessions, and relies
# on GC to close connections. This leads to harmless (but valid) warnings about unclosed
# ssl.SSLSocket during cleanup: https://github.com/psf/requests/issues/1882
# There's not much we can do about that, short of switching from the SparkPost package
# to our own requests backend implementation (which *does* manage sessions properly).
# Unless/until we do that, filter the warnings to avoid test noise.
# Filter in TestCase.setUp because unittest resets the warning filters for each test.
# https://stackoverflow.com/a/26620811/647002
from anymail.backends.base_requests import AnymailRequestsBackend
from anymail.backends.sparkpost import EmailBackend as SparkPostBackend
assert not issubclass(SparkPostBackend, AnymailRequestsBackend) # else this filter can be removed
warnings.filterwarnings("ignore", message=r"unclosed <ssl\.SSLSocket", category=ResourceWarning)
def test_simple_send(self):
# Example of getting the SparkPost send status and transmission id from the message
sent_count = self.message.send()

View File

@@ -8,16 +8,16 @@ from mock import ANY
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.sparkpost import SparkPostTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestsMixin, WebhookTestCase
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('sparkpost')
class SparkPostWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
class SparkPostWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps([]))
# Actual tests are in WebhookBasicAuthTestsMixin
# Actual tests are in WebhookBasicAuthTestCase
@tag('sparkpost')

View File

@@ -4,22 +4,11 @@ import base64
import copy
import pickle
from email.mime.image import MIMEImage
from unittest import skipIf
import six
from django.http import QueryDict
from django.test import SimpleTestCase, RequestFactory, override_settings
from django.utils.translation import ugettext_lazy
try:
from django.utils.text import format_lazy # Django >= 1.11
except ImportError:
format_lazy = None
try:
from django.utils.translation import string_concat # Django < 2.1
except ImportError:
string_concat = None
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy
from anymail.exceptions import AnymailInvalidAddress, _LazyError
from anymail.utils import (
@@ -66,11 +55,11 @@ class ParseAddressListTests(SimpleTestCase):
self.assertEqual(parsed.address, 'Display Name <test@example.com>')
def test_unicode_display_name(self):
parsed_list = parse_address_list([u'"Unicode \N{HEAVY BLACK HEART}" <test@example.com>'])
parsed_list = parse_address_list(['"Unicode \N{HEAVY BLACK HEART}" <test@example.com>'])
self.assertEqual(len(parsed_list), 1)
parsed = parsed_list[0]
self.assertEqual(parsed.addr_spec, "test@example.com")
self.assertEqual(parsed.display_name, u"Unicode \N{HEAVY BLACK HEART}")
self.assertEqual(parsed.display_name, "Unicode \N{HEAVY BLACK HEART}")
# formatted display-name automatically shifts to quoted-printable/base64 for non-ascii chars:
self.assertEqual(parsed.address, '=?utf-8?b?VW5pY29kZSDinaQ=?= <test@example.com>')
@@ -83,13 +72,13 @@ class ParseAddressListTests(SimpleTestCase):
parse_address_list(['Display Name, Inc. <test@example.com>'])
def test_idn(self):
parsed_list = parse_address_list([u"idn@\N{ENVELOPE}.example.com"])
parsed_list = parse_address_list(["idn@\N{ENVELOPE}.example.com"])
self.assertEqual(len(parsed_list), 1)
parsed = parsed_list[0]
self.assertEqual(parsed.addr_spec, u"idn@\N{ENVELOPE}.example.com")
self.assertEqual(parsed.addr_spec, "idn@\N{ENVELOPE}.example.com")
self.assertEqual(parsed.address, "idn@xn--4bi.example.com") # punycode-encoded domain
self.assertEqual(parsed.username, "idn")
self.assertEqual(parsed.domain, u"\N{ENVELOPE}.example.com")
self.assertEqual(parsed.domain, "\N{ENVELOPE}.example.com")
def test_none_address(self):
# used for, e.g., telling Mandrill to use template default from_email
@@ -139,10 +128,9 @@ class ParseAddressListTests(SimpleTestCase):
parse_address_list(['"Display Name"', '<valid@example.com>'])
def test_invalid_with_unicode(self):
# (assertRaisesMessage can't handle unicode in Python 2)
with self.assertRaises(AnymailInvalidAddress) as cm:
parse_address_list([u"\N{ENVELOPE}"])
self.assertIn(u"Invalid email address '\N{ENVELOPE}'", six.text_type(cm.exception))
with self.assertRaisesMessage(AnymailInvalidAddress,
"Invalid email address '\N{ENVELOPE}'"):
parse_address_list(["\N{ENVELOPE}"])
def test_single_string(self):
# bare strings are used by the from_email parsing in BasePayload
@@ -151,12 +139,12 @@ class ParseAddressListTests(SimpleTestCase):
self.assertEqual(parsed_list[0].addr_spec, "one@example.com")
def test_lazy_strings(self):
parsed_list = parse_address_list([ugettext_lazy('"Example, Inc." <one@example.com>')])
parsed_list = parse_address_list([gettext_lazy('"Example, Inc." <one@example.com>')])
self.assertEqual(len(parsed_list), 1)
self.assertEqual(parsed_list[0].display_name, "Example, Inc.")
self.assertEqual(parsed_list[0].addr_spec, "one@example.com")
parsed_list = parse_address_list(ugettext_lazy("one@example.com"))
parsed_list = parse_address_list(gettext_lazy("one@example.com"))
self.assertEqual(len(parsed_list), 1)
self.assertEqual(parsed_list[0].display_name, "")
self.assertEqual(parsed_list[0].addr_spec, "one@example.com")
@@ -221,47 +209,38 @@ class LazyCoercionTests(SimpleTestCase):
"""Test utils.is_lazy and force_non_lazy*"""
def test_is_lazy(self):
self.assertTrue(is_lazy(ugettext_lazy("lazy string is lazy")))
self.assertTrue(is_lazy(gettext_lazy("lazy string is lazy")))
def test_not_lazy(self):
self.assertFalse(is_lazy(u"text not lazy"))
self.assertFalse(is_lazy("text not lazy"))
self.assertFalse(is_lazy(b"bytes not lazy"))
self.assertFalse(is_lazy(None))
self.assertFalse(is_lazy({'dict': "not lazy"}))
self.assertFalse(is_lazy(["list", "not lazy"]))
self.assertFalse(is_lazy(object()))
self.assertFalse(is_lazy([ugettext_lazy("doesn't recurse")]))
self.assertFalse(is_lazy([gettext_lazy("doesn't recurse")]))
def test_force_lazy(self):
result = force_non_lazy(ugettext_lazy(u"text"))
self.assertIsInstance(result, six.text_type)
self.assertEqual(result, u"text")
result = force_non_lazy(gettext_lazy("text"))
self.assertIsInstance(result, str)
self.assertEqual(result, "text")
@skipIf(string_concat is None, "string_concat not in this Django version")
def test_force_concat(self):
self.assertTrue(is_lazy(string_concat(ugettext_lazy("concatenation"),
ugettext_lazy("is lazy"))))
result = force_non_lazy(string_concat(ugettext_lazy(u"text"), ugettext_lazy("concat")))
self.assertIsInstance(result, six.text_type)
self.assertEqual(result, u"textconcat")
@skipIf(format_lazy is None, "format_lazy not in this Django version")
def test_format_lazy(self):
self.assertTrue(is_lazy(format_lazy("{0}{1}",
ugettext_lazy("concatenation"), ugettext_lazy("is lazy"))))
gettext_lazy("concatenation"), gettext_lazy("is lazy"))))
result = force_non_lazy(format_lazy("{first}/{second}",
first=ugettext_lazy(u"text"), second=ugettext_lazy("format")))
self.assertIsInstance(result, six.text_type)
self.assertEqual(result, u"text/format")
first=gettext_lazy("text"), second=gettext_lazy("format")))
self.assertIsInstance(result, str)
self.assertEqual(result, "text/format")
def test_force_string(self):
result = force_non_lazy(u"text")
self.assertIsInstance(result, six.text_type)
self.assertEqual(result, u"text")
result = force_non_lazy("text")
self.assertIsInstance(result, str)
self.assertEqual(result, "text")
def test_force_bytes(self):
result = force_non_lazy(b"bytes \xFE")
self.assertIsInstance(result, six.binary_type)
self.assertIsInstance(result, bytes)
self.assertEqual(result, b"bytes \xFE")
def test_force_none(self):
@@ -269,16 +248,16 @@ class LazyCoercionTests(SimpleTestCase):
self.assertIsNone(result)
def test_force_dict(self):
result = force_non_lazy_dict({'a': 1, 'b': ugettext_lazy(u"b"),
'c': {'c1': ugettext_lazy(u"c1")}})
self.assertEqual(result, {'a': 1, 'b': u"b", 'c': {'c1': u"c1"}})
self.assertIsInstance(result['b'], six.text_type)
self.assertIsInstance(result['c']['c1'], six.text_type)
result = force_non_lazy_dict({'a': 1, 'b': gettext_lazy("b"),
'c': {'c1': gettext_lazy("c1")}})
self.assertEqual(result, {'a': 1, 'b': "b", 'c': {'c1': "c1"}})
self.assertIsInstance(result['b'], str)
self.assertIsInstance(result['c']['c1'], str)
def test_force_list(self):
result = force_non_lazy_list([0, ugettext_lazy(u"b"), u"c"])
self.assertEqual(result, [0, u"b", u"c"]) # coerced to list
self.assertIsInstance(result[1], six.text_type)
result = force_non_lazy_list([0, gettext_lazy("b"), "c"])
self.assertEqual(result, [0, "b", "c"]) # coerced to list
self.assertIsInstance(result[1], str)
class UpdateDeepTests(SimpleTestCase):
@@ -313,7 +292,7 @@ class RequestUtilsTests(SimpleTestCase):
def setUp(self):
self.request_factory = RequestFactory()
super(RequestUtilsTests, self).setUp()
super().setUp()
@staticmethod
def basic_auth(username, password):

View File

@@ -1,6 +1,4 @@
# Anymail test utils
import collections
import logging
import os
import re
import sys
@@ -8,10 +6,10 @@ import uuid
import warnings
from base64 import b64decode
from contextlib import contextmanager
from io import StringIO
from unittest import TestCase
import six
from django.test import Client
from six.moves import StringIO
def decode_att(att):
@@ -71,36 +69,9 @@ def sample_email_content(filename=SAMPLE_EMAIL_FILENAME):
# TestCase helpers
#
# noinspection PyUnresolvedReferences
class AnymailTestMixin:
class AnymailTestMixin(TestCase):
"""Helpful additional methods for Anymail tests"""
def assertLogs(self, logger=None, level=None):
# Note: django.utils.log.DEFAULT_LOGGING config is set to *not* propagate certain
# logging records. That means you *can't* capture those logs at the root (None) logger.
assert logger is not None # `None` root logger won't reliably capture
try:
return super(AnymailTestMixin, self).assertLogs(logger, level)
except (AttributeError, TypeError):
# Python <3.4: use our backported assertLogs
return _AssertLogsContext(self, logger, level)
def assertWarns(self, expected_warning, msg=None):
# We only support the context-manager version
try:
return super(AnymailTestMixin, self).assertWarns(expected_warning, msg=msg)
except TypeError:
# Python 2.x: use our backported assertWarns
return _AssertWarnsContext(expected_warning, self, msg=msg)
def assertWarnsRegex(self, expected_warning, expected_regex, msg=None):
# We only support the context-manager version
try:
return super(AnymailTestMixin, self).assertWarnsRegex(expected_warning, expected_regex, msg=msg)
except TypeError:
# Python 2.x: use our backported assertWarns
return _AssertWarnsContext(expected_warning, self, expected_regex=expected_regex, msg=msg)
@contextmanager
def assertDoesNotWarn(self, disallowed_warning=Warning):
"""Makes test error (rather than fail) if disallowed_warning occurs.
@@ -115,31 +86,13 @@ class AnymailTestMixin:
finally:
warnings.resetwarnings()
def assertCountEqual(self, *args, **kwargs):
try:
return super(AnymailTestMixin, self).assertCountEqual(*args, **kwargs)
except TypeError:
return self.assertItemsEqual(*args, **kwargs) # Python 2
def assertRaisesRegex(self, *args, **kwargs):
try:
return super(AnymailTestMixin, self).assertRaisesRegex(*args, **kwargs)
except TypeError:
return self.assertRaisesRegexp(*args, **kwargs) # Python 2
def assertRegex(self, *args, **kwargs):
try:
return super(AnymailTestMixin, self).assertRegex(*args, **kwargs)
except TypeError:
return self.assertRegexpMatches(*args, **kwargs) # Python 2
def assertEqualIgnoringHeaderFolding(self, first, second, msg=None):
# Unfold (per RFC-8222) all text first and second, then compare result.
# Useful for message/rfc822 attachment tests, where various Python email
# versions handled folding slightly differently.
# (Technically, this is unfolding both headers and (incorrectly) bodies,
# but that doesn't really affect the tests.)
if isinstance(first, six.binary_type) and isinstance(second, six.binary_type):
if isinstance(first, bytes) and isinstance(second, bytes):
first = first.decode('utf-8')
second = second.decode('utf-8')
first = rfc822_unfold(first)
@@ -190,131 +143,6 @@ class AnymailTestMixin:
sys.stdout = old_stdout
# Backported from Python 3.4
class _AssertLogsContext(object):
"""A context manager used to implement TestCase.assertLogs()."""
LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
def __init__(self, test_case, logger_name, level):
self.test_case = test_case
self.logger_name = logger_name
if level:
self.level = logging._nameToLevel.get(level, level)
else:
self.level = logging.INFO
self.msg = None
def _raiseFailure(self, standardMsg):
msg = self.test_case._formatMessage(self.msg, standardMsg)
raise self.test_case.failureException(msg)
class _CapturingHandler(logging.Handler):
"""A logging handler capturing all (raw and formatted) logging output."""
_LoggingWatcher = collections.namedtuple("_LoggingWatcher", ["records", "output"])
def __init__(self):
logging.Handler.__init__(self)
self.watcher = self._LoggingWatcher([], [])
def flush(self):
pass
def emit(self, record):
self.watcher.records.append(record)
msg = self.format(record)
self.watcher.output.append(msg)
def __enter__(self):
if isinstance(self.logger_name, logging.Logger):
logger = self.logger = self.logger_name
else:
logger = self.logger = logging.getLogger(self.logger_name)
formatter = logging.Formatter(self.LOGGING_FORMAT)
handler = self._CapturingHandler()
handler.setFormatter(formatter)
self.watcher = handler.watcher
self.old_handlers = logger.handlers[:]
self.old_level = logger.level
self.old_propagate = logger.propagate
logger.handlers = [handler]
logger.setLevel(self.level)
logger.propagate = False
return handler.watcher
def __exit__(self, exc_type, exc_value, tb):
self.logger.handlers = self.old_handlers
self.logger.propagate = self.old_propagate
self.logger.setLevel(self.old_level)
if exc_type is not None:
# let unexpected exceptions pass through
return False
if len(self.watcher.records) == 0:
self._raiseFailure(
"no logs of level {} or higher triggered on {}"
.format(logging.getLevelName(self.level), self.logger.name))
# Backported from python 3.5
class _AssertWarnsContext(object):
"""A context manager used to implement TestCase.assertWarns* methods."""
def __init__(self, expected, test_case, expected_regex=None, msg=None):
self.test_case = test_case
self.expected = expected
self.test_case = test_case
if expected_regex is not None:
expected_regex = re.compile(expected_regex)
self.expected_regex = expected_regex
self.msg = msg
def _raiseFailure(self, standardMsg):
# msg = self.test_case._formatMessage(self.msg, standardMsg)
msg = self.msg or standardMsg
raise self.test_case.failureException(msg)
def __enter__(self):
# The __warningregistry__'s need to be in a pristine state for tests
# to work properly.
for v in sys.modules.values():
if getattr(v, '__warningregistry__', None):
v.__warningregistry__ = {}
self.warnings_manager = warnings.catch_warnings(record=True)
self.warnings = self.warnings_manager.__enter__()
warnings.simplefilter("always", self.expected)
return self
def __exit__(self, exc_type, exc_value, tb):
self.warnings_manager.__exit__(exc_type, exc_value, tb)
if exc_type is not None:
# let unexpected exceptions pass through
return
try:
exc_name = self.expected.__name__
except AttributeError:
exc_name = str(self.expected)
first_matching = None
for m in self.warnings:
w = m.message
if not isinstance(w, self.expected):
continue
if first_matching is None:
first_matching = w
if self.expected_regex is not None and not self.expected_regex.search(str(w)):
continue
# store warning for later retrieval
self.warning = w
self.filename = m.filename
self.lineno = m.lineno
return
# Now we simply try to choose a helpful failure message
if first_matching is not None:
self._raiseFailure('"{}" does not match "{}"'.format(
self.expected_regex.pattern, str(first_matching)))
self._raiseFailure("{} not triggered".format(exc_name))
class ClientWithCsrfChecks(Client):
"""Django test Client that enforces CSRF checks
@@ -322,8 +150,7 @@ class ClientWithCsrfChecks(Client):
"""
def __init__(self, **defaults):
super(ClientWithCsrfChecks, self).__init__(
enforce_csrf_checks=True, **defaults)
super().__init__(enforce_csrf_checks=True, **defaults)
# dedent for bytestrs

View File

@@ -25,7 +25,7 @@ class WebhookTestCase(AnymailTestMixin, SimpleTestCase):
client_class = ClientWithCsrfChecks
def setUp(self):
super(WebhookTestCase, self).setUp()
super().setUp()
# Use correct basic auth by default (individual tests can override):
self.set_basic_auth()
@@ -71,15 +71,24 @@ class WebhookTestCase(AnymailTestMixin, SimpleTestCase):
return actual_kwargs
# noinspection PyUnresolvedReferences
class WebhookBasicAuthTestsMixin(object):
class WebhookBasicAuthTestCase(WebhookTestCase):
"""Common test cases for webhook basic authentication.
Instantiate for each ESP's webhooks by:
- mixing into WebhookTestCase
- subclassing
- defining call_webhook to invoke the ESP's webhook
- adding or overriding any tests as appropriate
"""
def __init__(self, methodName='runTest'):
if self.__class__ is WebhookBasicAuthTestCase:
# don't run these tests on the abstract base implementation
methodName = 'runNoTestsInBaseClass'
super().__init__(methodName)
def runNoTestsInBaseClass(self):
pass
should_warn_if_no_auth = True # subclass set False if other webhook verification used
def call_webhook(self):