diff --git a/gracedb/alerts/tests/test_email.py b/gracedb/alerts/tests/test_email.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea2a655592c709cca68cf512022656feb189febb
--- /dev/null
+++ b/gracedb/alerts/tests/test_email.py
@@ -0,0 +1,424 @@
+import mock
+
+from django.test import override_settings
+
+from alerts.models import Contact, Notification
+from alerts.email import issue_email_alerts, prepare_email_body
+from core.tests.utils import GraceDbTestBase
+from core.time_utils import gpsToUtc
+from events.models import Label
+from superevents.tests.mixins import SupereventCreateMixin
+
+
+class TestEmailBody(GraceDbTestBase, SupereventCreateMixin):
+
+    @classmethod
+    def setUpTestData(cls):
+        super(TestEmailBody, cls).setUpTestData()
+
+        # Create a superevent
+        cls.superevent = cls.create_superevent(cls.internal_user,
+            'fake_group', 'fake_pipeline', 'fake_search')
+
+        # References to cls/self.event will refer to the superevent's
+        cls.event = cls.superevent.preferred_event
+
+        # Create a few labels
+        cls.label1, _ = Label.objects.get_or_create(name='TEST_LABEL1')
+        cls.label2, _ = Label.objects.get_or_create(name='TEST_LABEL2')
+
+    def test_new_event(self):
+        """Test email contents for vanilla new event"""
+        # Get email body
+        email_body = prepare_email_body(self.event, 'new')
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn('New {grp} / {pipeline} event: {gid}'.format(
+            grp=self.event.group.name, pipeline=self.event.pipeline.name,
+            gid=self.event.graceid), lines[0])
+        self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
+            lines[2])
+        self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.event.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.event.labels.values_list('name', flat=True))),
+            lines[7])
+
+    def test_new_event_more_info(self):
+        """Test email contents for more interesting new event"""
+        # Make event more interesting
+        self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+        self.event.far = 1e-10
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+        self.event.instruments = 'I1,I2'
+        self.event.save()
+
+        # Get email body
+        email_body = prepare_email_body(self.event, 'new')
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn('New {grp} / {pipeline} event: {gid}'.format(
+            grp=self.event.group.name, pipeline=self.event.pipeline.name,
+            gid=self.event.graceid), lines[0])
+        self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
+            lines[2])
+        self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.event.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.event.labels.values_list('name', flat=True))),
+            lines[7])
+        self.assertIn('Instruments: {inst}'.format(inst=
+            self.event.instruments), lines[8])
+        self.assertIn('Component masses: {m1}, {m2}'.format(
+            m1=self.event.singleinspiral_set.first().mass1,
+            m2=self.event.singleinspiral_set.first().mass2), lines[9])
+
+    def test_update_event(self):
+        """Test email contents for event update"""
+        # Get email body
+        email_body = prepare_email_body(self.event, 'update')
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn('Updated {grp} / {pipeline} event: {gid}'.format(
+            grp=self.event.group.name, pipeline=self.event.pipeline.name,
+            gid=self.event.graceid), lines[0])
+        self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
+            lines[2])
+        self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.event.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.event.labels.values_list('name', flat=True))),
+            lines[7])
+
+    def test_label_added_event(self):
+        """Test email contents for label_added to event alert"""
+        # Get email body
+        email_body = prepare_email_body(self.event, 'label_added',
+            label=self.label1)
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn(('Label {label} added to {grp} / {pipeline} event: '
+            '{gid}').format(label=self.label1.name, grp=self.event.group.name,
+            pipeline=self.event.pipeline.name, gid=self.event.graceid),
+            lines[0])
+        self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
+            lines[2])
+        self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.event.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.event.labels.values_list('name', flat=True))),
+            lines[7])
+
+    def test_label_removed_event(self):
+        """Test email contents for label_removed to event alert"""
+        # Get email body
+        email_body = prepare_email_body(self.event, 'label_removed',
+            label=self.label1)
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn(('Label {label} removed from {grp} / {pipeline} event: '
+            '{gid}').format(label=self.label1.name, grp=self.event.group.name,
+            pipeline=self.event.pipeline.name, gid=self.event.graceid),
+            lines[0])
+        self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
+            lines[2])
+        self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.event.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.event.labels.values_list('name', flat=True))),
+            lines[7])
+
+    def test_new_superevent(self):
+        """Test email contents for vanilla new superevent"""
+        # Get email body
+        email_body = prepare_email_body(self.superevent, 'new')
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn('New superevent: {sid}'.format(
+            sid=self.superevent.superevent_id), lines[0])
+        self.assertIn('Superevent time (GPS): {gps}'.format(
+            gps=self.superevent.gpstime), lines[2])
+        self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.superevent.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.superevent.labels.values_list('name', flat=True))),
+            lines[7])
+
+    def test_new_superevent_more_info(self):
+        """Test email contents for more interesting new event"""
+        # Make superevent more interesting
+        self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+        self.event.far = 1e-10
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+        self.event.save()
+
+        # Get email body
+        email_body = prepare_email_body(self.superevent, 'new')
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn('New superevent: {sid}'.format(
+            sid=self.superevent.superevent_id), lines[0])
+        self.assertIn('Superevent time (GPS): {gps}'.format(
+            gps=self.superevent.gpstime), lines[2])
+        self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.superevent.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.superevent.labels.values_list('name', flat=True))),
+            lines[7])
+        self.assertIn('Component masses: {m1}, {m2}'.format(
+            m1=self.superevent.preferred_event.singleinspiral_set.first().mass1,
+            m2=self.superevent.preferred_event.singleinspiral_set.first().mass2),
+            lines[8])
+
+    def test_update_superevent(self):
+        """Test email contents for superevent update"""
+        # Get email body
+        email_body = prepare_email_body(self.superevent, 'update')
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn('Updated superevent: {sid}'.format(
+            sid=self.superevent.superevent_id), lines[0])
+        self.assertIn('Superevent time (GPS): {gps}'.format(
+            gps=self.superevent.gpstime), lines[2])
+        self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.superevent.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.superevent.labels.values_list('name', flat=True))),
+            lines[7])
+
+    def test_label_added_superevent(self):
+        """Test email contents for label_added to superevent alert"""
+        # Get email body
+        email_body = prepare_email_body(self.superevent, 'label_added',
+            label=self.label1)
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn('Label {label} added to superevent: {sid}'.format(
+            label=self.label1.name, sid=self.superevent.superevent_id),
+            lines[0])
+        self.assertIn('Superevent time (GPS): {gps}'.format(
+            gps=self.superevent.gpstime), lines[2])
+        self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.superevent.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.superevent.labels.values_list('name', flat=True))),
+            lines[7])
+
+    def test_label_removed_superevent(self):
+        """Test email contents for label_removed from superevent alert"""
+        # Get email body
+        email_body = prepare_email_body(self.superevent, 'label_removed',
+            label=self.label1)
+
+        # Check email body
+        lines = email_body.split('\n')
+        self.assertIn('Label {label} removed from superevent: {sid}'.format(
+            label=self.label1.name, sid=self.superevent.superevent_id),
+            lines[0])
+        self.assertIn('Superevent time (GPS): {gps}'.format(
+            gps=self.superevent.gpstime), lines[2])
+        self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
+            self.superevent.gpstime).isoformat()), lines[3])
+        self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
+        self.assertIn('Labels: {labels}'.format(labels=", ".join(
+            self.superevent.labels.values_list('name', flat=True))),
+            lines[7])
+
+
+@mock.patch('alerts.email.EmailMessage')
+class TestEmailSend(GraceDbTestBase, SupereventCreateMixin):
+
+    @classmethod
+    def setUpTestData(cls):
+        super(TestEmailSend, cls).setUpTestData()
+
+        # Create a superevent
+        cls.superevent = cls.create_superevent(cls.internal_user,
+            'fake_group', 'fake_pipeline', 'fake_search')
+
+        # References to cls/self.event will refer to the superevent's
+        cls.event = cls.superevent.preferred_event
+
+        # Create a few labels
+        cls.label1, _ = Label.objects.get_or_create(name='TEST_LABEL1')
+        cls.label2, _ = Label.objects.get_or_create(name='TEST_LABEL2')
+
+        # Create a few email contacts
+        cls.contact1 = Contact.objects.create(user=cls.internal_user,
+            description='contact1', email='contact1@test.com',
+            verified=True)
+        cls.contact2 = Contact.objects.create(user=cls.internal_user,
+            description='contact2', email='contact2@test.com',
+            verified=True)
+        cls.contacts = Contact.objects.filter(pk__in=[cls.contact1.pk,
+            cls.contact2.pk])
+
+    def test_new_superevent(self, mock_email):
+        """Test contacts and email subject for new superevent alert"""
+        issue_email_alerts(self.superevent, 'new', self.contacts)
+
+        # Check calls to EmailMessage init
+        self.assertEqual(mock_email.call_count, self.contacts.count())
+
+        # Check 'to' address
+        tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
+        self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
+            'email', flat=True))))
+
+        # Check subject
+        subject = mock_email.call_args[1]['subject']
+        self.assertEqual(subject, '[gracedb] Superevent created: {sid}'
+            .format(sid=self.superevent.superevent_id))
+
+    def test_update_superevent(self, mock_email):
+        """Test contacts and email subject for update superevent alert"""
+        issue_email_alerts(self.superevent, 'update', self.contacts)
+
+        # Check calls to EmailMessage init
+        self.assertEqual(mock_email.call_count, self.contacts.count())
+
+        # Check 'to' address
+        tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
+        self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
+            'email', flat=True))))
+
+        # Check subject
+        subject = mock_email.call_args[1]['subject']
+        self.assertEqual(subject, '[gracedb] Superevent updated: {sid}'
+            .format(sid=self.superevent.superevent_id))
+
+    def test_label_added_superevent(self, mock_email):
+        """Test contacts and email subject for label_added superevent alert"""
+        issue_email_alerts(self.superevent, 'label_added', self.contacts,
+            label=self.label1)
+
+        # Check calls to EmailMessage init
+        self.assertEqual(mock_email.call_count, self.contacts.count())
+
+        # Check 'to' address
+        tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
+        self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
+            'email', flat=True))))
+
+        # Check subject
+        subject = mock_email.call_args[1]['subject']
+        self.assertEqual(subject, ('[gracedb] Superevent labeled with '
+            '{label}: {sid}').format(label=self.label1.name,
+            sid=self.superevent.superevent_id))
+
+    def test_label_removed_superevent(self, mock_email):
+        """
+        Test contacts and email subject for label_removed superevent alert
+        """
+        issue_email_alerts(self.superevent, 'label_removed', self.contacts,
+            label=self.label1)
+
+        # Check calls to EmailMessage init
+        self.assertEqual(mock_email.call_count, self.contacts.count())
+
+        # Check 'to' address
+        tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
+        self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
+            'email', flat=True))))
+
+        # Check subject
+        subject = mock_email.call_args[1]['subject']
+        self.assertEqual(subject, ('[gracedb] Label {label} removed from '
+            'superevent: {sid}').format(label=self.label1.name,
+            sid=self.superevent.superevent_id))
+
+    def test_new_event(self, mock_email):
+        """Test contacts and email subject for new event alert"""
+        issue_email_alerts(self.event, 'new', self.contacts)
+
+        # Check calls to EmailMessage init
+        self.assertEqual(mock_email.call_count, self.contacts.count())
+
+        # Check 'to' address
+        tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
+        self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
+            'email', flat=True))))
+
+        # Check subject
+        subject = mock_email.call_args[1]['subject']
+        self.assertEqual(subject, '[gracedb] {pipeline} event created: {gid}'
+            .format(pipeline=self.event.pipeline.name, gid=self.event.graceid))
+
+    def test_update_event(self, mock_email):
+        """Test contacts and email subject for update event alert"""
+        issue_email_alerts(self.event, 'update', self.contacts)
+
+        # Check calls to EmailMessage init
+        self.assertEqual(mock_email.call_count, self.contacts.count())
+
+        # Check 'to' address
+        tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
+        self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
+            'email', flat=True))))
+
+        # Check subject
+        subject = mock_email.call_args[1]['subject']
+        self.assertEqual(subject, '[gracedb] {pipeline} event updated: {gid}'
+            .format(pipeline=self.event.pipeline.name, gid=self.event.graceid))
+
+    def test_label_added_superevent(self, mock_email):
+        """Test contacts and email subject for label_added event alert"""
+        issue_email_alerts(self.event, 'label_added', self.contacts,
+            label=self.label1)
+
+        # Check calls to EmailMessage init
+        self.assertEqual(mock_email.call_count, self.contacts.count())
+
+        # Check 'to' address
+        tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
+        self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
+            'email', flat=True))))
+
+        # Check subject
+        subject = mock_email.call_args[1]['subject']
+        self.assertEqual(subject, ('[gracedb] {pipeline} event labeled with '
+            '{label}: {gid}').format(pipeline=self.event.pipeline.name,
+            label=self.label1.name, gid=self.event.graceid))
+
+    def test_label_removed_event(self, mock_email):
+        """Test contacts and email subject for label_removed event alert"""
+        issue_email_alerts(self.event, 'label_removed', self.contacts,
+            label=self.label1)
+
+        # Check calls to EmailMessage init
+        self.assertEqual(mock_email.call_count, self.contacts.count())
+
+        # Check 'to' address
+        tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
+        self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
+            'email', flat=True))))
+
+        # Check subject
+        subject = mock_email.call_args[1]['subject']
+        self.assertEqual(subject, ('[gracedb] Label {label} removed from '
+            '{pipeline} event: {gid}').format(label=self.label1.name,
+            pipeline=self.event.pipeline.name, gid=self.event.graceid))
diff --git a/gracedb/alerts/tests/test_phone.py b/gracedb/alerts/tests/test_phone.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ee8df3dfeced3f258fed740d619c8e4d7799272
--- /dev/null
+++ b/gracedb/alerts/tests/test_phone.py
@@ -0,0 +1,563 @@
+import mock
+
+from django.conf import settings
+from django.test import override_settings
+from django.urls import reverse
+from django.utils.http import urlencode
+
+from alerts.models import Contact
+from alerts.phone import (
+    get_message_content, compile_twiml_url, issue_phone_alerts,
+)
+from alerts.utils import convert_superevent_id_to_speech
+from core.tests.utils import GraceDbTestBase
+from core.time_utils import gpsToUtc
+from core.urls import build_absolute_uri
+from events.models import Label
+from superevents.tests.mixins import SupereventCreateMixin
+
+
+class TestMessageContent(GraceDbTestBase, SupereventCreateMixin):
+    """Test content sent in text message alerts"""
+
+    @classmethod
+    def setUpTestData(cls):
+        super(TestMessageContent, cls).setUpTestData()
+
+        # Create a superevent
+        cls.superevent = cls.create_superevent(cls.internal_user,
+            'fake_group', 'fake_pipeline', 'fake_search')
+
+        # References to cls/self.event will refer to the superevent's
+        cls.event = cls.superevent.preferred_event
+
+        # Create a label
+        cls.label, _ = Label.objects.get_or_create(name='TEST_LABEL1')
+
+    def test_new_event(self):
+        """Test message contents for new event alert"""
+        message = get_message_content(self.event, 'new')
+
+        # Check content
+        expected = 'A {pipeline} event with GraceDB ID {gid} was created' \
+            .format(pipeline=self.event.pipeline.name, gid=self.event.graceid)
+        expected_url = build_absolute_uri(reverse('view',
+            args=[self.event.graceid]))
+        self.assertIn(expected, message)
+        self.assertIn(expected_url, message)
+
+    def test_update_event(self):
+        """Test message contents for event update alert"""
+        message = get_message_content(self.event, 'update')
+
+        # Check content
+        expected = 'A {pipeline} event with GraceDB ID {gid} was updated' \
+            .format(pipeline=self.event.pipeline.name, gid=self.event.graceid)
+        expected_url = build_absolute_uri(reverse('view',
+            args=[self.event.graceid]))
+        self.assertIn(expected, message)
+        self.assertIn(expected_url, message)
+
+    def test_label_added_event(self):
+        """Test message contents for label_added event alert"""
+        message = get_message_content(self.event, 'label_added',
+            label=self.label)
+
+        # Check content
+        expected = ('A {pipeline} event with GraceDB ID {gid} was labeled '
+            'with {label}').format(pipeline=self.event.pipeline.name,
+            label=self.label.name, gid=self.event.graceid)
+        expected_url = build_absolute_uri(reverse('view',
+            args=[self.event.graceid]))
+        self.assertIn(expected, message)
+        self.assertIn(expected_url, message)
+
+    def test_label_removed_event(self):
+        """Test message contents for label_removed event alert"""
+        message = get_message_content(self.event, 'label_removed',
+            label=self.label)
+
+        # Check content
+        expected = ('The label {label} was removed from a {pipeline} event '
+            'with GraceDB ID {gid}').format(pipeline=self.event.pipeline.name,
+            label=self.label.name, gid=self.event.graceid)
+        expected_url = build_absolute_uri(reverse('view',
+            args=[self.event.graceid]))
+        self.assertIn(expected, message)
+        self.assertIn(expected_url, message)
+
+    def test_new_superevent(self):
+        """Test message contents for new superevent alert"""
+        message = get_message_content(self.superevent, 'new')
+
+        # Check content
+        expected = 'A superevent with GraceDB ID {sid} was created'.format(
+            sid=self.superevent.superevent_id)
+        expected_url = build_absolute_uri(reverse('superevents:view',
+            args=[self.superevent.superevent_id]))
+        self.assertIn(expected, message)
+        self.assertIn(expected_url, message)
+
+    def test_update_superevent(self):
+        """Test message contents for superevent update alert"""
+        message = get_message_content(self.superevent, 'update')
+
+        # Check content
+        expected = 'A superevent with GraceDB ID {sid} was updated'.format(
+            sid=self.superevent.superevent_id)
+        expected_url = build_absolute_uri(reverse('superevents:view',
+            args=[self.superevent.superevent_id]))
+        self.assertIn(expected, message)
+        self.assertIn(expected_url, message)
+
+    def test_label_added_superevent(self):
+        """Test message contents for label_added superevent alert"""
+        message = get_message_content(self.superevent, 'label_added',
+            label=self.label)
+
+        # Check content
+        expected = ('A superevent with GraceDB ID {sid} was labeled '
+            'with {label}').format(sid=self.superevent.superevent_id,
+            label=self.label.name)
+        expected_url = build_absolute_uri(reverse('superevents:view',
+            args=[self.superevent.superevent_id]))
+        self.assertIn(expected, message)
+        self.assertIn(expected_url, message)
+
+    def test_label_removed_superevent(self):
+        """Test message contents for label_removed superevent alert"""
+        message = get_message_content(self.superevent, 'label_removed',
+            label=self.label)
+
+        # Check content
+        expected = ('The label {label} was removed from a superevent with '
+            'GraceDB ID {sid}').format(sid=self.superevent.superevent_id,
+            label=self.label.name)
+        expected_url = build_absolute_uri(reverse('superevents:view',
+            args=[self.superevent.superevent_id]))
+        self.assertIn(expected, message)
+        self.assertIn(expected_url, message)
+
+
+class TestTwimlUrl(GraceDbTestBase, SupereventCreateMixin):
+    """Test content sent in text message alerts"""
+
+    @classmethod
+    def setUpTestData(cls):
+        super(TestTwimlUrl, cls).setUpTestData()
+
+        # Create a superevent
+        cls.superevent = cls.create_superevent(cls.internal_user,
+            'fake_group', 'fake_pipeline', 'fake_search')
+
+        # References to cls/self.event will refer to the superevent's
+        cls.event = cls.superevent.preferred_event
+
+        # Create a label
+        cls.label, _ = Label.objects.get_or_create(name='TEST_LABEL1')
+
+    def test_new_event(self):
+        """Test TwiML URL for new event alert"""
+        url = compile_twiml_url(self.event, 'new')
+
+        # Compile expected URL
+        urlparams = {
+            'graceid': self.event.graceid,
+            'pipeline': self.event.pipeline.name,
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['event']['new'],
+            params=urlencode(urlparams))
+        # Check URL
+        self.assertEqual(expected_url, url)
+
+    def test_update_event(self):
+        """Test TwiML URL for event update alert"""
+        url = compile_twiml_url(self.event, 'update')
+
+        # Compile expected URL
+        urlparams = {
+            'graceid': self.event.graceid,
+            'pipeline': self.event.pipeline.name,
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['event']['update'],
+            params=urlencode(urlparams))
+        # Check URL
+        self.assertEqual(expected_url, url)
+
+    def test_label_added_event(self):
+        """Test TwiML URL for label_added event alert"""
+        url = compile_twiml_url(self.event, 'label_added',
+            label=self.label.name)
+
+        # Compile expected URL
+        urlparams = {
+            'graceid': self.event.graceid,
+            'pipeline': self.event.pipeline.name,
+            'label_lower': self.label.name.lower(),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['event']['label_added'],
+            params=urlencode(urlparams))
+        # Check URL
+        self.assertEqual(expected_url, url)
+
+    def test_label_removed_event(self):
+        """Test TwiML URL for label_removed event alert"""
+        url = compile_twiml_url(self.event, 'label_removed',
+        label=self.label.name)
+
+        # Compile expected URL
+        urlparams = {
+            'graceid': self.event.graceid,
+            'pipeline': self.event.pipeline.name,
+            'label_lower': self.label.name.lower(),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['event']['label_removed'],
+            params=urlencode(urlparams))
+        # Check URL
+        self.assertEqual(expected_url, url)
+
+    def test_new_superevent(self):
+        """Test TwiML URL for new superevent alert"""
+        url = compile_twiml_url(self.superevent, 'new')
+
+        # Compile expected URL
+        urlparams = {
+            'sid': convert_superevent_id_to_speech(
+                self.superevent.superevent_id),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['superevent']['new'],
+            params=urlencode(urlparams))
+        # Check URL
+        self.assertEqual(expected_url, url)
+
+    def test_update_superevent(self):
+        """Test TwiML URL for superevent update alert"""
+        url = compile_twiml_url(self.superevent, 'update')
+
+        # Compile expected URL
+        urlparams = {
+            'sid': convert_superevent_id_to_speech(
+                self.superevent.superevent_id),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['superevent']['update'],
+            params=urlencode(urlparams))
+        # Check URL
+        self.assertEqual(expected_url, url)
+
+    def test_label_added_superevent(self):
+        """Test TwiML URL for label_added superevent alert"""
+        url = compile_twiml_url(self.superevent, 'label_added',
+            label=self.label.name)
+
+        # Compile expected URL
+        urlparams = {
+            'sid': convert_superevent_id_to_speech(
+                self.superevent.superevent_id),
+            'label_lower': self.label.name.lower(),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['superevent']['label_added'],
+            params=urlencode(urlparams))
+        # Check URL
+        self.assertEqual(expected_url, url)
+
+    def test_label_removed_superevent(self):
+        """Test TwiML URL for label_removed superevent alert"""
+        url = compile_twiml_url(self.superevent, 'label_removed',
+            label=self.label.name)
+
+        # Compile expected URL
+        urlparams = {
+            'sid': convert_superevent_id_to_speech(
+                self.superevent.superevent_id),
+            'label_lower': self.label.name.lower(),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['superevent']['label_removed'],
+            params=urlencode(urlparams))
+        # Check URL
+        self.assertEqual(expected_url, url)
+
+
+@mock.patch('alerts.phone.twilio_client.messages.create')
+@mock.patch('alerts.phone.twilio_client.calls.create')
+class TestPhoneCallAndText(GraceDbTestBase, SupereventCreateMixin):
+
+    @classmethod
+    def setUpTestData(cls):
+        super(TestPhoneCallAndText, cls).setUpTestData()
+
+        # Create a superevent
+        cls.superevent = cls.create_superevent(cls.internal_user,
+            'fake_group', 'fake_pipeline', 'fake_search')
+
+        # References to cls/self.event will refer to the superevent's
+        cls.event = cls.superevent.preferred_event
+
+        # Create a label
+        cls.label, _ = Label.objects.get_or_create(name='TEST_LABEL1')
+
+        # Create a few phone contacts
+        cls.contact1 = Contact.objects.create(user=cls.internal_user,
+            description='contact1', phone='12345678901', verified=True,
+            phone_method=Contact.CONTACT_PHONE_CALL)
+        cls.contact2 = Contact.objects.create(user=cls.internal_user,
+            description='contact2', phone='12345654321', verified=True,
+            phone_method=Contact.CONTACT_PHONE_BOTH)
+        cls.contacts = Contact.objects.filter(pk__in=[cls.contact1.pk,
+            cls.contact2.pk])
+
+        # Refresh from DB to pull formatted phone numbers
+        cls.contact1.refresh_from_db()
+        cls.contact2.refresh_from_db()
+
+    def test_new_superevent(self, mock_call, mock_text):
+        """Test issuing phone alerts for new superevent alert"""
+        issue_phone_alerts(self.superevent, 'new', self.contacts)
+
+        # Check calls
+        self.assertEqual(mock_call.call_count, 2)
+        self.assertEqual(mock_call.call_args_list[0][1]['to'],
+            self.contact1.phone)
+        self.assertEqual(mock_call.call_args_list[1][1]['to'],
+            self.contact2.phone)
+
+        # Check texts
+        self.assertEqual(mock_text.call_count, 1)
+        self.assertEqual(mock_text.call_args[1]['to'],
+            self.contact2.phone)
+
+        # Check URLs
+        urlparams = {
+            'sid': convert_superevent_id_to_speech(
+                self.superevent.superevent_id),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['superevent']['new'],
+            params=urlencode(urlparams))
+        # Check URL
+        for call_args in mock_call.call_args_list:
+            self.assertEqual(call_args[1]['url'], expected_url)
+
+    def test_update_superevent(self, mock_call, mock_text):
+        """Test issuing phone alerts for superevent update alert"""
+        issue_phone_alerts(self.superevent, 'update', self.contacts)
+
+        # Check calls
+        self.assertEqual(mock_call.call_count, 2)
+        self.assertEqual(mock_call.call_args_list[0][1]['to'],
+            self.contact1.phone)
+        self.assertEqual(mock_call.call_args_list[1][1]['to'],
+            self.contact2.phone)
+
+        # Check texts
+        self.assertEqual(mock_text.call_count, 1)
+        self.assertEqual(mock_text.call_args[1]['to'],
+            self.contact2.phone)
+        # Check URLs
+        urlparams = {
+            'sid': convert_superevent_id_to_speech(
+                self.superevent.superevent_id),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['superevent']['update'],
+            params=urlencode(urlparams))
+        # Check URL
+        for call_args in mock_call.call_args_list:
+            self.assertEqual(call_args[1]['url'], expected_url)
+
+    def test_label_added_superevent(self, mock_call, mock_text):
+        """Test issuing phone alerts for label_added superevent alert"""
+        issue_phone_alerts(self.superevent, 'label_added', self.contacts,
+            label=self.label)
+
+        # Check calls
+        self.assertEqual(mock_call.call_count, 2)
+        self.assertEqual(mock_call.call_args_list[0][1]['to'],
+            self.contact1.phone)
+        self.assertEqual(mock_call.call_args_list[1][1]['to'],
+            self.contact2.phone)
+
+        # Check texts
+        self.assertEqual(mock_text.call_count, 1)
+        self.assertEqual(mock_text.call_args[1]['to'],
+            self.contact2.phone)
+        # Check URLs
+        urlparams = {
+            'sid': convert_superevent_id_to_speech(
+                self.superevent.superevent_id),
+            'label_lower': self.label.name.lower(),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['superevent']['label_added'],
+            params=urlencode(urlparams))
+        # Check URL
+        for call_args in mock_call.call_args_list:
+            self.assertEqual(call_args[1]['url'], expected_url)
+
+    def test_label_removed_superevent(self, mock_call, mock_text):
+        """Test issuing phone alerts for label_removed superevent alert"""
+        issue_phone_alerts(self.superevent, 'label_removed', self.contacts,
+            label=self.label)
+
+        # Check calls
+        self.assertEqual(mock_call.call_count, 2)
+        self.assertEqual(mock_call.call_args_list[0][1]['to'],
+            self.contact1.phone)
+        self.assertEqual(mock_call.call_args_list[1][1]['to'],
+            self.contact2.phone)
+
+        # Check texts
+        self.assertEqual(mock_text.call_count, 1)
+        self.assertEqual(mock_text.call_args[1]['to'],
+            self.contact2.phone)
+        # Check URLs
+        urlparams = {
+            'sid': convert_superevent_id_to_speech(
+                self.superevent.superevent_id),
+            'label_lower': self.label.name.lower(),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['superevent']['label_removed'],
+            params=urlencode(urlparams))
+        # Check URL
+        for call_args in mock_call.call_args_list:
+            self.assertEqual(call_args[1]['url'], expected_url)
+
+    def test_new_event(self, mock_call, mock_text):
+        """Test issuing phone alerts for new event alert"""
+        issue_phone_alerts(self.event, 'new', self.contacts)
+
+        # Check calls
+        self.assertEqual(mock_call.call_count, 2)
+        self.assertEqual(mock_call.call_args_list[0][1]['to'],
+            self.contact1.phone)
+        self.assertEqual(mock_call.call_args_list[1][1]['to'],
+            self.contact2.phone)
+
+        # Check texts
+        self.assertEqual(mock_text.call_count, 1)
+        self.assertEqual(mock_text.call_args[1]['to'],
+            self.contact2.phone)
+
+        # Check URLs
+        urlparams = {
+            'graceid': self.event.graceid,
+            'pipeline': self.event.pipeline.name,
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['event']['new'],
+            params=urlencode(urlparams))
+        # Check URL
+        for call_args in mock_call.call_args_list:
+            self.assertEqual(call_args[1]['url'], expected_url)
+
+    def test_update_event(self, mock_call, mock_text):
+        """Test issuing phone alerts for event update alert"""
+        issue_phone_alerts(self.event, 'update', self.contacts)
+
+        # Check calls
+        self.assertEqual(mock_call.call_count, 2)
+        self.assertEqual(mock_call.call_args_list[0][1]['to'],
+            self.contact1.phone)
+        self.assertEqual(mock_call.call_args_list[1][1]['to'],
+            self.contact2.phone)
+
+        # Check texts
+        self.assertEqual(mock_text.call_count, 1)
+        self.assertEqual(mock_text.call_args[1]['to'],
+            self.contact2.phone)
+
+        # Check URLs
+        urlparams = {
+            'graceid': self.event.graceid,
+            'pipeline': self.event.pipeline.name,
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['event']['update'],
+            params=urlencode(urlparams))
+        # Check URL
+        for call_args in mock_call.call_args_list:
+            self.assertEqual(call_args[1]['url'], expected_url)
+
+    def test_label_added_event(self, mock_call, mock_text):
+        """Test issuing phone alerts for label_added event alert"""
+        issue_phone_alerts(self.event, 'label_added', self.contacts,
+            self.label)
+
+        # Check calls
+        self.assertEqual(mock_call.call_count, 2)
+        self.assertEqual(mock_call.call_args_list[0][1]['to'],
+            self.contact1.phone)
+        self.assertEqual(mock_call.call_args_list[1][1]['to'],
+            self.contact2.phone)
+
+        # Check texts
+        self.assertEqual(mock_text.call_count, 1)
+        self.assertEqual(mock_text.call_args[1]['to'],
+            self.contact2.phone)
+
+        # Check URLs
+        urlparams = {
+            'graceid': self.event.graceid,
+            'pipeline': self.event.pipeline.name,
+            'label_lower': self.label.name.lower(),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['event']['label_added'],
+            params=urlencode(urlparams))
+        # Check URL
+        for call_args in mock_call.call_args_list:
+            self.assertEqual(call_args[1]['url'], expected_url)
+
+    def test_label_removed_event(self, mock_call, mock_text):
+        """Test issuing phone alerts for label_removed event alert"""
+        issue_phone_alerts(self.event, 'label_removed', self.contacts,
+            self.label)
+
+        # Check calls
+        self.assertEqual(mock_call.call_count, 2)
+        self.assertEqual(mock_call.call_args_list[0][1]['to'],
+            self.contact1.phone)
+        self.assertEqual(mock_call.call_args_list[1][1]['to'],
+            self.contact2.phone)
+
+        # Check texts
+        self.assertEqual(mock_text.call_count, 1)
+        self.assertEqual(mock_text.call_args[1]['to'],
+            self.contact2.phone)
+
+        # Check URLs
+        urlparams = {
+            'graceid': self.event.graceid,
+            'pipeline': self.event.pipeline.name,
+            'label_lower': self.label.name.lower(),
+        }
+        expected_url = '{base}{twiml_bin}?{params}'.format(
+            base=settings.TWIML_BASE_URL,
+            twiml_bin=settings.TWIML_BIN['event']['label_removed'],
+            params=urlencode(urlparams))
+        # Check URL
+        for call_args in mock_call.call_args_list:
+            self.assertEqual(call_args[1]['url'], expected_url)
diff --git a/gracedb/alerts/tests/test_recipients.py b/gracedb/alerts/tests/test_recipients.py
new file mode 100644
index 0000000000000000000000000000000000000000..22b83dbcde7062a62011e4e6c74d0fa98a87fdfd
--- /dev/null
+++ b/gracedb/alerts/tests/test_recipients.py
@@ -0,0 +1,2368 @@
+import mock
+
+from django.test import override_settings
+
+from alerts.issuers.events import EventAlertIssuer, EventLabelAlertIssuer
+from alerts.issuers.superevents import (
+    SupereventAlertIssuer, SupereventLabelAlertIssuer,
+)
+from alerts.models import Contact, Notification
+from core.tests.utils import GraceDbTestBase
+from events.models import Label, Group, Pipeline, Search
+from events.tests.mixins import EventCreateMixin
+from superevents.tests.mixins import SupereventCreateMixin
+
+
+@override_settings(
+    SEND_XMPP_ALERTS=False,
+    SEND_EMAIL_ALERTS=True,
+    SEND_PHONE_ALERTS=True,
+)
+@mock.patch('alerts.main.issue_phone_alerts')
+@mock.patch('alerts.main.issue_email_alerts')
+class TestEventRecipients(GraceDbTestBase, EventCreateMixin):
+
+    @classmethod
+    def setUpTestData(cls):
+        super(TestEventRecipients, cls).setUpTestData()
+
+        # Create an event
+        cls.event = cls.create_event('fake_group', 'fake_pipeline',
+            'fake_search', user=cls.internal_user)
+
+        # Create a bunch of notifications
+        cls.far_thresh = 0.01
+        cls.notification_dict = {}
+        cls.notification_dict['basic'] = Notification.objects.create(
+            user=cls.internal_user, description='basic',
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['far'] = Notification.objects.create(
+            user=cls.internal_user, description='far',
+            far_threshold=cls.far_thresh,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['nscand'] = Notification.objects.create(
+            user=cls.internal_user, description='nscand',
+            ns_candidate=True,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['far_nscand'] = Notification.objects.create(
+            user=cls.internal_user, description='far_nscand',
+            far_threshold=cls.far_thresh, ns_candidate=True,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['labels'] = Notification.objects.create(
+            user=cls.internal_user, description='labels',
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['far_labels'] = Notification.objects.create(
+            user=cls.internal_user, description='far_labels',
+            far_threshold=cls.far_thresh,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['nscand_labels'] = Notification.objects.create(
+            user=cls.internal_user, description='nscand_labels',
+            ns_candidate=True,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['far_nscand_labels'] = \
+            Notification.objects.create(user=cls.internal_user,
+            description='far_nscand_labels', far_threshold=cls.far_thresh,
+            ns_candidate=True,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['labelq'] = Notification.objects.create(
+            label_query='TEST_LABEL3 & ~TEST_LABEL4',
+            user=cls.internal_user, description='labelq',
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['far_labelq'] = Notification.objects.create(
+            user=cls.internal_user, description='far_labelq',
+            far_threshold=cls.far_thresh,
+            label_query='TEST_LABEL3 & ~TEST_LABEL4',
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['nscand_labelq'] = Notification.objects.create(
+            user=cls.internal_user, description='nscand_labelq',
+            ns_candidate=True, label_query='TEST_LABEL3 & ~TEST_LABEL4',
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['far_nscand_labelq'] = \
+            Notification.objects.create(user=cls.internal_user,
+            description='far_nscand_labelq', far_threshold=cls.far_thresh,
+            ns_candidate=True, label_query='TEST_LABEL3 & ~TEST_LABEL4',
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+
+        # Group-pipeline-search notification
+        group, _ = Group.objects.get_or_create(name='TEST_GROUP')
+        pipeline, _ = Pipeline.objects.get_or_create(name='TEST_PIPELINE')
+        search, _ = Search.objects.get_or_create(name='TEST_SEARCH')
+        cls.notification_dict['gps'] = Notification.objects.create(
+            user=cls.internal_user, description='gps',
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.notification_dict['gps'].groups.add(group)
+        cls.notification_dict['gps'].pipelines.add(pipeline)
+        cls.notification_dict['gps'].searches.add(search)
+
+        # Add label stuff
+        cls.label1, _ = Label.objects.get_or_create(name='TEST_LABEL1')
+        cls.label2, _ = Label.objects.get_or_create(name='TEST_LABEL2')
+        cls.label3, _ = Label.objects.get_or_create(name='TEST_LABEL3')
+        cls.label4, _ = Label.objects.get_or_create(name='TEST_LABEL4')
+        for k in cls.notification_dict:
+            if 'labels' in k:
+                cls.notification_dict[k].labels.add(cls.label1)
+                cls.notification_dict[k].labels.add(cls.label2)
+            elif 'labelq' in k:
+                cls.notification_dict[k].labels.add(cls.label3)
+                cls.notification_dict[k].labels.add(cls.label4)
+
+        # Create an email and phone contact for each notification
+        for k in cls.notification_dict:
+            n = cls.notification_dict[k]
+            n.contacts.create(user=cls.internal_user,
+                description=n.description, email='test@test.com',
+                verified=True)
+            n.contacts.create(user=cls.internal_user,
+                description=n.description, phone='12345678901',
+                phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+    def test_new(self, email_mock, phone_mock):
+        """Test alerts for event creation - no FAR, no NSCAND"""
+        EventAlertIssuer(self.event, alert_type='new').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Should just be the "basic" notification being triggered
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(phone_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'basic')
+        self.assertEqual(phone_recips.first().description, 'basic')
+
+    def test_new_with_far(self, email_mock, phone_mock):
+        """Test alerts for event creation with FAR"""
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Create a new notification with too low of a FAR threshold
+        n = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n.contacts.create(user=self.internal_user,
+            description=n.description, email='test@test.com',
+            verified=True)
+        n.contacts.create(user=self.internal_user,
+            description=n.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='new').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic and FAR alerts should be triggered, but not the "new" FAR
+        # one we defined with a really low threshold
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['basic', 'far'])
+
+        # Ensure that "new" FAR alert is not in the lists
+        self.assertFalse(email_recips.filter(
+            description=n.description).exists())
+        self.assertFalse(phone_recips.filter(
+            description=n.description).exists())
+
+
+    def test_new_with_nscand(self, email_mock, phone_mock):
+        """Test alerts for event creation with NS candidate"""
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='new').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only basic and NSCAND alerts should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['basic', 'nscand'])
+
+    def test_new_with_far_nscand(self, email_mock, phone_mock):
+        """Test alerts for event creation with FAR and NS candidate"""
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Create a new notification with too low of a FAR threshold
+        n = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n.contacts.create(user=self.internal_user,
+            description=n.description, email='test@test.com',
+            verified=True)
+        n.contacts.create(user=self.internal_user,
+            description=n.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='new').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic and FAR alerts should be triggered, but not the "new" FAR
+        # one we defined with a really low threshold
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 4)
+            for r in recips:
+                self.assertIn(r.description,
+                    ['basic', 'far', 'nscand', 'far_nscand'])
+
+        # Ensure that "new" FAR alert is not in the lists
+        self.assertFalse(email_recips.filter(
+            description=n.description).exists())
+        self.assertFalse(phone_recips.filter(
+            description=n.description).exists())
+
+    def test_new_with_group_pipeline_search(self, email_mock, phone_mock):
+        """Test alerts for event creation which match group-pipeline-search"""
+        # Change event group, pipeline, search
+        self.event.group = self.notification_dict['gps'].groups.first()
+        self.event.pipeline = self.notification_dict['gps'].pipelines.first()
+        self.event.search = self.notification_dict['gps'].searches.first()
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='new').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic alert and 'gps' alert (which matches group-pipeline-search)
+        # should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['basic', 'gps'])
+
+    def test_update_with_no_change(self, email_mock, phone_mock):
+        """Test alerts for event update with no FAR or NSCAND change"""
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts()
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_update_with_same_far(self, email_mock, phone_mock):
+        """Test alerts for event update with no FAR or NSCAND change"""
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_far=self.event.far)
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_update_with_lower_far(self, email_mock, phone_mock):
+        """Test alerts for event update with lower FAR"""
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Create a new notification with too low of a FAR threshold
+        n_low = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, email='test@test.com',
+            verified=True)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Create a new notification with too high of a FAR threshold
+        n_high = Notification.objects.create(user=self.internal_user,
+            description='far_high', far_threshold=1,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, email='test@test.com',
+            verified=True)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'far' alert should be triggered and not the
+        # n_low or n_high that we defined here
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(phone_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'far')
+        self.assertEqual(phone_recips.first().description, 'far')
+
+    def test_update_with_nscand(self, email_mock, phone_mock):
+        """Test alerts for event update with NSCAND trigger"""
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_nscand=False)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'nscand' alert should be triggered
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(phone_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'nscand')
+        self.assertEqual(phone_recips.first().description, 'nscand')
+
+    def test_update_with_far_no_prev_far(self, email_mock, phone_mock):
+        """Test alerts for event update with new FAR, no previous FAR"""
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Create a new notification with too low of a FAR threshold
+        n_low = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, email='test@test.com',
+            verified=True)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Create a new notification with too high of a FAR threshold
+        n_high = Notification.objects.create(user=self.internal_user,
+            description='far_high', far_threshold=1,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, email='test@test.com',
+            verified=True)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_far=None)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'far' alert and 'far_high' should be in here
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'far_high'])
+
+    def test_update_with_far_no_old_far_passed(self, email_mock,
+        phone_mock):
+        """Test alerts for event update with new FAR, no old FAR passed"""
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Create a new notification with too low of a FAR threshold
+        n_low = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, email='test@test.com',
+            verified=True)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Create a new notification with too high of a FAR threshold
+        n_high = Notification.objects.create(user=self.internal_user,
+            description='far_high', far_threshold=1,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, email='test@test.com',
+            verified=True)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'far' alert and 'far_high' should be in here
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'far_high'])
+
+    def test_update_with_nscand_still_true(self, email_mock, phone_mock):
+        """Test alerts for event update where NSCAND was and is true"""
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_nscand=True)
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_update_with_lower_far_nscand(self, email_mock, phone_mock):
+        """Test alerts for event update with lower FAR and NSCAND"""
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Create a new notification with too low of a FAR threshold
+        n_low = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, email='test@test.com',
+            verified=True)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Create a new notification with too high of a FAR threshold
+        n_high = Notification.objects.create(user=self.internal_user,
+            description='far_high', far_threshold=1,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, email='test@test.com',
+            verified=True)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_far=self.far_thresh*2, old_nscand=False)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'far' alert should be triggered and not the
+        # n_low or n_high that we defined here
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 3)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'nscand', 'far_nscand'])
+
+    def test_labeled_update_with_lower_far(self, email_mock, phone_mock):
+        """
+        Test alerts for an event which has labels and updated with lower FAR
+        """
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add label1 to event - not enough to match labels yet
+        self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # 'far' with no label requirements should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['far'])
+
+        # Add label2 to event - should match now
+        self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # 'far' with no label requirements and 'far_labels' should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'far_labels'])
+
+    def test_labelq_update_with_lower_far(self, email_mock, phone_mock):
+        """Test alerts for event update with lower FAR and label_query match"""
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add labels to event - this set of labels shouldn't match label query
+        self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        self.event.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # 'far' with no label requirements should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['far'])
+
+        # Remove label4 and the label query should match
+        self.event.labelling_set.get(label=self.label4).delete()
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # 'far' with no label requirements should be triggered
+        # 'far_labelq' (with label query) should now be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'far_labelq'])
+
+    def test_update_match_group_pipeline_search(self, email_mock, phone_mock):
+        """Test alerts for event update which match group-pipeline-search"""
+        # Change event group, pipeline, search
+        self.event.group = self.notification_dict['gps'].groups.first()
+        self.event.pipeline = self.notification_dict['gps'].pipelines.first()
+        self.event.search = self.notification_dict['gps'].searches.first()
+
+        # Change GPS notification to have a far_threshold
+        self.notification_dict['gps'].far_threshold = self.far_thresh
+
+        # Add FAR to event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Issue alerts
+        EventAlertIssuer(self.event, alert_type='update').issue_alerts(
+            old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # 'far' with no label requirements should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'far_gps'])
+
+    def test_label_added(self, email_mock, phone_mock):
+        """Test adding label alert for event"""
+        # Add label1 to event - this shouldn't match any queries
+        lab1 = self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab1, alert_type='label_added').issue_alerts()
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+        # Add label2 to event
+        lab2 = self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab2, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only 'labels' trigger should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labels'])
+
+        # Issue alert for label 1 now that it has both labels; should be
+        # the same result
+        EventLabelAlertIssuer(lab1, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only 'labels' trigger should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labels'])
+
+    def test_label_added_extra_labels(self, email_mock, phone_mock):
+        """Test adding label alert for event with other labels"""
+        # Add label 1, 2, and 4 to event
+        lab1 = self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab4 = self.event.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+        lab2 = self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts for label 2
+        EventLabelAlertIssuer(lab2, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # The 'labels' trigger only requires label1 and label2, but it should
+        # still trigger on label2 addition even though label4 is also present
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labels'])
+
+    def test_label_added_with_far(self, email_mock, phone_mock):
+        """Test adding label alert for event with FAR"""
+        # Set event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add label1 and label2 to event
+        lab1 = self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab2, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labels' trigger and labels w/ FAR ('far_labels') trigger
+        # should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labels', 'far_labels'])
+
+    def test_label_added_with_nscand(self, email_mock, phone_mock):
+        """Test adding label alert for event with NSCAND"""
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add label1 and label2 to event
+        lab1 = self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab2, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labels' trigger and labels w/ NSCAND ('nscand_labels') trigger
+        # should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labels', 'nscand_labels'])
+
+    def test_label_added_with_far_nscand(self, email_mock, phone_mock):
+        """Test adding label alert for event with FAR threshold and NSCAND"""
+        # Set event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add label1 and label2 to event
+        lab1 = self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab2, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labels' trigger, labels with FAR, labels with NSCAND, and
+        # labels with FAR and NSCAND should all be triggered
+        # should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 4)
+            for r in recips:
+                self.assertIn(r.description, ['labels', 'far_labels',
+                    'nscand_labels', 'far_nscand_labels'])
+
+    def test_label_added_with_gps(self, email_mock, phone_mock):
+        """
+        Test adding label alert for event with group-pipeline-search
+        requirements
+        """
+        # Change event group, pipeline, search
+        self.event.group = self.notification_dict['gps'].groups.first()
+        self.event.pipeline = self.notification_dict['gps'].pipelines.first()
+        self.event.search = self.notification_dict['gps'].searches.first()
+
+        # Add label 1 and 2 to notification
+        self.notification_dict['gps'].labels.add(self.label1)
+        self.notification_dict['gps'].labels.add(self.label2)
+
+        # Add label 1 and 2 to event
+        lab1 = self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab2, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labels' trigger and 'gps' trigger should match
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labels', 'gps'])
+
+    def test_label_added_labelq(self, email_mock, phone_mock):
+        """Test adding label alert for event with label query match"""
+        # Add label3 and label4 to event
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.event.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab4, alert_type='label_added').issue_alerts()
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+        # Remove label4 and the label query should match
+        self.event.labelling_set.get(label=self.label4).delete()
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab3, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic label_query trigger should be only match
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labelq'])
+
+    def test_label_added_labelq_with_far(self, email_mock, phone_mock):
+        """Test adding label alert for event with FAR and label query match"""
+        # Set event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add label3 to event
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab3, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labelq' trigger and label query w/ FAR ('far_labelq') trigger
+        # should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'far_labelq'])
+
+    def test_label_added_labelq_with_nscand(self, email_mock, phone_mock):
+        """
+        Test adding label alert for event with NSCAND and label query match
+        """
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add label3 to event
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab3, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labelq' trigger and label query w/ NSCAND ('nscand_labelq')
+        # trigger should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'nscand_labelq'])
+
+    def test_label_added_labelq_with_far_nscand(self, email_mock, phone_mock):
+        """
+        Test adding label alert for event with FAR threshold and NSCAND and
+        label query match
+        """
+        # Set event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add label3 to event
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab3, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labelq' trigger, label query with FAR, label query with
+        # NSCAND, and label query with FAR and NSCAND should all be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 4)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'far_labelq',
+                    'nscand_labelq', 'far_nscand_labelq'])
+
+    def test_label_added_labelq_with_gps(self, email_mock, phone_mock):
+        """
+        Test adding label alert for event with group-pipeline-search
+        requirements and label query match
+        """
+        # Change event group, pipeline, search
+        self.event.group = self.notification_dict['gps'].groups.first()
+        self.event.pipeline = self.notification_dict['gps'].pipelines.first()
+        self.event.search = self.notification_dict['gps'].searches.first()
+
+        # Add label query to notification
+        self.notification_dict['gps'].label_query = '{l3} & ~{l4}'.format(
+            l3=self.label3.name, l4=self.label4.name)
+        self.notification_dict['gps'].labels.add(self.label3)
+        self.notification_dict['gps'].labels.add(self.label4)
+        self.notification_dict['gps'].save()
+
+        # Add label3 to event 
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab3, alert_type='label_added').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labelq' trigger and 'gps' trigger should match
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'gps'])
+
+    def test_label_removed_match_labels(self, email_mock, phone_mock):
+        """
+        Test label_removed alert for event where only triggers with
+        labels, not label queries are matched
+        """
+        # Add labels 1 and 2
+        lab1 = self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Remove label 2 and issue alert for label 2 removal
+        self.event.labelling_set.get(label=self.label2).delete()
+        EventLabelAlertIssuer(lab2, alert_type='label_removed').issue_alerts()
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+        # Add labels 2 and 3
+        lab2 = self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+
+        # Remove label 3 and issue alert
+        self.event.labelling_set.get(label=self.label3).delete()
+        EventLabelAlertIssuer(lab3, alert_type='label_removed').issue_alerts()
+
+        # Although the event has label1 and label2 and matches the
+        # 'labels' trigger, this trigger was matched last time either
+        # label1 or label2 was added, and label3 being removed doesn't
+        # change that (i.e., label_removed alerts only trigger notifications
+        # with label queries)
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_label_removed(self, email_mock, phone_mock):
+        """Test label_removed alert for event with label query match"""
+        # Add labels 3 and 4
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.event.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Remove label 4 and issue alert for label 4 removal
+        self.event.labelling_set.get(label=self.label4).delete()
+        EventLabelAlertIssuer(lab4, alert_type='label_removed').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only 'labelq' trigger should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labelq'])
+
+    def test_label_removed_with_far(self, email_mock, phone_mock):
+        """
+        Test label_removed alert for event with label query match and
+        FAR threshold match"""
+        # Set event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add labels 3 and 4
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.event.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Remove label 4 and issue alert for label 4 removal
+        self.event.labelling_set.get(label=self.label4).delete()
+        EventLabelAlertIssuer(lab4, alert_type='label_removed').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only 'labelq' and label_query with FAR should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'far_labelq'])
+
+    def test_label_removed_with_nscand(self, email_mock, phone_mock):
+        """Test label_removed alert with label query match and NSCAND match"""
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add labels 3 and 4
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.event.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Remove label 4 and issue alert for label 4 removal
+        self.event.labelling_set.get(label=self.label4).delete()
+        EventLabelAlertIssuer(lab4, alert_type='label_removed').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic label query trigger and label query w/ NSCAND should be
+        # triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'nscand_labelq'])
+
+    def test_label_removed_with_far_nscand(self, email_mock, phone_mock):
+        """
+        Test label_removed alert for event with FAR threshold and NSCAND
+        and label query match
+        """
+        # Set event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add NSCAND to event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add labels 3 and 4
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.event.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Remove label 4 and issue alert for label 4 removal
+        self.event.labelling_set.get(label=self.label4).delete()
+        EventLabelAlertIssuer(lab4, alert_type='label_removed').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Should match basic label query trigger, label query with FAR,
+        # label query with NSCAND, and label query with FAR and NSCAND
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 4)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'far_labelq',
+                    'nscand_labelq', 'far_nscand_labelq'])
+
+    def test_label_removed_with_gps(self, email_mock, phone_mock):
+        """
+        Test label_removed alert for event with group-pipeline-search
+        requirements and label query match
+        """
+        # Change event group, pipeline, search
+        self.event.group = self.notification_dict['gps'].groups.first()
+        self.event.pipeline = self.notification_dict['gps'].pipelines.first()
+        self.event.search = self.notification_dict['gps'].searches.first()
+
+        # Add labels 3 and 4
+        lab3 = self.event.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.event.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Add label query to 'gps' trigger
+        self.notification_dict['gps'].label_query = '{l3} & ~{l4}'.format(
+            l3=self.label3.name, l4=self.label4.name)
+        self.notification_dict['gps'].labels.add(self.label3)
+        self.notification_dict['gps'].labels.add(self.label4)
+        self.notification_dict['gps'].save()
+
+        # Remove label 4 and issue alert for label 4 removal
+        self.event.labelling_set.get(label=self.label4).delete()
+        EventLabelAlertIssuer(lab4, alert_type='label_removed').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labels' trigger and 'gps' trigger should match
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'gps'])
+
+
+@override_settings(
+    SEND_XMPP_ALERTS=False,
+    SEND_EMAIL_ALERTS=True,
+    SEND_PHONE_ALERTS=True,
+)
+@mock.patch('alerts.main.issue_phone_alerts')
+@mock.patch('alerts.main.issue_email_alerts')
+class TestSupereventRecipients(GraceDbTestBase, SupereventCreateMixin):
+
+    @classmethod
+    def setUpTestData(cls):
+        super(TestSupereventRecipients, cls).setUpTestData()
+
+        # Create a superevent
+        cls.superevent = cls.create_superevent(cls.internal_user,
+            'fake_group', 'fake_pipeline', 'fake_search')
+
+        # References to cls/self.event will refer to the superevent's
+        # preferred event
+        cls.event = cls.superevent.preferred_event
+
+        # Create a bunch of notifications
+        cls.far_thresh = 0.01
+        cls.notification_dict = {}
+        cls.notification_dict['basic'] = Notification.objects.create(
+            user=cls.internal_user, description='basic',
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['far'] = Notification.objects.create(
+            user=cls.internal_user, description='far',
+            far_threshold=cls.far_thresh,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['nscand'] = Notification.objects.create(
+            user=cls.internal_user, description='nscand',
+            ns_candidate=True,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['far_nscand'] = Notification.objects.create(
+            user=cls.internal_user, description='far_nscand',
+            far_threshold=cls.far_thresh, ns_candidate=True,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['labels'] = Notification.objects.create(
+            user=cls.internal_user, description='labels',
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['far_labels'] = Notification.objects.create(
+            user=cls.internal_user, description='far_labels',
+            far_threshold=cls.far_thresh,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['nscand_labels'] = Notification.objects.create(
+            user=cls.internal_user, description='nscand_labels',
+            ns_candidate=True,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['far_nscand_labels'] = \
+            Notification.objects.create(user=cls.internal_user,
+            description='far_nscand_labels', far_threshold=cls.far_thresh,
+            ns_candidate=True,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['labelq'] = Notification.objects.create(
+            label_query='TEST_LABEL3 & ~TEST_LABEL4',
+            user=cls.internal_user, description='labelq',
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['far_labelq'] = Notification.objects.create(
+            user=cls.internal_user, description='far_labelq',
+            far_threshold=cls.far_thresh,
+            label_query='TEST_LABEL3 & ~TEST_LABEL4',
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['nscand_labelq'] = Notification.objects.create(
+            user=cls.internal_user, description='nscand_labelq',
+            ns_candidate=True, label_query='TEST_LABEL3 & ~TEST_LABEL4',
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.notification_dict['far_nscand_labelq'] = \
+            Notification.objects.create(user=cls.internal_user,
+            description='far_nscand_labelq', far_threshold=cls.far_thresh,
+            ns_candidate=True, label_query='TEST_LABEL3 & ~TEST_LABEL4',
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+
+        # Add label stuff
+        cls.label1, _ = Label.objects.get_or_create(name='TEST_LABEL1')
+        cls.label2, _ = Label.objects.get_or_create(name='TEST_LABEL2')
+        cls.label3, _ = Label.objects.get_or_create(name='TEST_LABEL3')
+        cls.label4, _ = Label.objects.get_or_create(name='TEST_LABEL4')
+        for k in cls.notification_dict:
+            if 'labels' in k:
+                cls.notification_dict[k].labels.add(cls.label1)
+                cls.notification_dict[k].labels.add(cls.label2)
+            elif 'labelq' in k:
+                cls.notification_dict[k].labels.add(cls.label3)
+                cls.notification_dict[k].labels.add(cls.label4)
+
+        # Create an email and phone contact for each notification
+        for k in cls.notification_dict:
+            n = cls.notification_dict[k]
+            n.contacts.create(user=cls.internal_user,
+                description=n.description, email='test@test.com',
+                verified=True)
+            n.contacts.create(user=cls.internal_user,
+                description=n.description, phone='12345678901',
+                phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+    def test_new(self, email_mock, phone_mock):
+        """Test alerts for superevent creation - no FAR, no NSCAND"""
+        SupereventAlertIssuer(self.superevent, alert_type='new').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Should just be the "basic" notification being triggered
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(phone_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'basic')
+        self.assertEqual(phone_recips.first().description, 'basic')
+
+    def test_new_with_far(self, email_mock, phone_mock):
+        """Test alerts for superevent creation with FAR"""
+        # Add FAR to preferred event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Create a new notification with too low of a FAR threshold
+        n = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n.contacts.create(user=self.internal_user,
+            description=n.description, email='test@test.com',
+            verified=True)
+        n.contacts.create(user=self.internal_user,
+            description=n.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='new').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic and FAR alerts should be triggered, but not the "new" FAR
+        # one we defined with a really low threshold
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['basic', 'far'])
+
+        # Ensure that "new" FAR alert is not in the lists
+        self.assertFalse(email_recips.filter(
+            description=n.description).exists())
+        self.assertFalse(phone_recips.filter(
+            description=n.description).exists())
+
+    def test_new_with_nscand(self, email_mock, phone_mock):
+        """Test alerts for superevent creation with NS candidate"""
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='new').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only basic and NSCAND alerts should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['basic', 'nscand'])
+
+    def test_new_with_far_nscand(self, email_mock, phone_mock):
+        """Test alerts for superevent creation with FAR and NS candidate"""
+        # Add FAR to preferred event
+        self.event.far = 1e-10
+        self.event.save()
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Create a new notification with too low of a FAR threshold
+        n = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n.contacts.create(user=self.internal_user,
+            description=n.description, email='test@test.com',
+            verified=True)
+        n.contacts.create(user=self.internal_user,
+            description=n.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='new').issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic and FAR alerts should be triggered, but not the "new" FAR
+        # one we defined with a really low threshold
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 4)
+            for r in recips:
+                self.assertIn(r.description,
+                    ['basic', 'far', 'nscand', 'far_nscand'])
+
+        # Ensure that "new" FAR alert is not in the lists
+        self.assertFalse(email_recips.filter(
+            description=n.description).exists())
+        self.assertFalse(phone_recips.filter(
+            description=n.description).exists())
+
+    def test_update_with_no_change(self, email_mock, phone_mock):
+        """Test alerts for superevent update with no FAR or NSCAND change"""
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts()
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_update_with_same_far(self, email_mock, phone_mock):
+        """Test alerts for superevent update with no FAR or NSCAND change"""
+        # Add FAR to preferred event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=self.event.far)
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_update_with_lower_far(self, email_mock, phone_mock):
+        """Test alerts for superevent update with lower FAR"""
+        # Add FAR to preferred event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Create a new notification with too low of a FAR threshold
+        n_low = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, email='test@test.com',
+            verified=True)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Create a new notification with too high of a FAR threshold
+        n_high = Notification.objects.create(user=self.internal_user,
+            description='far_high', far_threshold=1,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, email='test@test.com',
+            verified=True)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'far' alert should be triggered and not the
+        # n_low or n_high that we defined here
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(phone_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'far')
+        self.assertEqual(phone_recips.first().description, 'far')
+
+    def test_update_with_new_preferred_event_no_far(self, email_mock,
+        phone_mock):
+        """
+        Test alerts for superevent update with new preferred_event with
+        no FAR
+        """
+        # Update preferred event
+        ev = self.create_event('fake_group', 'fake_pipeline',
+            'fake_search', user=self.internal_user)
+        self.superevent.preferred_event = ev
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=self.event.far)
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_update_with_new_preferred_event_same_far(self, email_mock,
+        phone_mock):
+        """
+        Test alerts for superevent update with new preferred_event with
+        same FAR
+        """
+        # Update preferred event
+        ev = self.create_event('fake_group', 'fake_pipeline',
+            'fake_search', user=self.internal_user)
+        ev.far = 1e-10
+        ev.save()
+        self.superevent.preferred_event = ev
+
+        # Add FAR to old preferred event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=self.event.far)
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_update_with_nscand(self, email_mock, phone_mock):
+        """Test alerts for superevent update with NSCAND trigger"""
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_nscand=False)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'nscand' alert should be triggered
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(phone_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'nscand')
+        self.assertEqual(phone_recips.first().description, 'nscand')
+
+    def test_update_with_far_no_prev_far(self, email_mock, phone_mock):
+        """Test alerts for superevent update with new FAR, no previous FAR"""
+        # Add FAR to preferred event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Create a new notification with too low of a FAR threshold
+        n_low = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, email='test@test.com',
+            verified=True)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Create a new notification with too high of a FAR threshold
+        n_high = Notification.objects.create(user=self.internal_user,
+            description='far_high', far_threshold=1,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, email='test@test.com',
+            verified=True)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=None)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'far' alert and 'far_high' should be in here
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'far_high'])
+
+    def test_update_with_far_no_old_far_passed(self, email_mock,
+        phone_mock):
+        """Test alerts for superevent update with new FAR, no old FAR passed"""
+        # Add FAR to preferred event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Create a new notification with too low of a FAR threshold
+        n_low = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, email='test@test.com',
+            verified=True)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Create a new notification with too high of a FAR threshold
+        n_high = Notification.objects.create(user=self.internal_user,
+            description='far_high', far_threshold=1,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, email='test@test.com',
+            verified=True)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'far' alert and 'far_high' should be in here
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'far_high'])
+
+    def test_update_with_nscand_still_true(self, email_mock, phone_mock):
+        """Test alerts for superevent update where NSCAND was and is true"""
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_nscand=True)
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_update_with_lower_far_nscand(self, email_mock, phone_mock):
+        """Test alerts for superevent update with lower FAR and NSCAND"""
+        # Add FAR to preferred event
+        self.event.far = 1e-10
+        self.event.save()
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Create a new notification with too low of a FAR threshold
+        n_low = Notification.objects.create(user=self.internal_user,
+            description='far_low', far_threshold=1e-20,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, email='test@test.com',
+            verified=True)
+        n_low.contacts.create(user=self.internal_user,
+            description=n_low.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Create a new notification with too high of a FAR threshold
+        n_high = Notification.objects.create(user=self.internal_user,
+            description='far_high', far_threshold=1,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, email='test@test.com',
+            verified=True)
+        n_high.contacts.create(user=self.internal_user,
+            description=n_high.description, phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=True)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=self.far_thresh*2, old_nscand=False)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only the original 'far' alert should be triggered and not the
+        # n_low or n_high that we defined here
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 3)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'nscand', 'far_nscand'])
+
+    def test_labeled_update_with_lower_far(self, email_mock, phone_mock):
+        """
+        Test alerts for a superevent which has labels and updated with lower
+        FAR
+        """
+        # Add FAR to preferred event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add label1 to superevent - not enough to match labels yet
+        self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # 'far' with no label requirements should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['far'])
+
+        # Add label2 to event - should match now
+        self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # 'far' with no label requirements and 'far_labels' should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'far_labels'])
+
+    def test_labelq_update_with_lower_far(self, email_mock, phone_mock):
+        """
+        Test alerts for superevent update with lower FAR and label_query
+        match
+        """
+        # Add FAR to preferred event
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add labels to superevent - this set of labels shouldn't match label
+        # query
+        self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        self.superevent.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # 'far' with no label requirements should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['far'])
+
+        # Remove label4 and the label query should match
+        self.superevent.labelling_set.get(label=self.label4).delete()
+
+        # Issue alerts
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=self.far_thresh*2)
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # 'far' with no label requirements should be triggered
+        # 'far_labelq' (with label query) should now be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['far', 'far_labelq'])
+
+    def test_label_added(self, email_mock, phone_mock):
+        """Test adding label alert for superevent"""
+        # Add label1 to superevent - this shouldn't match any queries
+        lab1 = self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab1, alert_type='label_added') \
+            .issue_alerts()
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+        # Add label2 to superevent
+        lab2 = self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab2, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only 'labels' trigger should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labels'])
+
+        # Issue alert for label 1 now that it has both labels; should be
+        # the same result
+        SupereventLabelAlertIssuer(lab1, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only 'labels' trigger should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labels'])
+
+    def test_label_added_extra_labels(self, email_mock, phone_mock):
+        """Test adding label alert for superevent with other labels"""
+        # Add label 1, 2, and 4 to superevent
+        lab1 = self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab4 = self.superevent.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+        lab2 = self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts for label 2
+        SupereventLabelAlertIssuer(lab2, alert_type='label_added')\
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # The 'labels' trigger only requires label1 and label2, but it should
+        # still trigger on label2 addition even though label4 is also present
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labels'])
+
+    def test_label_added_with_far(self, email_mock, phone_mock):
+        """Test adding label alert for superevent with FAR"""
+        # Set preferred event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add label1 and label2 to superevent
+        lab1 = self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab2, alert_type='label_added')\
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labels' trigger and labels w/ FAR ('far_labels') trigger
+        # should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labels', 'far_labels'])
+
+    def test_label_added_with_nscand(self, email_mock, phone_mock):
+        """Test adding label alert for superevent with NSCAND"""
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add label1 and label2 to event
+        lab1 = self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab2, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labels' trigger and labels w/ NSCAND ('nscand_labels') trigger
+        # should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labels', 'nscand_labels'])
+
+    def test_label_added_with_far_nscand(self, email_mock, phone_mock):
+        """
+        Test adding label alert for superevent with FAR threshold and NSCAND
+        """
+        # Set preferred event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add label1 and label2 to superevent
+        lab1 = self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab2, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labels' trigger, labels with FAR, labels with NSCAND, and
+        # labels with FAR and NSCAND should all be triggered
+        # should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 4)
+            for r in recips:
+                self.assertIn(r.description, ['labels', 'far_labels',
+                    'nscand_labels', 'far_nscand_labels'])
+
+    def test_label_added_labelq(self, email_mock, phone_mock):
+        """Test adding label alert for superevent with label query match"""
+        # Add label3 and label4 to superevent
+        lab3 = self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.superevent.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab4, alert_type='label_added') \
+            .issue_alerts()
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+        # Remove label4 and the label query should match
+        self.superevent.labelling_set.get(label=self.label4).delete()
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab3, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic label_query trigger should be only match
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labelq'])
+
+    def test_label_added_labelq_with_far(self, email_mock, phone_mock):
+        """
+        Test adding label alert for superevent with FAR and label query match
+        """
+        # Set preferred event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add label3 to superevent
+        lab3 = self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab3, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labelq' trigger and label query w/ FAR ('far_labelq') trigger
+        # should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'far_labelq'])
+
+    def test_label_added_labelq_with_nscand(self, email_mock, phone_mock):
+        """
+        Test adding label alert for superevent with NSCAND and label query
+        match
+        """
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add label3 to superevent
+        lab3 = self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab3, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labelq' trigger and label query w/ NSCAND ('nscand_labelq')
+        # trigger should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'nscand_labelq'])
+
+    def test_label_added_labelq_with_far_nscand(self, email_mock, phone_mock):
+        """
+        Test adding label alert for superevent with FAR threshold and NSCAND
+        and label query match
+        """
+        # Set preferred event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add label3 to superevent
+        lab3 = self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab3, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic 'labelq' trigger, label query with FAR, label query with
+        # NSCAND, and label query with FAR and NSCAND should all be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 4)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'far_labelq',
+                    'nscand_labelq', 'far_nscand_labelq'])
+
+    def test_label_removed_match_labels(self, email_mock, phone_mock):
+        """
+        Test label_removed alert for superevent where only triggers with
+        labels, not label queries are matched
+        """
+        # Add labels 1 and 2
+        lab1 = self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Remove label 2 and issue alert for label 2 removal
+        self.superevent.labelling_set.get(label=self.label2).delete()
+        SupereventLabelAlertIssuer(lab2, alert_type='label_removed') \
+            .issue_alerts()
+
+        # In this case, no recipients should match so the alert functions
+        # are not even called
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+        # Add labels 2 and 3
+        lab2 = self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+        lab3 = self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+
+        # Remove label 3 and issue alert
+        self.superevent.labelling_set.get(label=self.label3).delete()
+        SupereventLabelAlertIssuer(lab3, alert_type='label_removed') \
+            .issue_alerts()
+
+        # Although the event has label1 and label2 and matches the
+        # 'labels' trigger, this trigger was matched last time either
+        # label1 or label2 was added, and label3 being removed doesn't
+        # change that (i.e., label_removed alerts only trigger notifications
+        # with label queries)
+        email_mock.assert_not_called()
+        phone_mock.assert_not_called()
+
+    def test_label_removed(self, email_mock, phone_mock):
+        """Test label_removed alert for superevent with label query match"""
+        # Add labels 3 and 4
+        lab3 = self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.superevent.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Remove label 4 and issue alert for label 4 removal
+        self.superevent.labelling_set.get(label=self.label4).delete()
+        SupereventLabelAlertIssuer(lab4, alert_type='label_removed') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only 'labelq' trigger should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 1)
+            for r in recips:
+                self.assertIn(r.description, ['labelq'])
+
+    def test_label_removed_with_far(self, email_mock, phone_mock):
+        """
+        Test label_removed alert for superevent with label query match and
+        FAR threshold match"""
+        # Set preferred event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add labels 3 and 4
+        lab3 = self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.superevent.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Remove label 4 and issue alert for label 4 removal
+        self.superevent.labelling_set.get(label=self.label4).delete()
+        SupereventLabelAlertIssuer(lab4, alert_type='label_removed') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Only 'labelq' and label_query with FAR should be triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'far_labelq'])
+
+    def test_label_removed_with_nscand(self, email_mock, phone_mock):
+        """Test label_removed alert with label query match and NSCAND match"""
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add labels 3 and 4
+        lab3 = self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.superevent.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Remove label 4 and issue alert for label 4 removal
+        self.superevent.labelling_set.get(label=self.label4).delete()
+        SupereventLabelAlertIssuer(lab4, alert_type='label_removed') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Basic label query trigger and label query w/ NSCAND should be
+        # triggered
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 2)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'nscand_labelq'])
+
+    def test_label_removed_with_far_nscand(self, email_mock, phone_mock):
+        """
+        Test label_removed alert for superevent with FAR threshold and NSCAND
+        and label query match
+        """
+        # Set preferred event FAR
+        self.event.far = 1e-10
+        self.event.save()
+
+        # Add NSCAND to preferred event
+        self.event.singleinspiral_set.create(mass1=3, mass2=1)
+
+        # Add labels 3 and 4
+        lab3 = self.superevent.labelling_set.create(label=self.label3,
+            creator=self.internal_user)
+        lab4 = self.superevent.labelling_set.create(label=self.label4,
+            creator=self.internal_user)
+
+        # Remove label 4 and issue alert for label 4 removal
+        self.superevent.labelling_set.get(label=self.label4).delete()
+        SupereventLabelAlertIssuer(lab4, alert_type='label_removed') \
+            .issue_alerts()
+
+        # Check recipients passed to alert functions
+        email_recips = email_mock.call_args[0][2]
+        phone_recips = phone_mock.call_args[0][2]
+
+        # Should match basic label query trigger, label query with FAR,
+        # label query with NSCAND, and label query with FAR and NSCAND
+        for recips in [email_recips, phone_recips]:
+            self.assertEqual(recips.count(), 4)
+            for r in recips:
+                self.assertIn(r.description, ['labelq', 'far_labelq',
+                    'nscand_labelq', 'far_nscand_labelq'])
+
+
+@override_settings(
+    SEND_XMPP_ALERTS=False,
+    SEND_EMAIL_ALERTS=True,
+    SEND_PHONE_ALERTS=True,
+)
+@mock.patch('alerts.main.issue_phone_alerts')
+@mock.patch('alerts.main.issue_email_alerts')
+class TestSupereventUnverifiedRecipients(GraceDbTestBase, SupereventCreateMixin):
+
+    @classmethod
+    def setUpTestData(cls):
+        super(TestSupereventUnverifiedRecipients, cls).setUpTestData()
+
+        # Create a superevent
+        cls.superevent = cls.create_superevent(cls.internal_user,
+            'fake_group', 'fake_pipeline', 'fake_search')
+
+        # References to cls/self.event will refer to the superevent's
+        # preferred event
+        cls.event = cls.superevent.preferred_event
+        # Set FAR
+        cls.event.far = 1e-6
+
+        # Create labaels
+        cls.label1, _ = Label.objects.get_or_create(name='TEST_LABEL1')
+        cls.label2, _ = Label.objects.get_or_create(name='TEST_LABEL2')
+
+        # Create a basic notification and a label query notification
+        cls.notification = Notification.objects.create(
+            user=cls.internal_user, description='basic', far_threshold=0.01,
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.label_notification = Notification.objects.create(
+            user=cls.internal_user, description='labels',
+            category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
+        cls.label_notification.labels.add(cls.label1)
+        cls.label_notification.labels.add(cls.label2)
+        cls.label_notification.label_query = '{l1} & ~{l2}'.format(
+            l1=cls.label1.name, l2=cls.label2.name)
+        cls.label_notification.save()
+
+        # Create an email and phone contact for notification
+        cls.notification.contacts.create(user=cls.internal_user,
+            description='basic email', email='test@test.com',
+            verified=True)
+        cls.notification.contacts.create(user=cls.internal_user,
+            description='basic phone', phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=False)
+
+        # Create contacts for label_query notification
+        cls.label_notification.contacts.create(user=cls.internal_user,
+            description='label email', email='test@test.com',
+            verified=True)
+        cls.label_notification.contacts.create(user=cls.internal_user,
+            description='label phone', phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=False)
+
+    def test_new_superevent(self, email_mock, phone_mock):
+        """Test verified recipients for new superevent alert"""
+        SupereventAlertIssuer(self.superevent, alert_type='new').issue_alerts()
+
+        # Check recipients passed to email alert function
+        email_recips = email_mock.call_args[0][2]
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'basic email')
+
+        # Phone alerts should be called at all since there were no
+        # verified recipients
+        phone_mock.assert_not_called()
+
+    def test_update_superevent(self, email_mock, phone_mock):
+        """Test verified recipients for update superevent alert"""
+        SupereventAlertIssuer(self.superevent, alert_type='update') \
+            .issue_alerts(old_far=0.02)
+
+        # Check recipients passed to email alert function
+        email_recips = email_mock.call_args[0][2]
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'basic email')
+
+        # Phone alerts should be called at all since there were no
+        # verified recipients
+        phone_mock.assert_not_called()
+
+    def test_label_added_superevent(self, email_mock, phone_mock):
+        """Test verified recipients for label_added superevent alert"""
+        # Add label
+        lab1 = self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+
+        # Issue alerts
+        SupereventLabelAlertIssuer(lab1, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to email alert function
+        email_recips = email_mock.call_args[0][2]
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'label email')
+
+        # Phone alerts should be called at all since there were no
+        # verified recipients
+        phone_mock.assert_not_called()
+
+    def test_label_removed_superevent(self, email_mock, phone_mock):
+        """Test verified recipients for label_removed superevent alert"""
+        # Add labels
+        lab1 = self.superevent.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.superevent.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Remove label 2 and issue alerts
+        lab2.delete()
+        SupereventLabelAlertIssuer(lab2, alert_type='label_removed') \
+            .issue_alerts()
+
+        # Check recipients passed to email alert function
+        email_recips = email_mock.call_args[0][2]
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'label email')
+
+        # Phone alerts should be called at all since there were no
+        # verified recipients
+        phone_mock.assert_not_called()
+
+
+@override_settings(
+    SEND_XMPP_ALERTS=False,
+    SEND_EMAIL_ALERTS=True,
+    SEND_PHONE_ALERTS=True,
+)
+@mock.patch('alerts.main.issue_phone_alerts')
+@mock.patch('alerts.main.issue_email_alerts')
+class TestEventUnverifiedRecipients(GraceDbTestBase, EventCreateMixin):
+
+    @classmethod
+    def setUpTestData(cls):
+        super(TestEventUnverifiedRecipients, cls).setUpTestData()
+
+        # Create an event
+        cls.event = cls.create_event('fake_group', 'fake_pipeline', 
+            search_name='fake_search', user=cls.internal_user)
+        # Set FAR
+        cls.event.far = 1e-6
+
+        # Create labaels
+        cls.label1, _ = Label.objects.get_or_create(name='TEST_LABEL1')
+        cls.label2, _ = Label.objects.get_or_create(name='TEST_LABEL2')
+
+        # Create a basic notification and a label query notification
+        cls.notification = Notification.objects.create(
+            user=cls.internal_user, description='basic', far_threshold=0.01,
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.label_notification = Notification.objects.create(
+            user=cls.internal_user, description='labels',
+            category=Notification.NOTIFICATION_CATEGORY_EVENT)
+        cls.label_notification.labels.add(cls.label1)
+        cls.label_notification.labels.add(cls.label2)
+        cls.label_notification.label_query = '{l1} & ~{l2}'.format(
+            l1=cls.label1.name, l2=cls.label2.name)
+        cls.label_notification.save()
+
+        # Create an email and phone contact for notification
+        cls.notification.contacts.create(user=cls.internal_user,
+            description='basic email', email='test@test.com',
+            verified=True)
+        cls.notification.contacts.create(user=cls.internal_user,
+            description='basic phone', phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=False)
+
+        # Create contacts for label_query notification
+        cls.label_notification.contacts.create(user=cls.internal_user,
+            description='label email', email='test@test.com',
+            verified=True)
+        cls.label_notification.contacts.create(user=cls.internal_user,
+            description='label phone', phone='12345678901',
+            phone_method=Contact.CONTACT_PHONE_BOTH, verified=False)
+
+    def test_new_event(self, email_mock, phone_mock):
+        """Test verified recipients for new event alert"""
+        EventAlertIssuer(self.event, alert_type='new').issue_alerts()
+
+        # Check recipients passed to email alert function
+        email_recips = email_mock.call_args[0][2]
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'basic email')
+
+        # Phone alerts should be called at all since there were no
+        # verified recipients
+        phone_mock.assert_not_called()
+
+    def test_update_event(self, email_mock, phone_mock):
+        """Test verified recipients for update event alert"""
+        EventAlertIssuer(self.event, alert_type='update') \
+            .issue_alerts(old_far=0.02)
+
+        # Check recipients passed to email alert function
+        email_recips = email_mock.call_args[0][2]
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'basic email')
+
+        # Phone alerts should be called at all since there were no
+        # verified recipients
+        phone_mock.assert_not_called()
+
+    def test_label_added_event(self, email_mock, phone_mock):
+        """Test verified recipients for label_added event alert"""
+        # Add label
+        lab1 = self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+
+        # Issue alerts
+        EventLabelAlertIssuer(lab1, alert_type='label_added') \
+            .issue_alerts()
+
+        # Check recipients passed to email alert function
+        email_recips = email_mock.call_args[0][2]
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'label email')
+
+        # Phone alerts should be called at all since there were no
+        # verified recipients
+        phone_mock.assert_not_called()
+
+    def test_label_removed_event(self, email_mock, phone_mock):
+        """Test verified recipients for label_removed event alert"""
+        # Add labels
+        lab1 = self.event.labelling_set.create(label=self.label1,
+            creator=self.internal_user)
+        lab2 = self.event.labelling_set.create(label=self.label2,
+            creator=self.internal_user)
+
+        # Remove label 2 and issue alerts
+        lab2.delete()
+        EventLabelAlertIssuer(lab2, alert_type='label_removed') \
+            .issue_alerts()
+
+        # Check recipients passed to email alert function
+        email_recips = email_mock.call_args[0][2]
+        self.assertEqual(email_recips.count(), 1)
+        self.assertEqual(email_recips.first().description, 'label email')
+
+        # Phone alerts should be called at all since there were no
+        # verified recipients
+        phone_mock.assert_not_called()
diff --git a/gracedb/alerts/tests/test_views.py b/gracedb/alerts/tests/test_views.py
index 2098ec3c359349d62432a28df8145bd4cdfd3abe..966b3ed6e3b1b7962248d315c61761f1bc24d434 100644
--- a/gracedb/alerts/tests/test_views.py
+++ b/gracedb/alerts/tests/test_views.py
@@ -22,6 +22,10 @@ class TestUpdateContactView(GraceDbTestBase):
             description='test phone', phone='12345678901',
             phone_method=Contact.CONTACT_PHONE_BOTH)
 
+        # Refresh from database to get formatted phone numbers
+        cls.email_contact.refresh_from_db()
+        cls.phone_contact.refresh_from_db()
+
     def test_edit_email(self):
         """Users should not be able to update contact email"""
         # (because it sidesteps the verification process)
@@ -35,6 +39,9 @@ class TestUpdateContactView(GraceDbTestBase):
         response = self.request_as_user(url, "POST", self.internal_user,
             data=data)
 
+        # Response = 302 means success and redirect to main alerts page
+        self.assertEqual(response.status_code, 302)
+
         # Refresh from database
         self.email_contact.refresh_from_db()
 
@@ -58,6 +65,9 @@ class TestUpdateContactView(GraceDbTestBase):
         response = self.request_as_user(url, "POST", self.internal_user,
             data=data)
 
+        # Response = 302 means success and redirect to main alerts page
+        self.assertEqual(response.status_code, 302)
+
         # Refresh from database
         self.phone_contact.refresh_from_db()