Skip to content

Commit c60790f

Browse files
committed
Use generic TestBackend for base functionality tests
* Create generic TestBackend that simply collects send parameters * Change BackendSettingsTests to TestBackend, and add some missing cases * Add UnsupportedFeatureTests * Replace repetitive per-backend SEND_DEFAULTS test cases with single (and more comprehensive) SendDefaultsTests
1 parent a0b92be commit c60790f

File tree

6 files changed

+314
-245
lines changed

6 files changed

+314
-245
lines changed

anymail/backends/test.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from anymail.exceptions import AnymailAPIError
2+
from anymail.message import AnymailRecipientStatus
3+
4+
from .base import AnymailBaseBackend, BasePayload
5+
from ..utils import get_anymail_setting
6+
7+
8+
class TestBackend(AnymailBaseBackend):
9+
"""
10+
Anymail backend that doesn't do anything.
11+
12+
Used for testing Anymail common backend functionality.
13+
"""
14+
15+
def __init__(self, *args, **kwargs):
16+
# Init options from Django settings
17+
esp_name = self.esp_name
18+
self.sample_setting = get_anymail_setting('sample_setting', esp_name=esp_name,
19+
kwargs=kwargs, allow_bare=True)
20+
self.recorded_send_params = get_anymail_setting('recorded_send_params', default=[],
21+
esp_name=esp_name, kwargs=kwargs)
22+
super(TestBackend, self).__init__(*args, **kwargs)
23+
24+
def build_message_payload(self, message, defaults):
25+
return TestPayload(backend=self, message=message, defaults=defaults)
26+
27+
def post_to_esp(self, payload, message):
28+
# Keep track of the send params (for test-case access)
29+
self.recorded_send_params.append(payload.params)
30+
try:
31+
# Tests can supply their own message.test_response:
32+
response = message.test_response
33+
if isinstance(response, AnymailAPIError):
34+
raise response
35+
except AttributeError:
36+
# Default is to return 'sent' for each recipient
37+
status = AnymailRecipientStatus(message_id=1, status='sent')
38+
response = {
39+
'recipient_status': {email: status for email in payload.recipient_emails}
40+
}
41+
return response
42+
43+
def parse_recipient_status(self, response, payload, message):
44+
try:
45+
return response['recipient_status']
46+
except KeyError:
47+
raise AnymailAPIError('Unparsable test response')
48+
49+
50+
class TestPayload(BasePayload):
51+
# For test purposes, just keep a dict of the params we've received.
52+
# (This approach is also useful for native API backends -- think of
53+
# payload.params as collecting kwargs for esp_native_api.send().)
54+
55+
def init_payload(self):
56+
self.params = {}
57+
self.recipient_emails = []
58+
59+
def set_from_email(self, email):
60+
self.params['from'] = email
61+
62+
def set_to(self, emails):
63+
self.params['to'] = emails
64+
self.recipient_emails += [email.email for email in emails]
65+
66+
def set_cc(self, emails):
67+
self.params['cc'] = emails
68+
self.recipient_emails += [email.email for email in emails]
69+
70+
def set_bcc(self, emails):
71+
self.params['bcc'] = emails
72+
self.recipient_emails += [email.email for email in emails]
73+
74+
def set_subject(self, subject):
75+
self.params['subject'] = subject
76+
77+
def set_reply_to(self, emails):
78+
self.params['reply_to'] = emails
79+
80+
def set_extra_headers(self, headers):
81+
self.params['extra_headers'] = headers
82+
83+
def set_text_body(self, body):
84+
self.params['text_body'] = body
85+
86+
def set_html_body(self, body):
87+
self.params['html_body'] = body
88+
89+
def add_alternative(self, content, mimetype):
90+
self.unsupported_feature("alternative part with type '%s'" % mimetype)
91+
92+
def add_attachment(self, attachment):
93+
self.params.setdefault('attachments', []).append(attachment)
94+
95+
def set_metadata(self, metadata):
96+
self.params['metadata'] = metadata
97+
98+
def set_send_at(self, send_at):
99+
self.params['send_at'] = send_at
100+
101+
def set_tags(self, tags):
102+
self.params['tags'] = tags
103+
104+
def set_track_clicks(self, track_clicks):
105+
self.params['track_clicks'] = track_clicks
106+
107+
def set_track_opens(self, track_opens):
108+
self.params['track_opens'] = track_opens
109+
110+
def set_template_id(self, template_id):
111+
self.params['template_id'] = template_id
112+
113+
def set_merge_data(self, merge_data):
114+
self.params['merge_data'] = merge_data
115+
116+
def set_merge_global_data(self, merge_global_data):
117+
self.params['merge_global_data'] = merge_global_data
118+
119+
def set_esp_extra(self, extra):
120+
# Merge extra into params
121+
self.params.update(extra)

tests/test_general_backend.py

Lines changed: 193 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,208 @@
1-
from django.core.mail import get_connection
1+
from datetime import datetime
2+
3+
from django.core.exceptions import ImproperlyConfigured
4+
from django.core.mail import get_connection, send_mail
25
from django.test import SimpleTestCase
36
from django.test.utils import override_settings
7+
from django.utils.timezone import utc
8+
9+
from anymail.exceptions import AnymailConfigurationError, AnymailUnsupportedFeature
10+
from anymail.message import AnymailMessage
411

512
from .utils import AnymailTestMixin
613

714

8-
class BackendSettingsTests(SimpleTestCase, AnymailTestMixin):
15+
recorded_send_params = []
16+
17+
18+
@override_settings(EMAIL_BACKEND='anymail.backends.test.TestBackend',
19+
ANYMAIL_TEST_SAMPLE_SETTING='sample', # required TestBackend setting
20+
ANYMAIL_TEST_RECORDED_SEND_PARAMS=recorded_send_params)
21+
class TestBackendTestCase(SimpleTestCase, AnymailTestMixin):
22+
"""Base TestCase using Anymail's TestBackend"""
23+
24+
def setUp(self):
25+
super(TestBackendTestCase, self).setUp()
26+
del recorded_send_params[:] # empty the list from previous tests
27+
# Simple message useful for many tests
28+
self.message = AnymailMessage('Subject', 'Text Body', '[email protected]', ['[email protected]'])
29+
30+
@staticmethod
31+
def get_send_params():
32+
return recorded_send_params[-1]
33+
34+
35+
@override_settings(EMAIL_BACKEND='anymail.backends.test.TestBackend') # but no ANYMAIL settings overrides
36+
class BackendSettingsTests(SimpleTestCase, AnymailTestMixin): # (so not TestBackendTestCase)
937
"""Test settings initializations for Anymail EmailBackends"""
1038

11-
# We should add a "GenericBackend" or something basic for testing.
12-
# For now, we just access real backends directly.
39+
@override_settings(ANYMAIL={'TEST_SAMPLE_SETTING': 'setting_from_anymail_settings'})
40+
def test_anymail_setting(self):
41+
"""ESP settings usually come from ANYMAIL settings dict"""
42+
backend = get_connection()
43+
self.assertEqual(backend.sample_setting, 'setting_from_anymail_settings')
44+
45+
@override_settings(TEST_SAMPLE_SETTING='setting_from_bare_settings')
46+
def test_bare_setting(self):
47+
"""ESP settings are also usually allowed at root of settings file"""
48+
backend = get_connection()
49+
self.assertEqual(backend.sample_setting, 'setting_from_bare_settings')
1350

14-
@override_settings(ANYMAIL={'MAILGUN_API_KEY': 'api_key_from_settings'})
51+
@override_settings(ANYMAIL={'TEST_SAMPLE_SETTING': 'setting_from_settings'})
1552
def test_connection_kwargs_overrides_settings(self):
16-
connection = get_connection('anymail.backends.mailgun.MailgunBackend')
17-
self.assertEqual(connection.api_key, 'api_key_from_settings')
53+
"""Can override settings file in get_connection"""
54+
backend = get_connection()
55+
self.assertEqual(backend.sample_setting, 'setting_from_settings')
1856

19-
connection = get_connection('anymail.backends.mailgun.MailgunBackend',
20-
api_key='api_key_from_kwargs')
21-
self.assertEqual(connection.api_key, 'api_key_from_kwargs')
57+
backend = get_connection(sample_setting='setting_from_kwargs')
58+
self.assertEqual(backend.sample_setting, 'setting_from_kwargs')
59+
60+
def test_missing_setting(self):
61+
"""Settings without defaults must be provided"""
62+
with self.assertRaises(AnymailConfigurationError) as cm:
63+
get_connection()
64+
self.assertIsInstance(cm.exception, ImproperlyConfigured) # Django consistency
65+
errmsg = str(cm.exception)
66+
self.assertRegex(errmsg, r'\bTEST_SAMPLE_SETTING\b')
67+
self.assertRegex(errmsg, r'\bANYMAIL_TEST_SAMPLE_SETTING\b')
2268

2369
@override_settings(ANYMAIL={'SENDGRID_USERNAME': 'username_from_settings',
2470
'SENDGRID_PASSWORD': 'password_from_settings'})
2571
def test_username_password_kwargs_overrides(self):
26-
# Additional checks for username and password, which are special-cased
27-
# because of Django core mail function defaults.
28-
connection = get_connection('anymail.backends.sendgrid.SendGridBackend')
29-
self.assertEqual(connection.username, 'username_from_settings')
30-
self.assertEqual(connection.password, 'password_from_settings')
31-
32-
connection = get_connection('anymail.backends.sendgrid.SendGridBackend',
33-
username='username_from_kwargs', password='password_from_kwargs')
34-
self.assertEqual(connection.username, 'username_from_kwargs')
35-
self.assertEqual(connection.password, 'password_from_kwargs')
72+
"""Overrides for 'username' and 'password' should work like other overrides"""
73+
# These are special-cased because of default args in Django core mail functions.
74+
# (Use the SendGrid backend, which has settings named 'username' and 'password'.)
75+
backend = get_connection('anymail.backends.sendgrid.SendGridBackend')
76+
self.assertEqual(backend.username, 'username_from_settings')
77+
self.assertEqual(backend.password, 'password_from_settings')
78+
79+
backend = get_connection('anymail.backends.sendgrid.SendGridBackend',
80+
username='username_from_kwargs', password='password_from_kwargs')
81+
self.assertEqual(backend.username, 'username_from_kwargs')
82+
self.assertEqual(backend.password, 'password_from_kwargs')
83+
84+
85+
class UnsupportedFeatureTests(TestBackendTestCase):
86+
"""Tests mail features not supported by backend are handled properly"""
87+
88+
def test_unsupported_feature(self):
89+
"""Unsupported features raise AnymailUnsupportedFeature"""
90+
# TestBackend doesn't support non-HTML alternative parts
91+
self.message.attach_alternative(b'FAKE_MP3_DATA', 'audio/mpeg')
92+
with self.assertRaises(AnymailUnsupportedFeature):
93+
self.message.send()
94+
95+
@override_settings(ANYMAIL={
96+
'IGNORE_UNSUPPORTED_FEATURES': True
97+
})
98+
def test_ignore_unsupported_features(self):
99+
"""Setting prevents exception"""
100+
self.message.attach_alternative(b'FAKE_MP3_DATA', 'audio/mpeg')
101+
self.message.send() # should not raise exception
102+
103+
104+
class SendDefaultsTests(TestBackendTestCase):
105+
"""Tests backend support for global SEND_DEFAULTS and <ESP>_SEND_DEFAULTS"""
106+
107+
@override_settings(ANYMAIL={
108+
'SEND_DEFAULTS': {
109+
# This isn't an exhaustive list of Anymail message attrs; just one of each type
110+
'metadata': {'global': 'globalvalue'},
111+
'send_at': datetime(2016, 5, 12, 4, 17, 0, tzinfo=utc),
112+
'tags': ['globaltag'],
113+
'template_id': 'my-template',
114+
'track_clicks': True,
115+
'esp_extra': {'globalextra': 'globalsetting'},
116+
}
117+
})
118+
def test_send_defaults(self):
119+
"""Test that (non-esp-specific) send defaults are applied"""
120+
self.message.send()
121+
params = self.get_send_params()
122+
# All these values came from ANYMAIL_SEND_DEFAULTS:
123+
self.assertEqual(params['metadata'], {'global': 'globalvalue'})
124+
self.assertEqual(params['send_at'], datetime(2016, 5, 12, 4, 17, 0, tzinfo=utc))
125+
self.assertEqual(params['tags'], ['globaltag'])
126+
self.assertEqual(params['template_id'], 'my-template')
127+
self.assertEqual(params['track_clicks'], True)
128+
self.assertEqual(params['globalextra'], 'globalsetting') # TestBackend merges esp_extra into params
129+
130+
@override_settings(ANYMAIL={
131+
'TEST_SEND_DEFAULTS': { # "TEST" is the name of the TestBackend's ESP
132+
'metadata': {'global': 'espvalue'},
133+
'tags': ['esptag'],
134+
'track_opens': False,
135+
'esp_extra': {'globalextra': 'espsetting'},
136+
}
137+
})
138+
def test_esp_send_defaults(self):
139+
"""Test that esp-specific send defaults are applied"""
140+
self.message.send()
141+
params = self.get_send_params()
142+
self.assertEqual(params['metadata'], {'global': 'espvalue'})
143+
self.assertEqual(params['tags'], ['esptag'])
144+
self.assertEqual(params['track_opens'], False)
145+
self.assertEqual(params['globalextra'], 'espsetting') # TestBackend merges esp_extra into params
146+
147+
@override_settings(ANYMAIL={
148+
'SEND_DEFAULTS': {
149+
'metadata': {'global': 'globalvalue', 'other': 'othervalue'},
150+
'tags': ['globaltag'],
151+
'track_clicks': True,
152+
'track_opens': False,
153+
'esp_extra': {'globalextra': 'globalsetting'},
154+
}
155+
})
156+
def test_send_defaults_combine_with_message(self):
157+
"""Individual message settings are *merged into* the global send defaults"""
158+
self.message.metadata = {'message': 'messagevalue', 'other': 'override'}
159+
self.message.tags = ['messagetag']
160+
self.message.track_clicks = False
161+
self.message.esp_extra = {'messageextra': 'messagesetting'}
162+
163+
self.message.send()
164+
params = self.get_send_params()
165+
self.assertEqual(params['metadata'], { # metadata merged
166+
'global': 'globalvalue', # global default preserved
167+
'message': 'messagevalue', # message setting added
168+
'other': 'override'}) # message setting overrides global default
169+
self.assertEqual(params['tags'], ['globaltag', 'messagetag']) # tags concatenated
170+
self.assertEqual(params['track_clicks'], False) # message overrides
171+
self.assertEqual(params['track_opens'], False) # (no message setting)
172+
self.assertEqual(params['globalextra'], 'globalsetting')
173+
self.assertEqual(params['messageextra'], 'messagesetting')
174+
175+
# Send another message to make sure original SEND_DEFAULTS unchanged
176+
send_mail('subject', 'body', '[email protected]', ['[email protected]'])
177+
params = self.get_send_params()
178+
self.assertEqual(params['metadata'], {'global': 'globalvalue', 'other': 'othervalue'})
179+
self.assertEqual(params['tags'], ['globaltag'])
180+
self.assertEqual(params['track_clicks'], True)
181+
self.assertEqual(params['track_opens'], False)
182+
self.assertEqual(params['globalextra'], 'globalsetting')
183+
184+
@override_settings(ANYMAIL={
185+
'SEND_DEFAULTS': {
186+
# This isn't an exhaustive list of Anymail message attrs; just one of each type
187+
'metadata': {'global': 'globalvalue'},
188+
'tags': ['globaltag'],
189+
'template_id': 'global-template',
190+
'esp_extra': {'globalextra': 'globalsetting'},
191+
},
192+
'TEST_SEND_DEFAULTS': { # "TEST" is the name of the TestBackend's ESP
193+
'merge_global_data': {'esp': 'espmerge'},
194+
'metadata': {'esp': 'espvalue'},
195+
'tags': ['esptag'],
196+
'esp_extra': {'espextra': 'espsetting'},
197+
}
198+
})
199+
def test_esp_send_defaults_override_globals(self):
200+
"""ESP-specific send defaults override *individual* global defaults"""
201+
self.message.send()
202+
params = self.get_send_params()
203+
self.assertEqual(params['merge_global_data'], {'esp': 'espmerge'}) # esp-defaults only
204+
self.assertEqual(params['metadata'], {'esp': 'espvalue'})
205+
self.assertEqual(params['tags'], ['esptag'])
206+
self.assertEqual(params['template_id'], 'global-template') # global-defaults only
207+
self.assertEqual(params['espextra'], 'espsetting')
208+
self.assertNotIn('globalextra', params) # entire esp_extra is overriden by esp-send-defaults

0 commit comments

Comments
 (0)