1
0
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:
Mike Edmunds 2024-08-24 12:57:47 -07:00 committed by Sarah Boyce
parent 871e1ee5ff
commit bddd35cb1a

View File

@ -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(