mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
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:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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 ESP’s 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 ESP’s inbound charset handling…")
|
||||
self.assertEqual(message.html, "<p>¿Esto se ve como esperabas?</p>")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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/'
|
||||
@@ -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')),
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
183
tests/utils.py
183
tests/utils.py
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user