Skip to content

Commit

Permalink
Mailtrap: add backend unit tests, set_track_clicks, and set_track_opens
Browse files Browse the repository at this point in the history
  • Loading branch information
cahna committed Nov 30, 2024
1 parent c5f9140 commit f80dc55
Show file tree
Hide file tree
Showing 2 changed files with 327 additions and 19 deletions.
47 changes: 28 additions & 19 deletions anymail/backends/mailtrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ def set_tags(self, tags: List[str]):
if len(tags) > 0:
self.data["category"] = tags[0]

def set_track_clicks(self, *args, **kwargs):
"""Do nothing. Mailtrap supports this, but it is not configured in the send request."""
pass

def set_track_opens(self, *args, **kwargs):
"""Do nothing. Mailtrap supports this, but it is not configured in the send request."""
pass

def set_metadata(self, metadata):
self.data.setdefault("custom_variables", {}).update(
{str(k): str(v) for k, v in metadata.items()}
Expand Down Expand Up @@ -235,29 +243,30 @@ def parse_recipient_status(
):
parsed_response = self.deserialize_json_response(response, payload, message)

if (
# TODO: how to handle fail_silently?
if not self.fail_silently and (
not parsed_response.get("success")
or ("errors" in parsed_response and parsed_response["errors"])
or ("message_ids" not in parsed_response)
):
raise AnymailRequestsAPIError(
email_message=message, payload=payload, response=response, backend=self
)

# message-ids will be in this order
recipient_status_order = [
*payload.recipients_to,
*payload.recipients_cc,
*payload.recipients_bcc,
]
recipient_status = {
email: AnymailRecipientStatus(
message_id=message_id,
status="sent",
)
for email, message_id in zip(
recipient_status_order, parsed_response["message_ids"]
)
}

return recipient_status
else:
# message-ids will be in this order
recipient_status_order = [
*payload.recipients_to,
*payload.recipients_cc,
*payload.recipients_bcc,
]
recipient_status = {
email: AnymailRecipientStatus(
message_id=message_id,
status="sent",
)
for email, message_id in zip(
recipient_status_order, parsed_response["message_ids"]
)
}

return recipient_status
299 changes: 299 additions & 0 deletions tests/test_mailtrap_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
# FILE: tests/test_mailtrap_backend.py

import unittest
from datetime import datetime
from decimal import Decimal

from django.core import mail
from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase, override_settings, tag
from django.utils.timezone import timezone

from anymail.exceptions import (
AnymailAPIError,
AnymailRecipientsRefused,
AnymailSerializationError,
AnymailUnsupportedFeature,
)
from anymail.message import attach_inline_image

from .mock_requests_backend import (
RequestsBackendMockAPITestCase,
SessionSharingTestCases,
)
from .utils import AnymailTestMixin, sample_image_content


@tag("mailtrap")
@override_settings(
EMAIL_BACKEND="anymail.backends.mailtrap.EmailBackend",
ANYMAIL={"MAILTRAP_API_TOKEN": "test_api_token"},
)
class MailtrapBackendMockAPITestCase(RequestsBackendMockAPITestCase):
DEFAULT_RAW_RESPONSE = b"""{
"success": true,
"message_ids": ["1df37d17-0286-4d8b-8edf-bc4ec5be86e6"]
}"""

def setUp(self):
super().setUp()
self.message = mail.EmailMultiAlternatives(
"Subject", "Body", "[email protected]", ["[email protected]"]
)

def test_send_email(self):
"""Test sending a basic email"""
response = self.message.send()
self.assertEqual(response, 1)
self.assert_esp_called("https://send.api.mailtrap.io/api/send")

def test_send_with_attachments(self):
"""Test sending an email with attachments"""
self.message.attach("test.txt", "This is a test", "text/plain")
response = self.message.send()
self.assertEqual(response, 1)
self.assert_esp_called("https://send.api.mailtrap.io/api/send")

def test_send_with_inline_image(self):
"""Test sending an email with inline images"""
image_data = sample_image_content() # Read from a png file

cid = attach_inline_image(self.message, image_data)
html_content = (
'<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
)
self.message.attach_alternative(html_content, "text/html")

response = self.message.send()
self.assertEqual(response, 1)
self.assert_esp_called("https://send.api.mailtrap.io/api/send")

def test_send_with_metadata(self):
"""Test sending an email with metadata"""
self.message.metadata = {"user_id": "12345"}
response = self.message.send()
self.assertEqual(response, 1)
self.assert_esp_called("https://send.api.mailtrap.io/api/send")

def test_send_with_tag(self):
"""Test sending an email with one tag"""
self.message.tags = ["tag1"]
response = self.message.send()
self.assertEqual(response, 1)
self.assert_esp_called("https://send.api.mailtrap.io/api/send")

def test_send_with_tags(self):
"""Test sending an email with tags"""
self.message.tags = ["tag1", "tag2"]
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()

def test_send_with_template(self):
"""Test sending an email with a template"""
self.message.template_id = "template_id"
response = self.message.send()
self.assertEqual(response, 1)
self.assert_esp_called("https://send.api.mailtrap.io/api/send")

def test_send_with_merge_data(self):
"""Test sending an email with merge data"""
self.message.merge_data = {"[email protected]": {"name": "Recipient"}}
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()

def test_send_with_invalid_api_token(self):
"""Test sending an email with an invalid API token"""
self.set_mock_response(status_code=401, raw=b'{"error": "Invalid API token"}')
with self.assertRaises(AnymailAPIError):
self.message.send()

@unittest.skip("TODO: is this test correct/necessary?")
def test_send_with_recipients_refused(self):
"""Test sending an email with all recipients refused"""
self.set_mock_response(
status_code=400, raw=b'{"error": "All recipients refused"}'
)
with self.assertRaises(AnymailRecipientsRefused):
self.message.send()

def test_send_with_serialization_error(self):
"""Test sending an email with a serialization error"""
self.message.extra_headers = {
"foo": Decimal("1.23")
} # Decimal can't be serialized
with self.assertRaises(AnymailSerializationError) as cm:
self.message.send()
err = cm.exception
self.assertIsInstance(err, TypeError)
self.assertRegex(str(err), r"Decimal.*is not JSON serializable")

def test_send_with_api_error(self):
"""Test sending an email with a generic API error"""
self.set_mock_response(
status_code=500, raw=b'{"error": "Internal server error"}'
)
with self.assertRaises(AnymailAPIError):
self.message.send()

def test_send_with_headers_and_recipients(self):
"""Test sending an email with headers and multiple recipients"""
email = mail.EmailMessage(
"Subject",
"Body goes here",
"[email protected]",
["[email protected]", "Also To <[email protected]>"],
bcc=["[email protected]", "Also BCC <[email protected]>"],
cc=["[email protected]", "Also CC <[email protected]>"],
headers={
"Reply-To": "[email protected]",
"X-MyHeader": "my value",
"Message-ID": "[email protected]",
},
)
email.send()
data = self.get_api_call_json()
self.assertEqual(data["subject"], "Subject")
self.assertEqual(data["text"], "Body goes here")
self.assertEqual(data["from"]["email"], "[email protected]")
self.assertEqual(
data["headers"],
{
"Reply-To": "[email protected]",
"X-MyHeader": "my value",
"Message-ID": "[email protected]",
},
)
# Verify recipients correctly identified as "to", "cc", or "bcc"
self.assertEqual(
data["to"],
[
{"email": "[email protected]"},
{"email": "[email protected]", "name": "Also To"},
],
)
self.assertEqual(
data["cc"],
[
{"email": "[email protected]"},
{"email": "[email protected]", "name": "Also CC"},
],
)
self.assertEqual(
data["bcc"],
[
{"email": "[email protected]"},
{"email": "[email protected]", "name": "Also BCC"},
],
)


@tag("mailtrap")
class MailtrapBackendAnymailFeatureTests(MailtrapBackendMockAPITestCase):
"""Test backend support for Anymail added features"""

def test_envelope_sender(self):
self.message.envelope_sender = "[email protected]"
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()

def test_metadata(self):
self.message.metadata = {"user_id": "12345"}
response = self.message.send()
self.assertEqual(response, 1)
data = self.get_api_call_json()
self.assertEqual(data["custom_variables"], {"user_id": "12345"})

def test_send_at(self):
send_at = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc)
self.message.send_at = send_at
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()

def test_tags(self):
self.message.tags = ["tag1"]
response = self.message.send()
self.assertEqual(response, 1)
data = self.get_api_call_json()
self.assertEqual(data["category"], "tag1")

def test_tracking(self):
self.message.track_clicks = True
self.message.track_opens = True
response = self.message.send()
self.assertEqual(response, 1)

def test_template_id(self):
self.message.template_id = "template_id"
response = self.message.send()
self.assertEqual(response, 1)
data = self.get_api_call_json()
self.assertEqual(data["template_uuid"], "template_id")

def test_merge_data(self):
self.message.merge_data = {"[email protected]": {"name": "Recipient"}}
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()

def test_merge_global_data(self):
self.message.merge_global_data = {"global_name": "Global Recipient"}
response = self.message.send()
self.assertEqual(response, 1)
data = self.get_api_call_json()
self.assertEqual(
data["template_variables"], {"global_name": "Global Recipient"}
)

def test_esp_extra(self):
self.message.esp_extra = {"custom_option": "value"}
response = self.message.send()
self.assertEqual(response, 1)
data = self.get_api_call_json()
self.assertEqual(data["custom_option"], "value")


@tag("mailtrap")
class MailtrapBackendRecipientsRefusedTests(MailtrapBackendMockAPITestCase):
"""
Should raise AnymailRecipientsRefused when *all* recipients are rejected or invalid
"""

@unittest.skip("TODO: is this test correct/necessary?")
def test_recipients_refused(self):
self.set_mock_response(
status_code=400, raw=b'{"error": "All recipients refused"}'
)
with self.assertRaises(AnymailRecipientsRefused):
self.message.send()

@unittest.skip(
"TODO: is this test correct/necessary? How to handle this in mailtrap backend?"
)
def test_fail_silently(self):
self.set_mock_response(
status_code=400, raw=b'{"error": "All recipients refused"}'
)
self.message.fail_silently = True
sent = self.message.send()
self.assertEqual(sent, 0)


@tag("mailtrap")
class MailtrapBackendSessionSharingTestCase(
SessionSharingTestCases, MailtrapBackendMockAPITestCase
):
"""Requests session sharing tests"""

pass # tests are defined in SessionSharingTestCases


@tag("mailtrap")
@override_settings(EMAIL_BACKEND="anymail.backends.mailtrap.EmailBackend")
class MailtrapBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""

def test_missing_api_token(self):
with self.assertRaises(ImproperlyConfigured) as cm:
mail.send_mail("Subject", "Message", "[email protected]", ["[email protected]"])
errmsg = str(cm.exception)
self.assertRegex(errmsg, r"\bMAILTRAP_API_TOKEN\b")
self.assertRegex(errmsg, r"\bANYMAIL_MAILTRAP_API_TOKEN\b")

0 comments on commit f80dc55

Please sign in to comment.