mirror of
https://github.com/django/django.git
synced 2025-03-13 10:50:55 +00:00
Refs #35581 -- Improved reporting for failing tests in mail tests.
- Converted HeadersCheckMixin to MailTestsMixin for all shared helpers: - Hoisted assertStartsWith() from BaseEmailBackendTests. - Added matching assertEndsWith(). - Hoisted get_decoded_attachments() from MailTests. - Improved failure reporting in assertMessageHasHeaders(). - Used unittest subTest() to improve handling of compound test cases. - Replaced `assertTrue(test on string)` with custom assertions, so that failure reporting is more informative than `True != False`.
This commit is contained in:
parent
871e1ee5ff
commit
bddd35cb1a
@ -41,7 +41,7 @@ except ImportError:
|
|||||||
HAS_AIOSMTPD = False
|
HAS_AIOSMTPD = False
|
||||||
|
|
||||||
|
|
||||||
class HeadersCheckMixin:
|
class MailTestsMixin:
|
||||||
def assertMessageHasHeaders(self, message, headers):
|
def assertMessageHasHeaders(self, message, headers):
|
||||||
"""
|
"""
|
||||||
Asserts that the `message` has all `headers`.
|
Asserts that the `message` has all `headers`.
|
||||||
@ -53,17 +53,53 @@ class HeadersCheckMixin:
|
|||||||
if isinstance(message, bytes):
|
if isinstance(message, bytes):
|
||||||
message = message_from_bytes(message)
|
message = message_from_bytes(message)
|
||||||
msg_headers = set(message.items())
|
msg_headers = set(message.items())
|
||||||
self.assertTrue(
|
if not headers.issubset(msg_headers):
|
||||||
headers.issubset(msg_headers),
|
missing = "\n".join(f" {h}: {v}" for h, v in headers - msg_headers)
|
||||||
msg="Message is missing "
|
actual = "\n".join(f" {h}: {v}" for h, v in msg_headers)
|
||||||
"the following headers: %s" % (headers - msg_headers),
|
raise self.failureException(
|
||||||
)
|
f"Expected headers not found in message.\n"
|
||||||
|
f"Missing headers:\n{missing}\n"
|
||||||
|
f"Actual headers:\n{actual}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# In assertStartsWith()/assertEndsWith() failure messages, when truncating
|
||||||
|
# a long first ("haystack") string, include this many characters beyond the
|
||||||
|
# length of the second ("needle") string.
|
||||||
|
START_END_EXTRA_CONTEXT = 15
|
||||||
|
|
||||||
class MailTests(HeadersCheckMixin, SimpleTestCase):
|
def assertStartsWith(self, first, second):
|
||||||
"""
|
if not first.startswith(second):
|
||||||
Non-backend specific tests.
|
# Use assertEqual() for failure message with diffs. If first value
|
||||||
"""
|
# is much longer than second, truncate end and add an ellipsis.
|
||||||
|
self.longMessage = True
|
||||||
|
max_len = len(second) + self.START_END_EXTRA_CONTEXT
|
||||||
|
start_of_first = (
|
||||||
|
first
|
||||||
|
if len(first) <= max_len
|
||||||
|
else first[:max_len] + ("…" if isinstance(first, str) else b"...")
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
start_of_first,
|
||||||
|
second,
|
||||||
|
"First string doesn't start with the second.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def assertEndsWith(self, first, second):
|
||||||
|
if not first.endswith(second):
|
||||||
|
# Use assertEqual() for failure message with diffs. If first value
|
||||||
|
# is much longer than second, truncate start and prepend an ellipsis.
|
||||||
|
self.longMessage = True
|
||||||
|
max_len = len(second) + self.START_END_EXTRA_CONTEXT
|
||||||
|
end_of_first = (
|
||||||
|
first
|
||||||
|
if len(first) <= max_len
|
||||||
|
else ("…" if isinstance(first, str) else b"...") + first[-max_len:]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
end_of_first,
|
||||||
|
second,
|
||||||
|
"First string doesn't end with the second.",
|
||||||
|
)
|
||||||
|
|
||||||
def get_decoded_attachments(self, django_message):
|
def get_decoded_attachments(self, django_message):
|
||||||
"""
|
"""
|
||||||
@ -84,6 +120,12 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
|
|||||||
|
|
||||||
return list(iter_attachments())
|
return list(iter_attachments())
|
||||||
|
|
||||||
|
|
||||||
|
class MailTests(MailTestsMixin, SimpleTestCase):
|
||||||
|
"""
|
||||||
|
Non-backend specific tests.
|
||||||
|
"""
|
||||||
|
|
||||||
def test_ascii(self):
|
def test_ascii(self):
|
||||||
email = EmailMessage(
|
email = EmailMessage(
|
||||||
"Subject", "Content", "from@example.com", ["to@example.com"]
|
"Subject", "Content", "from@example.com", ["to@example.com"]
|
||||||
@ -303,26 +345,16 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
|
|||||||
|
|
||||||
def test_header_injection(self):
|
def test_header_injection(self):
|
||||||
msg = "Header values can't contain newlines "
|
msg = "Header values can't contain newlines "
|
||||||
email = EmailMessage(
|
cases = [
|
||||||
"Subject\nInjection Test", "Content", "from@example.com", ["to@example.com"]
|
{"subject": "Subject\nInjection Test"},
|
||||||
)
|
{"subject": gettext_lazy("Lazy Subject\nInjection Test")},
|
||||||
with self.assertRaisesMessage(BadHeaderError, msg):
|
{"to": ["Name\nInjection test <to@example.com>"]},
|
||||||
email.message()
|
]
|
||||||
email = EmailMessage(
|
for kwargs in cases:
|
||||||
gettext_lazy("Subject\nInjection Test"),
|
with self.subTest(case=kwargs):
|
||||||
"Content",
|
email = EmailMessage(**kwargs)
|
||||||
"from@example.com",
|
with self.assertRaisesMessage(BadHeaderError, msg):
|
||||||
["to@example.com"],
|
email.message()
|
||||||
)
|
|
||||||
with self.assertRaisesMessage(BadHeaderError, msg):
|
|
||||||
email.message()
|
|
||||||
with self.assertRaisesMessage(BadHeaderError, msg):
|
|
||||||
EmailMessage(
|
|
||||||
"Subject",
|
|
||||||
"Content",
|
|
||||||
"from@example.com",
|
|
||||||
["Name\nInjection test <to@example.com>"],
|
|
||||||
).message()
|
|
||||||
|
|
||||||
def test_folding_white_space(self):
|
def test_folding_white_space(self):
|
||||||
"""
|
"""
|
||||||
@ -615,8 +647,8 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
|
|||||||
("Content-Transfer-Encoding", "quoted-printable"),
|
("Content-Transfer-Encoding", "quoted-printable"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertEndsWith(
|
||||||
payload0.as_bytes().endswith(b"\n\nFirstname S=FCrname is a great guy.")
|
payload0.as_bytes(), b"\n\nFirstname S=FCrname is a great guy."
|
||||||
)
|
)
|
||||||
# Check the text/html alternative.
|
# Check the text/html alternative.
|
||||||
payload1 = message.get_payload(1)
|
payload1 = message.get_payload(1)
|
||||||
@ -630,10 +662,9 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
|
|||||||
("Content-Transfer-Encoding", "quoted-printable"),
|
("Content-Transfer-Encoding", "quoted-printable"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertEndsWith(
|
||||||
payload1.as_bytes().endswith(
|
payload1.as_bytes(),
|
||||||
b"\n\n<p>Firstname S=FCrname is a <strong>great</strong> guy.</p>"
|
b"\n\n<p>Firstname S=FCrname is a <strong>great</strong> guy.</p>",
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_attachments(self):
|
def test_attachments(self):
|
||||||
@ -761,34 +792,38 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
|
|||||||
|
|
||||||
for basename, real_mimetype in files:
|
for basename, real_mimetype in files:
|
||||||
for mimetype in test_mimetypes:
|
for mimetype in test_mimetypes:
|
||||||
self.assertEqual(mimetypes.guess_type(basename)[0], real_mimetype)
|
with self.subTest(
|
||||||
expected_mimetype = (
|
basename=basename, real_mimetype=real_mimetype, mimetype=mimetype
|
||||||
mimetype or real_mimetype or "application/octet-stream"
|
):
|
||||||
)
|
self.assertEqual(mimetypes.guess_type(basename)[0], real_mimetype)
|
||||||
file_path = Path(__file__).parent / "attachments" / basename
|
expected_mimetype = (
|
||||||
expected_content = file_path.read_bytes()
|
mimetype or real_mimetype or "application/octet-stream"
|
||||||
if expected_mimetype.startswith("text/"):
|
)
|
||||||
try:
|
file_path = Path(__file__).parent / "attachments" / basename
|
||||||
expected_content = expected_content.decode()
|
expected_content = file_path.read_bytes()
|
||||||
except UnicodeDecodeError:
|
if expected_mimetype.startswith("text/"):
|
||||||
expected_mimetype = "application/octet-stream"
|
try:
|
||||||
|
expected_content = expected_content.decode()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
expected_mimetype = "application/octet-stream"
|
||||||
|
|
||||||
email = EmailMessage()
|
email = EmailMessage()
|
||||||
email.attach_file(file_path, mimetype=mimetype)
|
email.attach_file(file_path, mimetype=mimetype)
|
||||||
|
|
||||||
# Check EmailMessage.attachments.
|
# Check EmailMessage.attachments.
|
||||||
self.assertEqual(len(email.attachments), 1)
|
self.assertEqual(len(email.attachments), 1)
|
||||||
self.assertEqual(email.attachments[0].filename, basename)
|
self.assertEqual(email.attachments[0].filename, basename)
|
||||||
self.assertEqual(email.attachments[0].mimetype, expected_mimetype)
|
self.assertEqual(email.attachments[0].mimetype, expected_mimetype)
|
||||||
self.assertEqual(email.attachments[0].content, expected_content)
|
self.assertEqual(email.attachments[0].content, expected_content)
|
||||||
|
|
||||||
# Check attachments in generated message.
|
# Check attachments in the generated message.
|
||||||
# (The actual content is not checked as variations in platform
|
# (The actual content is not checked as variations in platform
|
||||||
# line endings and rfc822 refolding complicate the logic.)
|
# line endings and rfc822 refolding complicate the logic.)
|
||||||
actual_attachment = self.get_decoded_attachments(email)[0]
|
attachments = self.get_decoded_attachments(email)
|
||||||
actual_filename, actual_content, actual_mimetype = actual_attachment
|
self.assertEqual(len(attachments), 1)
|
||||||
self.assertEqual(actual_filename, basename)
|
actual = attachments[0]
|
||||||
self.assertEqual(actual_mimetype, expected_mimetype)
|
self.assertEqual(actual.filename, basename)
|
||||||
|
self.assertEqual(actual.mimetype, expected_mimetype)
|
||||||
|
|
||||||
def test_attach_text_as_bytes(self):
|
def test_attach_text_as_bytes(self):
|
||||||
msg = EmailMessage()
|
msg = EmailMessage()
|
||||||
@ -1207,7 +1242,7 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
|
|||||||
|
|
||||||
|
|
||||||
@requires_tz_support
|
@requires_tz_support
|
||||||
class MailTimeZoneTests(SimpleTestCase):
|
class MailTimeZoneTests(MailTestsMixin, SimpleTestCase):
|
||||||
@override_settings(
|
@override_settings(
|
||||||
EMAIL_USE_LOCALTIME=False, USE_TZ=True, TIME_ZONE="Africa/Algiers"
|
EMAIL_USE_LOCALTIME=False, USE_TZ=True, TIME_ZONE="Africa/Algiers"
|
||||||
)
|
)
|
||||||
@ -1216,7 +1251,7 @@ class MailTimeZoneTests(SimpleTestCase):
|
|||||||
EMAIL_USE_LOCALTIME=False creates a datetime in UTC.
|
EMAIL_USE_LOCALTIME=False creates a datetime in UTC.
|
||||||
"""
|
"""
|
||||||
email = EmailMessage()
|
email = EmailMessage()
|
||||||
self.assertTrue(email.message()["Date"].endswith("-0000"))
|
self.assertEndsWith(email.message()["Date"], "-0000")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
EMAIL_USE_LOCALTIME=True, USE_TZ=True, TIME_ZONE="Africa/Algiers"
|
EMAIL_USE_LOCALTIME=True, USE_TZ=True, TIME_ZONE="Africa/Algiers"
|
||||||
@ -1226,9 +1261,8 @@ class MailTimeZoneTests(SimpleTestCase):
|
|||||||
EMAIL_USE_LOCALTIME=True creates a datetime in the local time zone.
|
EMAIL_USE_LOCALTIME=True creates a datetime in the local time zone.
|
||||||
"""
|
"""
|
||||||
email = EmailMessage()
|
email = EmailMessage()
|
||||||
self.assertTrue(
|
# Africa/Algiers is UTC+1 year round.
|
||||||
email.message()["Date"].endswith("+0100")
|
self.assertEndsWith(email.message()["Date"], "+0100")
|
||||||
) # Africa/Algiers is UTC+1
|
|
||||||
|
|
||||||
|
|
||||||
class PythonGlobalState(SimpleTestCase):
|
class PythonGlobalState(SimpleTestCase):
|
||||||
@ -1259,7 +1293,7 @@ class PythonGlobalState(SimpleTestCase):
|
|||||||
self.assertIn("Content-Transfer-Encoding: base64", txt.as_string())
|
self.assertIn("Content-Transfer-Encoding: base64", txt.as_string())
|
||||||
|
|
||||||
|
|
||||||
class BaseEmailBackendTests(HeadersCheckMixin):
|
class BaseEmailBackendTests(MailTestsMixin):
|
||||||
email_backend = None
|
email_backend = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -1267,15 +1301,6 @@ class BaseEmailBackendTests(HeadersCheckMixin):
|
|||||||
cls.enterClassContext(override_settings(EMAIL_BACKEND=cls.email_backend))
|
cls.enterClassContext(override_settings(EMAIL_BACKEND=cls.email_backend))
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
def assertStartsWith(self, first, second):
|
|
||||||
if not first.startswith(second):
|
|
||||||
self.longMessage = True
|
|
||||||
self.assertEqual(
|
|
||||||
first[: len(second)],
|
|
||||||
second,
|
|
||||||
"First string doesn't start with the second.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_mailbox_content(self):
|
def get_mailbox_content(self):
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"subclasses of BaseEmailBackendTests must provide a get_mailbox_content() "
|
"subclasses of BaseEmailBackendTests must provide a get_mailbox_content() "
|
||||||
@ -1349,13 +1374,14 @@ class BaseEmailBackendTests(HeadersCheckMixin):
|
|||||||
# send_messages() may take a list or an iterator.
|
# send_messages() may take a list or an iterator.
|
||||||
emails_lists = ([email1, email2], iter((email1, email2)))
|
emails_lists = ([email1, email2], iter((email1, email2)))
|
||||||
for emails_list in emails_lists:
|
for emails_list in emails_lists:
|
||||||
num_sent = mail.get_connection().send_messages(emails_list)
|
with self.subTest(emails_list=repr(emails_list)):
|
||||||
self.assertEqual(num_sent, 2)
|
num_sent = mail.get_connection().send_messages(emails_list)
|
||||||
messages = self.get_mailbox_content()
|
self.assertEqual(num_sent, 2)
|
||||||
self.assertEqual(len(messages), 2)
|
messages = self.get_mailbox_content()
|
||||||
self.assertEqual(messages[0]["To"], "to-1@example.com")
|
self.assertEqual(len(messages), 2)
|
||||||
self.assertEqual(messages[1]["To"], "to-2@example.com")
|
self.assertEqual(messages[0]["To"], "to-1@example.com")
|
||||||
self.flush_mailbox()
|
self.assertEqual(messages[1]["To"], "to-2@example.com")
|
||||||
|
self.flush_mailbox()
|
||||||
|
|
||||||
def test_send_verbose_name(self):
|
def test_send_verbose_name(self):
|
||||||
email = EmailMessage(
|
email = EmailMessage(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user