1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +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
class HeadersCheckMixin:
class MailTestsMixin:
def assertMessageHasHeaders(self, message, headers):
"""
Asserts that the `message` has all `headers`.
@ -53,17 +53,53 @@ class HeadersCheckMixin:
if isinstance(message, bytes):
message = message_from_bytes(message)
msg_headers = set(message.items())
self.assertTrue(
headers.issubset(msg_headers),
msg="Message is missing "
"the following headers: %s" % (headers - msg_headers),
)
if not headers.issubset(msg_headers):
missing = "\n".join(f" {h}: {v}" for h, v in headers - msg_headers)
actual = "\n".join(f" {h}: {v}" for h, v in 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):
"""
Non-backend specific tests.
"""
def assertStartsWith(self, first, second):
if not first.startswith(second):
# 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):
"""
@ -84,6 +120,12 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
return list(iter_attachments())
class MailTests(MailTestsMixin, SimpleTestCase):
"""
Non-backend specific tests.
"""
def test_ascii(self):
email = EmailMessage(
"Subject", "Content", "from@example.com", ["to@example.com"]
@ -303,26 +345,16 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
def test_header_injection(self):
msg = "Header values can't contain newlines "
email = EmailMessage(
"Subject\nInjection Test", "Content", "from@example.com", ["to@example.com"]
)
with self.assertRaisesMessage(BadHeaderError, msg):
email.message()
email = EmailMessage(
gettext_lazy("Subject\nInjection Test"),
"Content",
"from@example.com",
["to@example.com"],
)
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()
cases = [
{"subject": "Subject\nInjection Test"},
{"subject": gettext_lazy("Lazy Subject\nInjection Test")},
{"to": ["Name\nInjection test <to@example.com>"]},
]
for kwargs in cases:
with self.subTest(case=kwargs):
email = EmailMessage(**kwargs)
with self.assertRaisesMessage(BadHeaderError, msg):
email.message()
def test_folding_white_space(self):
"""
@ -615,8 +647,8 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
("Content-Transfer-Encoding", "quoted-printable"),
},
)
self.assertTrue(
payload0.as_bytes().endswith(b"\n\nFirstname S=FCrname is a great guy.")
self.assertEndsWith(
payload0.as_bytes(), b"\n\nFirstname S=FCrname is a great guy."
)
# Check the text/html alternative.
payload1 = message.get_payload(1)
@ -630,10 +662,9 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
("Content-Transfer-Encoding", "quoted-printable"),
},
)
self.assertTrue(
payload1.as_bytes().endswith(
b"\n\n<p>Firstname S=FCrname is a <strong>great</strong> guy.</p>"
)
self.assertEndsWith(
payload1.as_bytes(),
b"\n\n<p>Firstname S=FCrname is a <strong>great</strong> guy.</p>",
)
def test_attachments(self):
@ -761,34 +792,38 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
for basename, real_mimetype in files:
for mimetype in test_mimetypes:
self.assertEqual(mimetypes.guess_type(basename)[0], real_mimetype)
expected_mimetype = (
mimetype or real_mimetype or "application/octet-stream"
)
file_path = Path(__file__).parent / "attachments" / basename
expected_content = file_path.read_bytes()
if expected_mimetype.startswith("text/"):
try:
expected_content = expected_content.decode()
except UnicodeDecodeError:
expected_mimetype = "application/octet-stream"
with self.subTest(
basename=basename, real_mimetype=real_mimetype, mimetype=mimetype
):
self.assertEqual(mimetypes.guess_type(basename)[0], real_mimetype)
expected_mimetype = (
mimetype or real_mimetype or "application/octet-stream"
)
file_path = Path(__file__).parent / "attachments" / basename
expected_content = file_path.read_bytes()
if expected_mimetype.startswith("text/"):
try:
expected_content = expected_content.decode()
except UnicodeDecodeError:
expected_mimetype = "application/octet-stream"
email = EmailMessage()
email.attach_file(file_path, mimetype=mimetype)
email = EmailMessage()
email.attach_file(file_path, mimetype=mimetype)
# Check EmailMessage.attachments.
self.assertEqual(len(email.attachments), 1)
self.assertEqual(email.attachments[0].filename, basename)
self.assertEqual(email.attachments[0].mimetype, expected_mimetype)
self.assertEqual(email.attachments[0].content, expected_content)
# Check EmailMessage.attachments.
self.assertEqual(len(email.attachments), 1)
self.assertEqual(email.attachments[0].filename, basename)
self.assertEqual(email.attachments[0].mimetype, expected_mimetype)
self.assertEqual(email.attachments[0].content, expected_content)
# Check attachments in generated message.
# (The actual content is not checked as variations in platform
# line endings and rfc822 refolding complicate the logic.)
actual_attachment = self.get_decoded_attachments(email)[0]
actual_filename, actual_content, actual_mimetype = actual_attachment
self.assertEqual(actual_filename, basename)
self.assertEqual(actual_mimetype, expected_mimetype)
# Check attachments in the generated message.
# (The actual content is not checked as variations in platform
# line endings and rfc822 refolding complicate the logic.)
attachments = self.get_decoded_attachments(email)
self.assertEqual(len(attachments), 1)
actual = attachments[0]
self.assertEqual(actual.filename, basename)
self.assertEqual(actual.mimetype, expected_mimetype)
def test_attach_text_as_bytes(self):
msg = EmailMessage()
@ -1207,7 +1242,7 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
@requires_tz_support
class MailTimeZoneTests(SimpleTestCase):
class MailTimeZoneTests(MailTestsMixin, SimpleTestCase):
@override_settings(
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 = EmailMessage()
self.assertTrue(email.message()["Date"].endswith("-0000"))
self.assertEndsWith(email.message()["Date"], "-0000")
@override_settings(
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 = EmailMessage()
self.assertTrue(
email.message()["Date"].endswith("+0100")
) # Africa/Algiers is UTC+1
# Africa/Algiers is UTC+1 year round.
self.assertEndsWith(email.message()["Date"], "+0100")
class PythonGlobalState(SimpleTestCase):
@ -1259,7 +1293,7 @@ class PythonGlobalState(SimpleTestCase):
self.assertIn("Content-Transfer-Encoding: base64", txt.as_string())
class BaseEmailBackendTests(HeadersCheckMixin):
class BaseEmailBackendTests(MailTestsMixin):
email_backend = None
@classmethod
@ -1267,15 +1301,6 @@ class BaseEmailBackendTests(HeadersCheckMixin):
cls.enterClassContext(override_settings(EMAIL_BACKEND=cls.email_backend))
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):
raise NotImplementedError(
"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.
emails_lists = ([email1, email2], iter((email1, email2)))
for emails_list in emails_lists:
num_sent = mail.get_connection().send_messages(emails_list)
self.assertEqual(num_sent, 2)
messages = self.get_mailbox_content()
self.assertEqual(len(messages), 2)
self.assertEqual(messages[0]["To"], "to-1@example.com")
self.assertEqual(messages[1]["To"], "to-2@example.com")
self.flush_mailbox()
with self.subTest(emails_list=repr(emails_list)):
num_sent = mail.get_connection().send_messages(emails_list)
self.assertEqual(num_sent, 2)
messages = self.get_mailbox_content()
self.assertEqual(len(messages), 2)
self.assertEqual(messages[0]["To"], "to-1@example.com")
self.assertEqual(messages[1]["To"], "to-2@example.com")
self.flush_mailbox()
def test_send_verbose_name(self):
email = EmailMessage(