mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
@@ -11,8 +11,8 @@ from .test_postmark_backend import *
|
||||
from .test_postmark_integration import *
|
||||
|
||||
from .test_sendgrid_backend import *
|
||||
from .test_sendgrid_webhooks import *
|
||||
from .test_sendgrid_integration import *
|
||||
|
||||
# Djrill leftovers:
|
||||
from .test_mandrill_djrill_features import *
|
||||
from .test_mandrill_webhook import *
|
||||
|
||||
250
tests/test_mailgun_webhooks.py
Normal file
250
tests/test_mailgun_webhooks.py
Normal file
@@ -0,0 +1,250 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import override_settings
|
||||
from django.utils.timezone import utc
|
||||
from mock import ANY
|
||||
|
||||
from anymail.signals import AnymailTrackingEvent
|
||||
from anymail.webhooks.mailgun import MailgunTrackingWebhookView
|
||||
|
||||
from .webhook_cases import WebhookTestCase, WebhookBasicAuthTestsMixin
|
||||
|
||||
TEST_API_KEY = 'TEST_API_KEY'
|
||||
|
||||
|
||||
def mailgun_sign(data, api_key=TEST_API_KEY):
|
||||
"""Add a Mailgun webhook signature to data dict"""
|
||||
# Modifies the dict in place
|
||||
data.setdefault('timestamp', '1234567890')
|
||||
data.setdefault('token', '1234567890abcdef1234567890abcdef')
|
||||
data['signature'] = hmac.new(key=api_key.encode('ascii'),
|
||||
msg='{timestamp}{token}'.format(**data).encode('ascii'),
|
||||
digestmod=hashlib.sha256).hexdigest()
|
||||
return data
|
||||
|
||||
|
||||
def querydict_to_postdict(qd):
|
||||
"""Converts a Django QueryDict to a TestClient.post(data)-style dict
|
||||
|
||||
Single-value fields appear as normal
|
||||
Multi-value fields appear as a list (differs from QueryDict.dict)
|
||||
"""
|
||||
return {
|
||||
key: values if len(values) > 1 else values[0]
|
||||
for key, values in qd.lists()
|
||||
}
|
||||
|
||||
|
||||
class MailgunWebhookSettingsTestCase(WebhookTestCase):
|
||||
def test_requires_api_key(self):
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
self.client.post(webhook, data=mailgun_sign({'event': 'delivered'}))
|
||||
|
||||
|
||||
@override_settings(ANYMAIL_MAILGUN_API_KEY=TEST_API_KEY)
|
||||
class MailgunWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
|
||||
should_warn_if_no_auth = False # because we check webhook signature
|
||||
|
||||
def call_webhook(self):
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
return self.client.post(webhook, data=mailgun_sign({'event': 'delivered'}))
|
||||
|
||||
# Additional tests are in WebhookBasicAuthTestsMixin
|
||||
|
||||
def test_verifies_correct_signature(self):
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
response = self.client.post(webhook, data=mailgun_sign({'event': 'delivered'}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_verifies_missing_signature(self):
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
response = self.client.post(webhook, data={'event': 'delivered'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_verifies_bad_signature(self):
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
data = mailgun_sign({'event': 'delivered'}, api_key="wrong API key")
|
||||
response = self.client.post(webhook, data=data)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
||||
@override_settings(ANYMAIL_MAILGUN_API_KEY=TEST_API_KEY)
|
||||
class MailgunDeliveryTestCase(WebhookTestCase):
|
||||
|
||||
def test_delivered_event(self):
|
||||
raw_event = mailgun_sign({
|
||||
'domain': 'example.com',
|
||||
'message-headers': json.dumps([
|
||||
["Sender", "from=example.com"],
|
||||
["Date", "Thu, 21 Apr 2016 17:55:29 +0000"],
|
||||
["X-Mailgun-Sid", "WyIxZmY4ZSIsICJtZWRtdW5kc0BnbWFpbC5jb20iLCAiZjFjNzgyIl0="],
|
||||
["Received", "by luna.mailgun.net with HTTP; Thu, 21 Apr 2016 17:55:29 +0000"],
|
||||
["Message-Id", "<20160421175529.19495.89030.B3AE3728@example.com>"],
|
||||
["To", "recipient@example.com"],
|
||||
["From", "from@example.com"],
|
||||
["Subject", "Webhook testing"],
|
||||
["Mime-Version", "1.0"],
|
||||
["Content-Type", ["multipart/alternative", {"boundary": "74fb561763da440d8e6a034054974251"}]]
|
||||
]),
|
||||
'X-Mailgun-Sid': 'WyIxZmY4ZSIsICJtZWRtdW5kc0BnbWFpbC5jb20iLCAiZjFjNzgyIl0=',
|
||||
'token': '06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0',
|
||||
'timestamp': '1461261330',
|
||||
'Message-Id': '<20160421175529.19495.89030.B3AE3728@example.com>',
|
||||
'recipient': 'recipient@example.com',
|
||||
'event': 'delivered',
|
||||
})
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
response = self.client.post(webhook, data=raw_event)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailgunTrackingWebhookView,
|
||||
event=ANY, esp_name='Mailgun')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "delivered")
|
||||
self.assertEqual(event.timestamp, datetime(2016, 4, 21, 17, 55, 30, tzinfo=utc))
|
||||
self.assertEqual(event.message_id, "<20160421175529.19495.89030.B3AE3728@example.com>")
|
||||
self.assertEqual(event.event_id, "06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0")
|
||||
self.assertEqual(event.recipient, "recipient@example.com")
|
||||
self.assertEqual(querydict_to_postdict(event.esp_event), raw_event)
|
||||
|
||||
def test_dropped_bounce(self):
|
||||
raw_event = mailgun_sign({
|
||||
'code': '605',
|
||||
'domain': 'example.com',
|
||||
'description': 'Not delivering to previously bounced address',
|
||||
'attachment-count': '1',
|
||||
'Message-Id': '<20160421180324.70521.79375.96884DDB@example.com>',
|
||||
'reason': 'hardfail',
|
||||
'event': 'dropped',
|
||||
'message-headers': json.dumps([
|
||||
["X-Mailgun-Sid", "WyI3Y2VjMyIsICJib3VuY2VAZXhhbXBsZS5jb20iLCAiZjFjNzgyIl0="],
|
||||
["Received", "by luna.mailgun.net with HTTP; Thu, 21 Apr 2016 18:03:24 +0000"],
|
||||
["Message-Id", "<20160421180324.70521.79375.96884DDB@example.com>"],
|
||||
["To", "bounce@example.com"],
|
||||
["From", "from@example.com"],
|
||||
["Subject", "Webhook testing"],
|
||||
["Mime-Version", "1.0"],
|
||||
["Content-Type", ["multipart/alternative", {"boundary": "a5b51388a4e3455d8feb8510bb8c9fa2"}]]
|
||||
]),
|
||||
'recipient': 'bounce@example.com',
|
||||
'timestamp': '1461261330',
|
||||
'X-Mailgun-Sid': 'WyI3Y2VjMyIsICJib3VuY2VAZXhhbXBsZS5jb20iLCAiZjFjNzgyIl0=',
|
||||
'token': 'a3fe1fa1640349ac552b84ddde373014b4c41645830c8dd3fc',
|
||||
})
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
response = self.client.post(webhook, data=raw_event)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailgunTrackingWebhookView,
|
||||
event=ANY, esp_name='Mailgun')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "rejected")
|
||||
self.assertEqual(event.timestamp, datetime(2016, 4, 21, 17, 55, 30, tzinfo=utc))
|
||||
self.assertEqual(event.message_id, "<20160421180324.70521.79375.96884DDB@example.com>")
|
||||
self.assertEqual(event.event_id, "a3fe1fa1640349ac552b84ddde373014b4c41645830c8dd3fc")
|
||||
self.assertEqual(event.recipient, "bounce@example.com")
|
||||
self.assertEqual(event.reject_reason, "bounced")
|
||||
self.assertEqual(event.description, 'Not delivering to previously bounced address')
|
||||
self.assertEqual(querydict_to_postdict(event.esp_event), raw_event)
|
||||
|
||||
def test_dropped_spam(self):
|
||||
raw_event = mailgun_sign({
|
||||
'code': '607',
|
||||
'description': 'Not delivering to a user who marked your messages as spam',
|
||||
'reason': 'hardfail',
|
||||
'event': 'dropped',
|
||||
'recipient': 'complaint@example.com',
|
||||
# (omitting some fields that aren't relevant to the test)
|
||||
})
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
response = self.client.post(webhook, data=raw_event)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailgunTrackingWebhookView,
|
||||
event=ANY, esp_name='Mailgun')
|
||||
event = kwargs['event']
|
||||
self.assertEqual(event.event_type, "rejected")
|
||||
self.assertEqual(event.reject_reason, "spam")
|
||||
self.assertEqual(event.description, 'Not delivering to a user who marked your messages as spam')
|
||||
|
||||
def test_dropped_timed_out(self):
|
||||
raw_event = mailgun_sign({
|
||||
'code': '499',
|
||||
'description': 'Unable to connect to MX servers: [example.com]',
|
||||
'reason': 'old',
|
||||
'event': 'dropped',
|
||||
'recipient': 'complaint@example.com',
|
||||
# (omitting some fields that aren't relevant to the test)
|
||||
})
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
response = self.client.post(webhook, data=raw_event)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailgunTrackingWebhookView,
|
||||
event=ANY, esp_name='Mailgun')
|
||||
event = kwargs['event']
|
||||
self.assertEqual(event.event_type, "rejected")
|
||||
self.assertEqual(event.reject_reason, "timed_out")
|
||||
self.assertEqual(event.description, 'Unable to connect to MX servers: [example.com]')
|
||||
|
||||
def test_invalid_mailbox(self):
|
||||
raw_event = mailgun_sign({
|
||||
'code': '550',
|
||||
'error': "550 5.1.1 The email account that you tried to reach does not exist. Please try "
|
||||
" 5.1.1 double-checking the recipient's email address for typos or "
|
||||
" 5.1.1 unnecessary spaces.",
|
||||
'event': 'bounced',
|
||||
'recipient': 'noreply@example.com',
|
||||
# (omitting some fields that aren't relevant to the test)
|
||||
})
|
||||
webhook = reverse('mailgun_tracking_webhook')
|
||||
response = self.client.post(webhook, data=raw_event)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailgunTrackingWebhookView,
|
||||
event=ANY, esp_name='Mailgun')
|
||||
event = kwargs['event']
|
||||
self.assertEqual(event.event_type, "bounced")
|
||||
self.assertEqual(event.reject_reason, "bounced")
|
||||
self.assertIn("The email account that you tried to reach does not exist", event.mta_response)
|
||||
|
||||
def test_metadata(self):
|
||||
# Metadata fields are interspersed with other data, but also in message-headers
|
||||
raw_event = mailgun_sign({
|
||||
'event': 'delivered',
|
||||
'message-headers': json.dumps([
|
||||
["X-Mailgun-Variables", "{\"custom1\": \"value1\", \"custom2\": \"{\\\"key\\\":\\\"value\\\"}\"}"],
|
||||
]),
|
||||
'custom1': 'value',
|
||||
'custom2': '{"key":"value"}', # you can store JSON, but you'll need to unpack it yourself
|
||||
})
|
||||
self.client.post(reverse('mailgun_tracking_webhook'), data=raw_event)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler)
|
||||
event = kwargs['event']
|
||||
self.assertEqual(event.metadata, {"custom1": "value1", "custom2": '{"key":"value"}'})
|
||||
|
||||
def test_tags(self):
|
||||
# Most events include multiple 'tag' fields for message's tags
|
||||
raw_event = mailgun_sign({
|
||||
'tag': ['tag1', 'tag2'], # Django TestClient encodes list as multiple field values
|
||||
'event': 'opened',
|
||||
})
|
||||
self.client.post(reverse('mailgun_tracking_webhook'), data=raw_event)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler)
|
||||
event = kwargs['event']
|
||||
self.assertEqual(event.tags, ["tag1", "tag2"])
|
||||
|
||||
def test_x_tags(self):
|
||||
# Delivery events don't include 'tag', but do include 'X-Mailgun-Tag' fields
|
||||
raw_event = mailgun_sign({
|
||||
'X-Mailgun-Tag': ['tag1', 'tag2'],
|
||||
'event': 'delivered',
|
||||
})
|
||||
self.client.post(reverse('mailgun_tracking_webhook'), data=raw_event)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler)
|
||||
event = kwargs['event']
|
||||
self.assertEqual(event.tags, ["tag1", "tag2"])
|
||||
@@ -1,128 +0,0 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
from base64 import b64encode
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from anymail.compat import b
|
||||
from anymail.signals import webhook_event
|
||||
|
||||
|
||||
class DjrillWebhookSecretMixinTests(TestCase):
|
||||
"""
|
||||
Test mixin used in optional Mandrill webhook support
|
||||
"""
|
||||
|
||||
def test_missing_secret(self):
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
self.client.get('/webhook/')
|
||||
|
||||
@override_settings(DJRILL_WEBHOOK_SECRET='abc123')
|
||||
def test_incorrect_secret(self):
|
||||
response = self.client.head('/webhook/?secret=wrong')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@override_settings(DJRILL_WEBHOOK_SECRET='abc123')
|
||||
def test_default_secret_name(self):
|
||||
response = self.client.head('/webhook/?secret=abc123')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_settings(DJRILL_WEBHOOK_SECRET='abc123', DJRILL_WEBHOOK_SECRET_NAME='verysecret')
|
||||
def test_custom_secret_name(self):
|
||||
response = self.client.head('/webhook/?verysecret=abc123')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
@override_settings(DJRILL_WEBHOOK_SECRET='abc123',
|
||||
DJRILL_WEBHOOK_SIGNATURE_KEY="signature")
|
||||
class DjrillWebhookSignatureMixinTests(TestCase):
|
||||
"""
|
||||
Test mixin used in optional Mandrill webhook signature support
|
||||
"""
|
||||
|
||||
def test_incorrect_settings(self):
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
self.client.post('/webhook/?secret=abc123')
|
||||
|
||||
@override_settings(DJRILL_WEBHOOK_URL="/webhook/?secret=abc123",
|
||||
DJRILL_WEBHOOK_SIGNATURE_KEY = "anothersignature")
|
||||
def test_unauthorized(self):
|
||||
response = self.client.post(settings.DJRILL_WEBHOOK_URL)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@override_settings(DJRILL_WEBHOOK_URL="/webhook/?secret=abc123")
|
||||
def test_signature(self):
|
||||
signature = hmac.new(key=b(settings.DJRILL_WEBHOOK_SIGNATURE_KEY),
|
||||
msg=b(settings.DJRILL_WEBHOOK_URL+"mandrill_events[]"),
|
||||
digestmod=hashlib.sha1)
|
||||
hash_string = b64encode(signature.digest())
|
||||
response = self.client.post('/webhook/?secret=abc123', data={"mandrill_events":"[]"},
|
||||
**{"HTTP_X_MANDRILL_SIGNATURE": hash_string})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
@override_settings(DJRILL_WEBHOOK_SECRET='abc123')
|
||||
class DjrillWebhookViewTests(TestCase):
|
||||
"""
|
||||
Test optional Mandrill webhook view
|
||||
"""
|
||||
|
||||
def test_head_request(self):
|
||||
response = self.client.head('/webhook/?secret=abc123')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_post_request_invalid_json(self):
|
||||
response = self.client.post('/webhook/?secret=abc123')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_post_request_valid_json(self):
|
||||
response = self.client.post('/webhook/?secret=abc123', {
|
||||
'mandrill_events': json.dumps([{"event": "send", "msg": {}}])
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_webhook_send_signal(self):
|
||||
self.signal_received_count = 0
|
||||
test_event = {"event": "send", "msg": {}}
|
||||
|
||||
def my_callback(sender, event_type, data, **kwargs):
|
||||
self.signal_received_count += 1
|
||||
self.assertEqual(event_type, 'send')
|
||||
self.assertEqual(data, test_event)
|
||||
|
||||
try:
|
||||
webhook_event.connect(my_callback, weak=False) # local callback func, so don't use weak ref
|
||||
response = self.client.post('/webhook/?secret=abc123', {
|
||||
'mandrill_events': json.dumps([test_event])
|
||||
})
|
||||
finally:
|
||||
webhook_event.disconnect(my_callback)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.signal_received_count, 1)
|
||||
|
||||
def test_webhook_sync_event(self):
|
||||
# Mandrill sync events use a different format from other events
|
||||
# https://mandrill.zendesk.com/hc/en-us/articles/205583297-Sync-Event-Webhook-format
|
||||
self.signal_received_count = 0
|
||||
test_event = {"type": "whitelist", "action": "add"}
|
||||
|
||||
def my_callback(sender, event_type, data, **kwargs):
|
||||
self.signal_received_count += 1
|
||||
self.assertEqual(event_type, 'whitelist_add') # synthesized event_type
|
||||
self.assertEqual(data, test_event)
|
||||
|
||||
try:
|
||||
webhook_event.connect(my_callback, weak=False) # local callback func, so don't use weak ref
|
||||
response = self.client.post('/webhook/?secret=abc123', {
|
||||
'mandrill_events': json.dumps([test_event])
|
||||
})
|
||||
finally:
|
||||
webhook_event.disconnect(my_callback)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.signal_received_count, 1)
|
||||
195
tests/test_mandrill_webhooks.py
Normal file
195
tests/test_mandrill_webhooks.py
Normal file
@@ -0,0 +1,195 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
# noinspection PyUnresolvedReferences
|
||||
from six.moves.urllib.parse import urljoin
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
from base64 import b64encode
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import override_settings
|
||||
from django.utils.timezone import utc
|
||||
from mock import ANY
|
||||
|
||||
from anymail.signals import AnymailTrackingEvent
|
||||
from anymail.webhooks.mandrill import MandrillTrackingWebhookView
|
||||
|
||||
from .webhook_cases import WebhookTestCase, WebhookBasicAuthTestsMixin
|
||||
|
||||
TEST_WEBHOOK_KEY = 'TEST_WEBHOOK_KEY'
|
||||
|
||||
|
||||
def mandrill_args(events=None, urlname='mandrill_tracking_webhook', key=TEST_WEBHOOK_KEY):
|
||||
"""Returns TestClient.post kwargs for Mandrill webhook call with events
|
||||
|
||||
Computes correct signature.
|
||||
"""
|
||||
if events is None:
|
||||
events = []
|
||||
url = urljoin('http://testserver/', reverse(urlname))
|
||||
mandrill_events = json.dumps(events)
|
||||
signed_data = url + 'mandrill_events' + mandrill_events
|
||||
signature = b64encode(hmac.new(key=key.encode('ascii'),
|
||||
msg=signed_data.encode('utf-8'),
|
||||
digestmod=hashlib.sha1).digest())
|
||||
return {
|
||||
'path': url,
|
||||
'data': {'mandrill_events': mandrill_events},
|
||||
'HTTP_X_MANDRILL_SIGNATURE': signature,
|
||||
}
|
||||
|
||||
|
||||
class MandrillWebhookSettingsTestCase(WebhookTestCase):
|
||||
def test_requires_webhook_key(self):
|
||||
webhook = reverse('mandrill_tracking_webhook')
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
self.client.post(webhook, data={'mandrill_events': '[]'})
|
||||
|
||||
|
||||
@override_settings(ANYMAIL_MANDRILL_WEBHOOK_KEY=TEST_WEBHOOK_KEY)
|
||||
class MandrillWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
|
||||
should_warn_if_no_auth = False # because we check webhook signature
|
||||
|
||||
def call_webhook(self):
|
||||
kwargs = mandrill_args([{'event': 'send'}])
|
||||
return self.client.post(**kwargs)
|
||||
|
||||
# Additional tests are in WebhookBasicAuthTestsMixin
|
||||
|
||||
def test_verifies_correct_signature(self):
|
||||
kwargs = mandrill_args([{'event': 'send'}])
|
||||
response = self.client.post(**kwargs)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_verifies_missing_signature(self):
|
||||
webhook = reverse('mandrill_tracking_webhook')
|
||||
response = self.client.post(webhook, data={'mandrill_events': '[{"event":"send"}]'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_verifies_bad_signature(self):
|
||||
kwargs = mandrill_args([{'event': 'send'}], key="wrong API key")
|
||||
response = self.client.post(**kwargs)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
||||
@override_settings(ANYMAIL_MANDRILL_WEBHOOK_KEY=TEST_WEBHOOK_KEY)
|
||||
class MandrillTrackingTestCase(WebhookTestCase):
|
||||
|
||||
def test_head_request(self):
|
||||
# Mandrill verifies webhooks at config time with a HEAD request
|
||||
webhook = reverse('mandrill_tracking_webhook')
|
||||
response = self.client.head(webhook)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_post_request_invalid_json(self):
|
||||
kwargs = mandrill_args()
|
||||
kwargs['data'] = {'mandrill_events': "GARBAGE DATA"}
|
||||
response = self.client.post(**kwargs)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_send_event(self):
|
||||
raw_events = [{
|
||||
"event": "send",
|
||||
"msg": {
|
||||
"ts": 1461095211, # time send called
|
||||
"subject": "Webhook Test",
|
||||
"email": "recipient@example.com",
|
||||
"sender": "sender@example.com",
|
||||
"tags": ["tag1", "tag2"],
|
||||
"metadata": {"custom1": "value1", "custom2": "value2"},
|
||||
"_id": "abcdef012345789abcdef012345789"
|
||||
},
|
||||
"_id": "abcdef012345789abcdef012345789",
|
||||
"ts": 1461095246 # time of event
|
||||
}]
|
||||
response = self.client.post(**mandrill_args(events=raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MandrillTrackingWebhookView,
|
||||
event=ANY, esp_name='Mandrill')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "sent")
|
||||
self.assertEqual(event.timestamp, datetime(2016, 4, 19, 19, 47, 26, tzinfo=utc))
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "abcdef012345789abcdef012345789")
|
||||
self.assertEqual(event.recipient, "recipient@example.com")
|
||||
self.assertEqual(event.tags, ["tag1", "tag2"])
|
||||
self.assertEqual(event.metadata, {"custom1": "value1", "custom2": "value2"})
|
||||
|
||||
def test_hard_bounce_event(self):
|
||||
raw_events = [{
|
||||
"event": "hard_bounce",
|
||||
"msg": {
|
||||
"ts": 1461095211, # time send called
|
||||
"subject": "Webhook Test",
|
||||
"email": "bounce@example.com",
|
||||
"sender": "sender@example.com",
|
||||
"bounce_description": "bad_mailbox",
|
||||
"bgtools_code": 10,
|
||||
"diag": "smtp;550 5.1.1 The email account that you tried to reach does not exist.",
|
||||
"_id": "abcdef012345789abcdef012345789"
|
||||
},
|
||||
"_id": "abcdef012345789abcdef012345789",
|
||||
"ts": 1461095246 # time of event
|
||||
}]
|
||||
response = self.client.post(**mandrill_args(events=raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MandrillTrackingWebhookView,
|
||||
event=ANY, esp_name='Mandrill')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "bounced")
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "abcdef012345789abcdef012345789")
|
||||
self.assertEqual(event.recipient, "bounce@example.com")
|
||||
self.assertEqual(event.mta_response,
|
||||
"smtp;550 5.1.1 The email account that you tried to reach does not exist.")
|
||||
|
||||
def test_click_event(self):
|
||||
raw_events = [{
|
||||
"event": "click",
|
||||
"msg": {
|
||||
"ts": 1461095211, # time send called
|
||||
"subject": "Webhook Test",
|
||||
"email": "recipient@example.com",
|
||||
"sender": "sender@example.com",
|
||||
"opens": [{"ts": 1461095242}],
|
||||
"clicks": [{"ts": 1461095246, "url": "http://example.com"}],
|
||||
"_id": "abcdef012345789abcdef012345789"
|
||||
},
|
||||
"user_agent": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0",
|
||||
"url": "http://example.com",
|
||||
"_id": "abcdef012345789abcdef012345789",
|
||||
"ts": 1461095246 # time of event
|
||||
}]
|
||||
response = self.client.post(**mandrill_args(events=raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MandrillTrackingWebhookView,
|
||||
event=ANY, esp_name='Mandrill')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "clicked")
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.click_url, "http://example.com")
|
||||
self.assertEqual(event.user_agent, "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0")
|
||||
|
||||
def test_sync_event(self):
|
||||
# Mandrill sync events use a different format from other events
|
||||
# https://mandrill.zendesk.com/hc/en-us/articles/205583297-Sync-Event-Webhook-format
|
||||
raw_events = [{
|
||||
"type": "blacklist",
|
||||
"action": "add",
|
||||
"reject": {
|
||||
"email": "recipient@example.com",
|
||||
"reason": "manual edit"
|
||||
}
|
||||
}]
|
||||
response = self.client.post(**mandrill_args(events=raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MandrillTrackingWebhookView,
|
||||
event=ANY, esp_name='Mandrill')
|
||||
event = kwargs['event']
|
||||
self.assertEqual(event.event_type, "unknown")
|
||||
self.assertEqual(event.recipient, "recipient@example.com")
|
||||
self.assertEqual(event.description, "manual edit")
|
||||
86
tests/test_postmark_webhooks.py
Normal file
86
tests/test_postmark_webhooks.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.timezone import get_fixed_timezone
|
||||
from mock import ANY
|
||||
|
||||
from anymail.signals import AnymailTrackingEvent
|
||||
from anymail.webhooks.postmark import PostmarkTrackingWebhookView
|
||||
from .webhook_cases import WebhookBasicAuthTestsMixin, WebhookTestCase
|
||||
|
||||
|
||||
class PostmarkWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
|
||||
def call_webhook(self):
|
||||
webhook = reverse('postmark_tracking_webhook')
|
||||
return self.client.post(webhook, content_type='application/json', data=json.dumps({}))
|
||||
|
||||
# Actual tests are in WebhookBasicAuthTestsMixin
|
||||
|
||||
|
||||
class PostmarkDeliveryTestCase(WebhookTestCase):
|
||||
def test_bounce_event(self):
|
||||
raw_event = {
|
||||
"ID": 901542550,
|
||||
"Type": "HardBounce",
|
||||
"TypeCode": 1,
|
||||
"Name": "Hard bounce",
|
||||
"MessageID": "2706ee8a-737c-4285-b032-ccd317af53ed",
|
||||
"Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
|
||||
"Details": "smtp;550 5.1.1 The email account that you tried to reach does not exist.",
|
||||
"Email": "bounce@example.com",
|
||||
"BouncedAt": "2016-04-27T16:28:50.3963933-04:00",
|
||||
"DumpAvailable": True,
|
||||
"Inactive": True,
|
||||
"CanActivate": True,
|
||||
"Subject": "Postmark event test",
|
||||
"Content": "..."
|
||||
}
|
||||
webhook = reverse('postmark_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_event))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
|
||||
event=ANY, esp_name='Postmark')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "bounced")
|
||||
self.assertEqual(event.esp_event, raw_event)
|
||||
self.assertEqual(event.timestamp, datetime(2016, 4, 27, 16, 28, 50, microsecond=396393,
|
||||
tzinfo=get_fixed_timezone(-4*60)))
|
||||
self.assertEqual(event.message_id, "2706ee8a-737c-4285-b032-ccd317af53ed")
|
||||
self.assertEqual(event.event_id, "901542550")
|
||||
self.assertEqual(event.recipient, "bounce@example.com")
|
||||
self.assertEqual(event.reject_reason, "bounced")
|
||||
self.assertEqual(event.description,
|
||||
"The server was unable to deliver your message (ex: unknown user, mailbox not found).")
|
||||
self.assertEqual(event.mta_response,
|
||||
"smtp;550 5.1.1 The email account that you tried to reach does not exist.")
|
||||
|
||||
def test_open_event(self):
|
||||
raw_event = {
|
||||
"FirstOpen": True,
|
||||
"Client": {"Name": "Gmail", "Company": "Google", "Family": "Gmail"},
|
||||
"OS": {"Name": "unknown", "Company": "unknown", "Family": "unknown"},
|
||||
"Platform": "Unknown",
|
||||
"UserAgent": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0",
|
||||
"ReadSeconds": 0,
|
||||
"Geo": {},
|
||||
"MessageID": "f4830d10-9c35-4f0c-bca3-3d9b459821f8",
|
||||
"ReceivedAt": "2016-04-27T16:21:41.2493688-04:00",
|
||||
"Recipient": "recipient@example.com"
|
||||
}
|
||||
webhook = reverse('postmark_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_event))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
|
||||
event=ANY, esp_name='Postmark')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "opened")
|
||||
self.assertEqual(event.esp_event, raw_event)
|
||||
self.assertEqual(event.timestamp, datetime(2016, 4, 27, 16, 21, 41, microsecond=249368,
|
||||
tzinfo=get_fixed_timezone(-4*60)))
|
||||
self.assertEqual(event.message_id, "f4830d10-9c35-4f0c-bca3-3d9b459821f8")
|
||||
self.assertEqual(event.recipient, "recipient@example.com")
|
||||
self.assertEqual(event.user_agent, "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0")
|
||||
|
||||
235
tests/test_sendgrid_webhooks.py
Normal file
235
tests/test_sendgrid_webhooks.py
Normal file
@@ -0,0 +1,235 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.timezone import utc
|
||||
from mock import ANY
|
||||
|
||||
from anymail.signals import AnymailTrackingEvent
|
||||
from anymail.webhooks.sendgrid import SendGridTrackingWebhookView
|
||||
from .webhook_cases import WebhookBasicAuthTestsMixin, WebhookTestCase
|
||||
|
||||
|
||||
class SendGridWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):
|
||||
def call_webhook(self):
|
||||
webhook = reverse('sendgrid_tracking_webhook')
|
||||
return self.client.post(webhook, content_type='application/json', data=json.dumps([]))
|
||||
|
||||
# Actual tests are in WebhookBasicAuthTestsMixin
|
||||
|
||||
|
||||
class SendGridDeliveryTestCase(WebhookTestCase):
|
||||
|
||||
def test_processed_event(self):
|
||||
raw_events = [{
|
||||
"email": "recipient@example.com",
|
||||
"timestamp": 1461095246,
|
||||
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@example.com>",
|
||||
"sg_event_id": "ZyjAM5rnQmuI1KFInHQ3Nw",
|
||||
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA.filter0425p1mdw1.13037.57168B4A1D.0",
|
||||
"event": "processed",
|
||||
"category": ["tag1", "tag2"],
|
||||
"custom1": "value1",
|
||||
"custom2": "value2",
|
||||
}]
|
||||
webhook = reverse('sendgrid_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
|
||||
event=ANY, esp_name='SendGrid')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "queued")
|
||||
self.assertEqual(event.timestamp, datetime(2016, 4, 19, 19, 47, 26, tzinfo=utc))
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "<wrfRRvF7Q0GgwUo2CvDmEA@example.com>")
|
||||
self.assertEqual(event.event_id, "ZyjAM5rnQmuI1KFInHQ3Nw")
|
||||
self.assertEqual(event.recipient, "recipient@example.com")
|
||||
self.assertEqual(event.tags, ["tag1", "tag2"])
|
||||
self.assertEqual(event.metadata, {"custom1": "value1", "custom2": "value2"})
|
||||
|
||||
def test_delivered_event(self):
|
||||
raw_events = [{
|
||||
"ip": "167.89.17.173",
|
||||
"response": "250 2.0.0 OK 1461095248 m143si2210036ioe.159 - gsmtp ",
|
||||
"sg_event_id": "nOSv8m0eTQ-vxvwNwt3fZQ",
|
||||
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA.filter0425p1mdw1.13037.57168B4A1D.0",
|
||||
"tls": 1,
|
||||
"event": "delivered",
|
||||
"email": "recipient@example.com",
|
||||
"timestamp": 1461095250,
|
||||
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@example.com>"
|
||||
}]
|
||||
webhook = reverse('sendgrid_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
|
||||
event=ANY, esp_name='SendGrid')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "delivered")
|
||||
self.assertEqual(event.timestamp, datetime(2016, 4, 19, 19, 47, 30, tzinfo=utc))
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "<wrfRRvF7Q0GgwUo2CvDmEA@example.com>")
|
||||
self.assertEqual(event.event_id, "nOSv8m0eTQ-vxvwNwt3fZQ")
|
||||
self.assertEqual(event.recipient, "recipient@example.com")
|
||||
self.assertEqual(event.mta_response, "250 2.0.0 OK 1461095248 m143si2210036ioe.159 - gsmtp ")
|
||||
self.assertEqual(event.tags, None)
|
||||
self.assertEqual(event.metadata, None)
|
||||
|
||||
def test_dropped_invalid_event(self):
|
||||
raw_events = [{
|
||||
"email": "invalid@invalid",
|
||||
"smtp-id": "<YZkwwo_vQUidhSh7sCzkvQ@example.com>",
|
||||
"timestamp": 1461095250,
|
||||
"sg_event_id": "3NPOePGOTkeM_U3fgWApfg",
|
||||
"sg_message_id": "filter0093p1las1.9128.5717FB8127.0",
|
||||
"reason": "Invalid",
|
||||
"event": "dropped"
|
||||
}]
|
||||
webhook = reverse('sendgrid_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
|
||||
event=ANY, esp_name='SendGrid')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "rejected")
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "<YZkwwo_vQUidhSh7sCzkvQ@example.com>")
|
||||
self.assertEqual(event.event_id, "3NPOePGOTkeM_U3fgWApfg")
|
||||
self.assertEqual(event.recipient, "invalid@invalid")
|
||||
self.assertEqual(event.reject_reason, "invalid")
|
||||
self.assertEqual(event.mta_response, None)
|
||||
|
||||
def test_dropped_unsubscribed_event(self):
|
||||
raw_events = [{
|
||||
"email": "unsubscribe@example.com",
|
||||
"smtp-id": "<Kwx3gAIKQOG7Nd5XEO7guQ@example.com>",
|
||||
"timestamp": 1461095250,
|
||||
"sg_event_id": "oxy9OLwMTAy5EsuZn1qhIg",
|
||||
"sg_message_id": "filter0199p1las1.4745.5717FB6F5.0",
|
||||
"reason": "Unsubscribed Address",
|
||||
"event": "dropped"
|
||||
}]
|
||||
webhook = reverse('sendgrid_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
|
||||
event=ANY, esp_name='SendGrid')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "rejected")
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "<Kwx3gAIKQOG7Nd5XEO7guQ@example.com>")
|
||||
self.assertEqual(event.event_id, "oxy9OLwMTAy5EsuZn1qhIg")
|
||||
self.assertEqual(event.recipient, "unsubscribe@example.com")
|
||||
self.assertEqual(event.reject_reason, "unsubscribed")
|
||||
self.assertEqual(event.mta_response, None)
|
||||
|
||||
def test_bounce_event(self):
|
||||
raw_events = [{
|
||||
"ip": "167.89.17.173",
|
||||
"status": "5.1.1",
|
||||
"sg_event_id": "lC0Rc-FuQmKbnxCWxX1jRQ",
|
||||
"reason": "550 5.1.1 The email account that you tried to reach does not exist.",
|
||||
"sg_message_id": "Lli-03HcQ5-JLybO9fXsJg.filter0077p1las1.21536.5717FC482.0",
|
||||
"tls": 1,
|
||||
"event": "bounce",
|
||||
"email": "noreply@example.com",
|
||||
"timestamp": 1461095250,
|
||||
"smtp-id": "<Lli-03HcQ5-JLybO9fXsJg@example.com>",
|
||||
"type": "bounce"
|
||||
}]
|
||||
webhook = reverse('sendgrid_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
|
||||
event=ANY, esp_name='SendGrid')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "bounced")
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "<Lli-03HcQ5-JLybO9fXsJg@example.com>")
|
||||
self.assertEqual(event.event_id, "lC0Rc-FuQmKbnxCWxX1jRQ")
|
||||
self.assertEqual(event.recipient, "noreply@example.com")
|
||||
self.assertEqual(event.mta_response, "550 5.1.1 The email account that you tried to reach does not exist.")
|
||||
|
||||
def test_deferred_event(self):
|
||||
raw_events = [{
|
||||
"response": "Email was deferred due to the following reason(s): [IPs were throttled by recipient server]",
|
||||
"sg_event_id": "b_syL5UiTvWC_Ky5L6Bs5Q",
|
||||
"sg_message_id": "u9Gvi3mzT6iC2poAb58_qQ.filter0465p1mdw1.8054.5718271B40.0",
|
||||
"event": "deferred",
|
||||
"email": "recipient@example.com",
|
||||
"attempt": "1",
|
||||
"timestamp": 1461200990,
|
||||
"smtp-id": "<20160421010427.2847.6797@example.com>",
|
||||
}]
|
||||
webhook = reverse('sendgrid_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
|
||||
event=ANY, esp_name='SendGrid')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "deferred")
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "<20160421010427.2847.6797@example.com>")
|
||||
self.assertEqual(event.event_id, "b_syL5UiTvWC_Ky5L6Bs5Q")
|
||||
self.assertEqual(event.recipient, "recipient@example.com")
|
||||
self.assertEqual(event.mta_response,
|
||||
"Email was deferred due to the following reason(s): [IPs were throttled by recipient server]")
|
||||
|
||||
def test_open_event(self):
|
||||
raw_events = [{
|
||||
"email": "recipient@example.com",
|
||||
"timestamp": 1461095250,
|
||||
"ip": "66.102.6.229",
|
||||
"sg_event_id": "MjIwNDg5NTgtZGE3OC00NDI1LWFiMmMtMDUyZTU2ZmFkOTFm",
|
||||
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA.filter0425p1mdw1.13037.57168B4A1D.0",
|
||||
"smtp-id": "<20160421010427.2847.6797@example.com>",
|
||||
"useragent": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0",
|
||||
"event": "open"
|
||||
}]
|
||||
webhook = reverse('sendgrid_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
|
||||
event=ANY, esp_name='SendGrid')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "opened")
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "<20160421010427.2847.6797@example.com>")
|
||||
self.assertEqual(event.event_id, "MjIwNDg5NTgtZGE3OC00NDI1LWFiMmMtMDUyZTU2ZmFkOTFm")
|
||||
self.assertEqual(event.recipient, "recipient@example.com")
|
||||
self.assertEqual(event.user_agent, "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0")
|
||||
|
||||
def test_click_event(self):
|
||||
raw_events = [{
|
||||
"ip": "24.130.34.103",
|
||||
"sg_event_id": "OTdlOGUzYjctYjc5Zi00OWE4LWE4YWUtNjIxNjk2ZTJlNGVi",
|
||||
"sg_message_id": "_fjPjuJfRW-IPs5SuvYotg.filter0590p1mdw1.2098.57168CFC4B.0",
|
||||
"useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36",
|
||||
"smtp-id": "<20160421010427.2847.6797@example.com>",
|
||||
"event": "click",
|
||||
"url_offset": {"index": 0, "type": "html"},
|
||||
"email": "recipient@example.com",
|
||||
"timestamp": 1461095250,
|
||||
"url": "http://www.example.com"
|
||||
}]
|
||||
webhook = reverse('sendgrid_tracking_webhook')
|
||||
response = self.client.post(webhook, content_type='application/json', data=json.dumps(raw_events))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
|
||||
event=ANY, esp_name='SendGrid')
|
||||
event = kwargs['event']
|
||||
self.assertIsInstance(event, AnymailTrackingEvent)
|
||||
self.assertEqual(event.event_type, "clicked")
|
||||
self.assertEqual(event.esp_event, raw_events[0])
|
||||
self.assertEqual(event.message_id, "<20160421010427.2847.6797@example.com>")
|
||||
self.assertEqual(event.event_id, "OTdlOGUzYjctYjc5Zi00OWE4LWE4YWUtNjIxNjk2ZTJlNGVi")
|
||||
self.assertEqual(event.recipient, "recipient@example.com")
|
||||
self.assertEqual(event.user_agent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36")
|
||||
self.assertEqual(event.click_url, "http://www.example.com")
|
||||
@@ -1,9 +1,13 @@
|
||||
# Anymail test utils
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from base64 import b64decode
|
||||
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import warnings
|
||||
from base64 import b64decode
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
def decode_att(att):
|
||||
@@ -28,10 +32,34 @@ def sample_image_content(filename=SAMPLE_IMAGE_FILENAME):
|
||||
return f.read()
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
class AnymailTestMixin:
|
||||
"""Helpful additional methods for Anymail tests"""
|
||||
|
||||
pass
|
||||
def assertWarns(self, expected_warning, msg=None):
|
||||
# We only support the context-manager version
|
||||
try:
|
||||
return super(AnymailTestMixin, self).assertWarns(expected_warning, msg=msg)
|
||||
except TypeError:
|
||||
# Python 2.x: use our backported assertWarns
|
||||
return _AssertWarnsContext(expected_warning, self, msg=msg)
|
||||
|
||||
def assertWarnsRegex(self, expected_warning, expected_regex, msg=None):
|
||||
# We only support the context-manager version
|
||||
try:
|
||||
return super(AnymailTestMixin, self).assertWarnsRegex(expected_warning, expected_regex, msg=msg)
|
||||
except TypeError:
|
||||
# Python 2.x: use our backported assertWarns
|
||||
return _AssertWarnsContext(expected_warning, self, expected_regex=expected_regex, msg=msg)
|
||||
|
||||
@contextmanager
|
||||
def assertDoesNotWarn(self):
|
||||
try:
|
||||
warnings.simplefilter("error")
|
||||
yield
|
||||
finally:
|
||||
warnings.resetwarnings()
|
||||
|
||||
# Plus these methods added below:
|
||||
# assertCountEqual
|
||||
# assertRaisesRegex
|
||||
@@ -45,3 +73,64 @@ for method in ('assertCountEqual', 'assertRaisesRegex', 'assertRegex'):
|
||||
getattr(unittest.TestCase, method)
|
||||
except AttributeError:
|
||||
setattr(AnymailTestMixin, method, getattr(six, method))
|
||||
|
||||
|
||||
# Backported from python 3.5
|
||||
class _AssertWarnsContext(object):
|
||||
"""A context manager used to implement TestCase.assertWarns* methods."""
|
||||
|
||||
def __init__(self, expected, test_case, expected_regex=None, msg=None):
|
||||
self.test_case = test_case
|
||||
self.expected = expected
|
||||
self.test_case = test_case
|
||||
if expected_regex is not None:
|
||||
expected_regex = re.compile(expected_regex)
|
||||
self.expected_regex = expected_regex
|
||||
self.msg = msg
|
||||
|
||||
def _raiseFailure(self, standardMsg):
|
||||
# msg = self.test_case._formatMessage(self.msg, standardMsg)
|
||||
msg = self.msg or standardMsg
|
||||
raise self.test_case.failureException(msg)
|
||||
|
||||
def __enter__(self):
|
||||
# The __warningregistry__'s need to be in a pristine state for tests
|
||||
# to work properly.
|
||||
for v in sys.modules.values():
|
||||
if getattr(v, '__warningregistry__', None):
|
||||
v.__warningregistry__ = {}
|
||||
self.warnings_manager = warnings.catch_warnings(record=True)
|
||||
self.warnings = self.warnings_manager.__enter__()
|
||||
warnings.simplefilter("always", self.expected)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
self.warnings_manager.__exit__(exc_type, exc_value, tb)
|
||||
if exc_type is not None:
|
||||
# let unexpected exceptions pass through
|
||||
return
|
||||
try:
|
||||
exc_name = self.expected.__name__
|
||||
except AttributeError:
|
||||
exc_name = str(self.expected)
|
||||
first_matching = None
|
||||
for m in self.warnings:
|
||||
w = m.message
|
||||
if not isinstance(w, self.expected):
|
||||
continue
|
||||
if first_matching is None:
|
||||
first_matching = w
|
||||
if (self.expected_regex is not None and
|
||||
not self.expected_regex.search(str(w))):
|
||||
continue
|
||||
# store warning for later retrieval
|
||||
self.warning = w
|
||||
self.filename = m.filename
|
||||
self.lineno = m.lineno
|
||||
return
|
||||
# Now we simply try to choose a helpful failure message
|
||||
if first_matching is not None:
|
||||
self._raiseFailure('"{}" does not match "{}"'.format(
|
||||
self.expected_regex.pattern, str(first_matching)))
|
||||
self._raiseFailure("{} not triggered".format(exc_name))
|
||||
|
||||
|
||||
116
tests/webhook_cases.py
Normal file
116
tests/webhook_cases.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import base64
|
||||
|
||||
from django.test import override_settings, SimpleTestCase
|
||||
from mock import create_autospec, ANY
|
||||
|
||||
from anymail.exceptions import AnymailInsecureWebhookWarning
|
||||
from anymail.signals import tracking, inbound
|
||||
|
||||
from .utils import AnymailTestMixin
|
||||
|
||||
|
||||
def event_handler(sender, event, esp_name, **kwargs):
|
||||
"""Prototypical webhook signal handler"""
|
||||
pass
|
||||
|
||||
|
||||
@override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': 'username:password'})
|
||||
class WebhookTestCase(AnymailTestMixin, SimpleTestCase):
|
||||
"""Base for testing webhooks
|
||||
|
||||
- connects webhook signal handlers
|
||||
- sets up basic auth by default (since most ESP webhooks warn if it's not enabled)
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(WebhookTestCase, self).setUp()
|
||||
# Use correct basic auth by default (individual tests can override):
|
||||
self.set_basic_auth()
|
||||
|
||||
# Install mocked signal handlers
|
||||
self.tracking_handler = create_autospec(event_handler)
|
||||
tracking.connect(self.tracking_handler)
|
||||
self.addCleanup(tracking.disconnect, self.tracking_handler)
|
||||
|
||||
self.inbound_handler = create_autospec(event_handler)
|
||||
inbound.connect(self.inbound_handler)
|
||||
self.addCleanup(inbound.disconnect, self.inbound_handler)
|
||||
|
||||
def set_basic_auth(self, username='username', password='password'):
|
||||
"""Set basic auth for all subsequent test client requests"""
|
||||
credentials = base64.b64encode("{}:{}".format(username, password).encode('utf-8')).decode('utf-8')
|
||||
self.client.defaults['HTTP_AUTHORIZATION'] = "Basic {}".format(credentials)
|
||||
|
||||
def assert_handler_called_once_with(self, mockfn, *expected_args, **expected_kwargs):
|
||||
"""Verifies mockfn was called with expected_args and at least expected_kwargs.
|
||||
|
||||
Ignores *additional* actual kwargs (which might be added by Django signal dispatch).
|
||||
(This differs from mock.assert_called_once_with.)
|
||||
|
||||
Returns the actual kwargs.
|
||||
"""
|
||||
self.assertEqual(mockfn.call_count, 1)
|
||||
actual_args, actual_kwargs = mockfn.call_args
|
||||
self.assertEqual(actual_args, expected_args)
|
||||
for key, expected_value in expected_kwargs.items():
|
||||
if expected_value is ANY:
|
||||
self.assertIn(key, actual_kwargs)
|
||||
else:
|
||||
self.assertEqual(actual_kwargs[key], expected_value)
|
||||
return actual_kwargs
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
class WebhookBasicAuthTestsMixin(object):
|
||||
"""Common test cases for webhook basic authentication.
|
||||
|
||||
Instantiate for each ESP's webhooks by:
|
||||
- mixing into WebhookTestCase
|
||||
- defining call_webhook to invoke the ESP's webhook
|
||||
"""
|
||||
|
||||
should_warn_if_no_auth = True # subclass set False if other webhook verification used
|
||||
|
||||
def call_webhook(self):
|
||||
# Concrete test cases should call a webhook via self.client.post,
|
||||
# and return the response
|
||||
raise NotImplementedError()
|
||||
|
||||
@override_settings(ANYMAIL={}) # Clear the WEBHOOK_AUTH settings from superclass
|
||||
def test_warns_if_no_auth(self):
|
||||
if self.should_warn_if_no_auth:
|
||||
with self.assertWarns(AnymailInsecureWebhookWarning):
|
||||
response = self.call_webhook()
|
||||
else:
|
||||
with self.assertDoesNotWarn():
|
||||
response = self.call_webhook()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_verifies_basic_auth(self):
|
||||
response = self.call_webhook()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_verifies_bad_auth(self):
|
||||
self.set_basic_auth('baduser', 'wrongpassword')
|
||||
response = self.call_webhook()
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_verifies_missing_auth(self):
|
||||
del self.client.defaults['HTTP_AUTHORIZATION']
|
||||
response = self.call_webhook()
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': ['cred1:pass1', 'cred2:pass2']})
|
||||
def test_supports_credential_rotation(self):
|
||||
"""You can supply a list of basic auth credentials, and any is allowed"""
|
||||
self.set_basic_auth('cred1', 'pass1')
|
||||
response = self.call_webhook()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.set_basic_auth('cred2', 'pass2')
|
||||
response = self.call_webhook()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.set_basic_auth('baduser', 'wrongpassword')
|
||||
response = self.call_webhook()
|
||||
self.assertEqual(response.status_code, 400)
|
||||
Reference in New Issue
Block a user