Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
93 changes: 93 additions & 0 deletions lms/djangoapps/mailgun_webhook/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from notification_prefs import NOTIFICATION_PREF_KEY
from student.tests.factories import UserFactory
from user_api.models import UserPreference
from mailgun_webhook.views import unsubscribe


@override_settings(MAILGUN_API_KEY="key-1lmspg67fhkxg534xhdaxcxc4ua54fo6")
class MailgunUnsubscribeTest(TestCase):
def setUp(self):
self.user = UserFactory.create()
self.request_factory = RequestFactory()
self.valid_request_data = {
"timestamp": "1373040880",
"token": "3p662km24nm--z85hgk0f1cjhuvpdnyv1gy5b0lrpj34r48dz1",
"signature": "3fb40b08fc7b1c57e1cfa616abb33cf5b613ea34801049147fb7742adda6ae82",
"event": "unsubscribed",
"recipient": self.user.email,
}
UserPreference.objects.create(user=self.user, key=NOTIFICATION_PREF_KEY)

def get_pref_exists(self):
return UserPreference.objects.filter(
user=self.user,
key=NOTIFICATION_PREF_KEY
).exists()

@override_settings(MAILGUN_API_KEY=None)
def test_mailgun_not_configured(self):
request = self.request_factory.post("dummy", data=self.valid_request_data)
response = unsubscribe(request)
self.assertEqual(response.status_code, 404)
self.assertTrue(self.get_pref_exists())

def test_get(self):
request = self.request_factory.get("dummy", data=self.valid_request_data)
response = unsubscribe(request)
self.assertEqual(response.status_code, 405)
self.assertTrue(self.get_pref_exists())

def test_missing_data(self):
def test_case(key):
data = self.valid_request_data.copy()
del data[key]
request = self.request_factory.post("dummy", data=data)
response = unsubscribe(request)
self.assertEqual(response.status_code, 400)
self.assertTrue(self.get_pref_exists())

test_case("timestamp")
test_case("token")
test_case("signature")
test_case("event")
test_case("recipient")

def test_invalid_signature(self):
data = self.valid_request_data.copy()
data["signature"] = "not the right signature"
request = self.request_factory.post("dummy", data=data)
response = unsubscribe(request)
self.assertEqual(response.status_code, 400)
self.assertTrue(self.get_pref_exists())

def test_invalid_event(self):
data = self.valid_request_data.copy()
data["event"] = "clicked"
request = self.request_factory.post("dummy", data=data)
response = unsubscribe(request)
self.assertEqual(response.status_code, 400)
self.assertTrue(self.get_pref_exists())

def test_success(self):
request = self.request_factory.post("dummy", data=self.valid_request_data)
response = unsubscribe(request)
self.assertEqual(response.status_code, 200)
self.assertFalse(self.get_pref_exists())

def test_already_unsubscribed(self):
UserPreference.objects.filter(user=self.user, key=NOTIFICATION_PREF_KEY).delete()
request = self.request_factory.post("dummy", data=self.valid_request_data)
response = unsubscribe(request)
self.assertEqual(response.status_code, 200)
self.assertFalse(self.get_pref_exists())

def test_invalid_email(self):
data = self.valid_request_data.copy()
data["recipient"] = "other@edx.org"
request = self.request_factory.post("dummy", data=data)
response = unsubscribe(request)
self.assertEqual(response.status_code, 200)
self.assertTrue(self.get_pref_exists())
60 changes: 60 additions & 0 deletions lms/djangoapps/mailgun_webhook/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed
from django.http import HttpResponseNotFound
import hashlib
import hmac
from notification_prefs import NOTIFICATION_PREF_KEY
from user_api.models import UserPreference


def unsubscribe(request):
"""
Process an unsubscribe request from Mailgun

This updates a user's notification preference to match the unsubscription.

Responses are empty because Mailgun does not process the response body.
See http://documentation.mailgun.com/user_manual.html#manual-webhooks for
more about Mailgun's webhooks.
"""
mailgun_key = getattr(settings, "MAILGUN_API_KEY", None)
if mailgun_key is None:
return HttpResponseNotFound()

if request.method != "POST":
return HttpResponseNotAllowed(["POST"])

timestamp = request.POST.get("timestamp")
token = request.POST.get("token")
signature = request.POST.get("signature")
event = request.POST.get("event")
recipient = request.POST.get("recipient")
if (
timestamp is None or
token is None or
signature is None or
event is None or
recipient is None
):
return HttpResponseBadRequest()

computed_signature = hmac.new(
key=mailgun_key,
msg="{timestamp}{token}".format(timestamp=timestamp, token=token),
digestmod=hashlib.sha256
).hexdigest()
if computed_signature != signature:
return HttpResponseBadRequest()

if event != "unsubscribed":
return HttpResponseBadRequest()

try:
user = User.objects.get(email=recipient)
UserPreference.objects.filter(user=user, key=NOTIFICATION_PREF_KEY).delete()
except User.DoesNotExist:
# User has changed email address; we have no way to know who this is
pass

return HttpResponse()
3 changes: 3 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,9 @@

# Notification preferences setting
'notification_prefs',

# Mailgun webhook
'mailgun_webhook',
)

######################### MARKETING SITE ###############################
Expand Down
1 change: 1 addition & 0 deletions lms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@
include('django_comment_client.urls')),
url(r'^notification_prefs/enable/', 'notification_prefs.views.enable'),
url(r'^notification_prefs/disable/', 'notification_prefs.views.disable'),
url(r'^mailgun/unsubscribe/', 'mailgun_webhook.views.unsubscribe'),
)
urlpatterns += (
# This MUST be the last view in the courseware--it's a catch-all for custom tabs.
Expand Down