Skip to content

Commit 9936738

Browse files
committed
Mailtrap: add backend unit tests, set_track_clicks, and set_track_opens
1 parent f0181c7 commit 9936738

File tree

2 files changed

+327
-19
lines changed

2 files changed

+327
-19
lines changed

anymail/backends/mailtrap.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ def set_tags(self, tags: List[str]):
160160
if len(tags) > 0:
161161
self.data["category"] = tags[0]
162162

163+
def set_track_clicks(self, *args, **kwargs):
164+
"""Do nothing. Mailtrap supports this, but it is not configured in the send request."""
165+
pass
166+
167+
def set_track_opens(self, *args, **kwargs):
168+
"""Do nothing. Mailtrap supports this, but it is not configured in the send request."""
169+
pass
170+
163171
def set_metadata(self, metadata):
164172
self.data.setdefault("custom_variables", {}).update(
165173
{str(k): str(v) for k, v in metadata.items()}
@@ -235,29 +243,30 @@ def parse_recipient_status(
235243
):
236244
parsed_response = self.deserialize_json_response(response, payload, message)
237245

238-
if (
246+
# TODO: how to handle fail_silently?
247+
if not self.fail_silently and (
239248
not parsed_response.get("success")
240249
or ("errors" in parsed_response and parsed_response["errors"])
241250
or ("message_ids" not in parsed_response)
242251
):
243252
raise AnymailRequestsAPIError(
244253
email_message=message, payload=payload, response=response, backend=self
245254
)
246-
247-
# message-ids will be in this order
248-
recipient_status_order = [
249-
*payload.recipients_to,
250-
*payload.recipients_cc,
251-
*payload.recipients_bcc,
252-
]
253-
recipient_status = {
254-
email: AnymailRecipientStatus(
255-
message_id=message_id,
256-
status="sent",
257-
)
258-
for email, message_id in zip(
259-
recipient_status_order, parsed_response["message_ids"]
260-
)
261-
}
262-
263-
return recipient_status
255+
else:
256+
# message-ids will be in this order
257+
recipient_status_order = [
258+
*payload.recipients_to,
259+
*payload.recipients_cc,
260+
*payload.recipients_bcc,
261+
]
262+
recipient_status = {
263+
email: AnymailRecipientStatus(
264+
message_id=message_id,
265+
status="sent",
266+
)
267+
for email, message_id in zip(
268+
recipient_status_order, parsed_response["message_ids"]
269+
)
270+
}
271+
272+
return recipient_status

tests/test_mailtrap_backend.py

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
# FILE: tests/test_mailtrap_backend.py
2+
3+
import unittest
4+
from datetime import datetime
5+
from decimal import Decimal
6+
7+
from django.core import mail
8+
from django.core.exceptions import ImproperlyConfigured
9+
from django.test import SimpleTestCase, override_settings, tag
10+
from django.utils.timezone import timezone
11+
12+
from anymail.exceptions import (
13+
AnymailAPIError,
14+
AnymailRecipientsRefused,
15+
AnymailSerializationError,
16+
AnymailUnsupportedFeature,
17+
)
18+
from anymail.message import attach_inline_image
19+
20+
from .mock_requests_backend import (
21+
RequestsBackendMockAPITestCase,
22+
SessionSharingTestCases,
23+
)
24+
from .utils import AnymailTestMixin, sample_image_content
25+
26+
27+
@tag("mailtrap")
28+
@override_settings(
29+
EMAIL_BACKEND="anymail.backends.mailtrap.EmailBackend",
30+
ANYMAIL={"MAILTRAP_API_TOKEN": "test_api_token"},
31+
)
32+
class MailtrapBackendMockAPITestCase(RequestsBackendMockAPITestCase):
33+
DEFAULT_RAW_RESPONSE = b"""{
34+
"success": true,
35+
"message_ids": ["1df37d17-0286-4d8b-8edf-bc4ec5be86e6"]
36+
}"""
37+
38+
def setUp(self):
39+
super().setUp()
40+
self.message = mail.EmailMultiAlternatives(
41+
"Subject", "Body", "from@example.com", ["to@example.com"]
42+
)
43+
44+
def test_send_email(self):
45+
"""Test sending a basic email"""
46+
response = self.message.send()
47+
self.assertEqual(response, 1)
48+
self.assert_esp_called("https://send.api.mailtrap.io/api/send")
49+
50+
def test_send_with_attachments(self):
51+
"""Test sending an email with attachments"""
52+
self.message.attach("test.txt", "This is a test", "text/plain")
53+
response = self.message.send()
54+
self.assertEqual(response, 1)
55+
self.assert_esp_called("https://send.api.mailtrap.io/api/send")
56+
57+
def test_send_with_inline_image(self):
58+
"""Test sending an email with inline images"""
59+
image_data = sample_image_content() # Read from a png file
60+
61+
cid = attach_inline_image(self.message, image_data)
62+
html_content = (
63+
'<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
64+
)
65+
self.message.attach_alternative(html_content, "text/html")
66+
67+
response = self.message.send()
68+
self.assertEqual(response, 1)
69+
self.assert_esp_called("https://send.api.mailtrap.io/api/send")
70+
71+
def test_send_with_metadata(self):
72+
"""Test sending an email with metadata"""
73+
self.message.metadata = {"user_id": "12345"}
74+
response = self.message.send()
75+
self.assertEqual(response, 1)
76+
self.assert_esp_called("https://send.api.mailtrap.io/api/send")
77+
78+
def test_send_with_tag(self):
79+
"""Test sending an email with one tag"""
80+
self.message.tags = ["tag1"]
81+
response = self.message.send()
82+
self.assertEqual(response, 1)
83+
self.assert_esp_called("https://send.api.mailtrap.io/api/send")
84+
85+
def test_send_with_tags(self):
86+
"""Test sending an email with tags"""
87+
self.message.tags = ["tag1", "tag2"]
88+
with self.assertRaises(AnymailUnsupportedFeature):
89+
self.message.send()
90+
91+
def test_send_with_template(self):
92+
"""Test sending an email with a template"""
93+
self.message.template_id = "template_id"
94+
response = self.message.send()
95+
self.assertEqual(response, 1)
96+
self.assert_esp_called("https://send.api.mailtrap.io/api/send")
97+
98+
def test_send_with_merge_data(self):
99+
"""Test sending an email with merge data"""
100+
self.message.merge_data = {"to@example.com": {"name": "Recipient"}}
101+
with self.assertRaises(AnymailUnsupportedFeature):
102+
self.message.send()
103+
104+
def test_send_with_invalid_api_token(self):
105+
"""Test sending an email with an invalid API token"""
106+
self.set_mock_response(status_code=401, raw=b'{"error": "Invalid API token"}')
107+
with self.assertRaises(AnymailAPIError):
108+
self.message.send()
109+
110+
@unittest.skip("TODO: is this test correct/necessary?")
111+
def test_send_with_recipients_refused(self):
112+
"""Test sending an email with all recipients refused"""
113+
self.set_mock_response(
114+
status_code=400, raw=b'{"error": "All recipients refused"}'
115+
)
116+
with self.assertRaises(AnymailRecipientsRefused):
117+
self.message.send()
118+
119+
def test_send_with_serialization_error(self):
120+
"""Test sending an email with a serialization error"""
121+
self.message.extra_headers = {
122+
"foo": Decimal("1.23")
123+
} # Decimal can't be serialized
124+
with self.assertRaises(AnymailSerializationError) as cm:
125+
self.message.send()
126+
err = cm.exception
127+
self.assertIsInstance(err, TypeError)
128+
self.assertRegex(str(err), r"Decimal.*is not JSON serializable")
129+
130+
def test_send_with_api_error(self):
131+
"""Test sending an email with a generic API error"""
132+
self.set_mock_response(
133+
status_code=500, raw=b'{"error": "Internal server error"}'
134+
)
135+
with self.assertRaises(AnymailAPIError):
136+
self.message.send()
137+
138+
def test_send_with_headers_and_recipients(self):
139+
"""Test sending an email with headers and multiple recipients"""
140+
email = mail.EmailMessage(
141+
"Subject",
142+
"Body goes here",
143+
"from@example.com",
144+
["to1@example.com", "Also To <to2@example.com>"],
145+
bcc=["bcc1@example.com", "Also BCC <bcc2@example.com>"],
146+
cc=["cc1@example.com", "Also CC <cc2@example.com>"],
147+
headers={
148+
"Reply-To": "another@example.com",
149+
"X-MyHeader": "my value",
150+
"Message-ID": "mycustommsgid@example.com",
151+
},
152+
)
153+
email.send()
154+
data = self.get_api_call_json()
155+
self.assertEqual(data["subject"], "Subject")
156+
self.assertEqual(data["text"], "Body goes here")
157+
self.assertEqual(data["from"]["email"], "from@example.com")
158+
self.assertEqual(
159+
data["headers"],
160+
{
161+
"Reply-To": "another@example.com",
162+
"X-MyHeader": "my value",
163+
"Message-ID": "mycustommsgid@example.com",
164+
},
165+
)
166+
# Verify recipients correctly identified as "to", "cc", or "bcc"
167+
self.assertEqual(
168+
data["to"],
169+
[
170+
{"email": "to1@example.com"},
171+
{"email": "to2@example.com", "name": "Also To"},
172+
],
173+
)
174+
self.assertEqual(
175+
data["cc"],
176+
[
177+
{"email": "cc1@example.com"},
178+
{"email": "cc2@example.com", "name": "Also CC"},
179+
],
180+
)
181+
self.assertEqual(
182+
data["bcc"],
183+
[
184+
{"email": "bcc1@example.com"},
185+
{"email": "bcc2@example.com", "name": "Also BCC"},
186+
],
187+
)
188+
189+
190+
@tag("mailtrap")
191+
class MailtrapBackendAnymailFeatureTests(MailtrapBackendMockAPITestCase):
192+
"""Test backend support for Anymail added features"""
193+
194+
def test_envelope_sender(self):
195+
self.message.envelope_sender = "envelope@example.com"
196+
with self.assertRaises(AnymailUnsupportedFeature):
197+
self.message.send()
198+
199+
def test_metadata(self):
200+
self.message.metadata = {"user_id": "12345"}
201+
response = self.message.send()
202+
self.assertEqual(response, 1)
203+
data = self.get_api_call_json()
204+
self.assertEqual(data["custom_variables"], {"user_id": "12345"})
205+
206+
def test_send_at(self):
207+
send_at = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc)
208+
self.message.send_at = send_at
209+
with self.assertRaises(AnymailUnsupportedFeature):
210+
self.message.send()
211+
212+
def test_tags(self):
213+
self.message.tags = ["tag1"]
214+
response = self.message.send()
215+
self.assertEqual(response, 1)
216+
data = self.get_api_call_json()
217+
self.assertEqual(data["category"], "tag1")
218+
219+
def test_tracking(self):
220+
self.message.track_clicks = True
221+
self.message.track_opens = True
222+
response = self.message.send()
223+
self.assertEqual(response, 1)
224+
225+
def test_template_id(self):
226+
self.message.template_id = "template_id"
227+
response = self.message.send()
228+
self.assertEqual(response, 1)
229+
data = self.get_api_call_json()
230+
self.assertEqual(data["template_uuid"], "template_id")
231+
232+
def test_merge_data(self):
233+
self.message.merge_data = {"to@example.com": {"name": "Recipient"}}
234+
with self.assertRaises(AnymailUnsupportedFeature):
235+
self.message.send()
236+
237+
def test_merge_global_data(self):
238+
self.message.merge_global_data = {"global_name": "Global Recipient"}
239+
response = self.message.send()
240+
self.assertEqual(response, 1)
241+
data = self.get_api_call_json()
242+
self.assertEqual(
243+
data["template_variables"], {"global_name": "Global Recipient"}
244+
)
245+
246+
def test_esp_extra(self):
247+
self.message.esp_extra = {"custom_option": "value"}
248+
response = self.message.send()
249+
self.assertEqual(response, 1)
250+
data = self.get_api_call_json()
251+
self.assertEqual(data["custom_option"], "value")
252+
253+
254+
@tag("mailtrap")
255+
class MailtrapBackendRecipientsRefusedTests(MailtrapBackendMockAPITestCase):
256+
"""
257+
Should raise AnymailRecipientsRefused when *all* recipients are rejected or invalid
258+
"""
259+
260+
@unittest.skip("TODO: is this test correct/necessary?")
261+
def test_recipients_refused(self):
262+
self.set_mock_response(
263+
status_code=400, raw=b'{"error": "All recipients refused"}'
264+
)
265+
with self.assertRaises(AnymailRecipientsRefused):
266+
self.message.send()
267+
268+
@unittest.skip(
269+
"TODO: is this test correct/necessary? How to handle this in mailtrap backend?"
270+
)
271+
def test_fail_silently(self):
272+
self.set_mock_response(
273+
status_code=400, raw=b'{"error": "All recipients refused"}'
274+
)
275+
self.message.fail_silently = True
276+
sent = self.message.send()
277+
self.assertEqual(sent, 0)
278+
279+
280+
@tag("mailtrap")
281+
class MailtrapBackendSessionSharingTestCase(
282+
SessionSharingTestCases, MailtrapBackendMockAPITestCase
283+
):
284+
"""Requests session sharing tests"""
285+
286+
pass # tests are defined in SessionSharingTestCases
287+
288+
289+
@tag("mailtrap")
290+
@override_settings(EMAIL_BACKEND="anymail.backends.mailtrap.EmailBackend")
291+
class MailtrapBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
292+
"""Test ESP backend without required settings in place"""
293+
294+
def test_missing_api_token(self):
295+
with self.assertRaises(ImproperlyConfigured) as cm:
296+
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
297+
errmsg = str(cm.exception)
298+
self.assertRegex(errmsg, r"\bMAILTRAP_API_TOKEN\b")
299+
self.assertRegex(errmsg, r"\bANYMAIL_MAILTRAP_API_TOKEN\b")

0 commit comments

Comments
 (0)