From 90452855503c4c6d7e8c9516e98b9568fc251167 Mon Sep 17 00:00:00 2001
From: Tanner Prestegard <tanner.prestegard@ligo.org>
Date: Fri, 16 Aug 2019 15:34:03 -0500
Subject: [PATCH] alerts: rework and significantly expand scope of tests

---
 gracedb/alerts/tests/conftest.py        |  243 ++
 gracedb/alerts/tests/constants.py       |   16 +
 gracedb/alerts/tests/test_alerts.py     |  127 +
 gracedb/alerts/tests/test_contacts.py   |  149 +
 gracedb/alerts/tests/test_recipients.py | 4198 ++++++++++-------------
 5 files changed, 2371 insertions(+), 2362 deletions(-)
 create mode 100644 gracedb/alerts/tests/conftest.py
 create mode 100644 gracedb/alerts/tests/constants.py
 create mode 100644 gracedb/alerts/tests/test_alerts.py
 create mode 100644 gracedb/alerts/tests/test_contacts.py

diff --git a/gracedb/alerts/tests/conftest.py b/gracedb/alerts/tests/conftest.py
new file mode 100644
index 000000000..e870491e3
--- /dev/null
+++ b/gracedb/alerts/tests/conftest.py
@@ -0,0 +1,243 @@
+import copy
+import pytest
+import re
+
+from django.contrib.auth import get_user_model
+
+from alerts.models import Contact, Notification
+from events.models import Label, Group, Pipeline, Search, Event
+from superevents.models import Superevent
+from .constants import (
+    DEFAULT_FAR_T, DEFAULT_LABELS, DEFAULT_LABEL_QUERY, LABEL_QUERY2,
+    RANDOM_LABEL, LABEL_QUERY_PARSER, DEFAULT_GROUP, DEFAULT_PIPELINE,
+    DEFAULT_SEARCH
+)
+
+UserModel = get_user_model()
+
+
+###############################################################################
+# UTILITY FUNCTIONS ###########################################################
+###############################################################################
+def create_notification(
+    user,
+    notification_category,
+    contact_description='test',
+    phone=None,
+    email=None,
+    phone_method=Contact.CONTACT_PHONE_BOTH,
+    notification_description='test',
+    far_threshold=None,
+    ns_candidate=None,
+    label_names=None,
+    label_query=None,
+    groups=None,
+    pipelines=None,
+    searches=None,
+):
+    # Create contact
+    contact_dict = {}
+    if phone is not None and email is not None:
+        raise ValueError("Specify only one of label_names or label_query")
+    elif phone:
+        contact_dict['phone'] = phone
+        contact_dict['phone_method'] = phone_method
+    elif email:
+        contact_dict['email'] = email
+    c = Contact.objects.create(
+        user=user,
+        description=contact_description,
+        verified=True,
+        **contact_dict
+    )
+
+    # Create notification
+    notification_dict = {}
+    if far_threshold:
+        notification_dict['far_threshold'] = far_threshold
+    if ns_candidate:
+        notification_dict['ns_candidate'] = ns_candidate
+    if label_query:
+        notification_dict['label_query'] = label_query
+        
+    n = Notification.objects.create(
+        user=user,
+        description=notification_description,
+        category=notification_category,
+        **notification_dict
+    )
+    # Add m2m relations
+    n.contacts.add(c)
+    if label_names and label_query:
+        raise ValueError('')
+    elif label_query:
+        label_names = LABEL_QUERY_PARSER.findall(label_query)
+    if label_names:
+        for l in label_names:
+            label, _ = Label.objects.get_or_create(name=l)
+            n.labels.add(label)
+    if notification_category == Notification.NOTIFICATION_CATEGORY_EVENT:
+        if groups:
+            for g in groups:
+                group, _ = Group.objects.get_or_create(name=g)
+                n.groups.add(group)
+        if pipelines:
+            for p in pipelines:
+                pipeline, _ = Pipeline.objects.get_or_create(name=p)
+                n.pipelines.add(pipeline)
+        if searches:
+            for s in searches:
+                search, _ = Search.objects.get_or_create(name=s)
+                n.searches.add(search)
+    return n
+
+
+###############################################################################
+# FIXTURES ####################################################################
+###############################################################################
+@pytest.mark.django_db
+@pytest.fixture
+def event():
+    group, _ = Group.objects.get_or_create(name='event_group')
+    pipeline, _ = Pipeline.objects.get_or_create(name='event_pipeline')
+    search, _ = Search.objects.get_or_create(name='event_search')
+    user = UserModel.objects.create(username='event.creator')
+    event = Event.objects.create(group=group, pipeline=pipeline, search=search,
+                                 far=1, submitter=user)
+    return event
+
+
+@pytest.mark.django_db
+@pytest.fixture
+def superevent(event):
+    user = UserModel.objects.create(username='superevent.creator')
+    superevent = Superevent.objects.create(submitter=user, t_start=0, t_0=1,
+                                           t_end=2, preferred_event=event)
+    return superevent
+
+
+SUPEREVENT_NOTIFICATION_DATA = [
+    dict(desc='all'),
+    dict(desc='far_t_only', far_threshold=DEFAULT_FAR_T),
+    dict(desc='nscand_only', ns_candidate=True),
+    dict(desc='labels_only', label_names=DEFAULT_LABELS),
+    dict(desc='labelq_only', label_query=DEFAULT_LABEL_QUERY['query']),
+    dict(desc='far_t_and_nscand', far_threshold=DEFAULT_FAR_T,
+         ns_candidate=True),
+    dict(desc='far_t_and_labels', far_threshold=DEFAULT_FAR_T,
+         label_names=DEFAULT_LABELS),
+    dict(desc='far_t_and_labelq', far_threshold=DEFAULT_FAR_T,
+         label_query=DEFAULT_LABEL_QUERY['query']),
+    dict(desc='nscand_and_labels', ns_candidate=True,
+         label_names=DEFAULT_LABELS),
+    dict(desc='nscand_and_labelq', ns_candidate=True,
+         label_query=DEFAULT_LABEL_QUERY['query']),
+    dict(desc='far_t_and_nscand_and_labels', far_threshold=DEFAULT_FAR_T,
+         ns_candidate=True, label_names=DEFAULT_LABELS),
+    dict(desc='far_t_and_nscand_and_labelq', far_threshold=DEFAULT_FAR_T,
+         ns_candidate=True, label_query=DEFAULT_LABEL_QUERY['query']),
+    dict(desc='labelq2_only', label_query=LABEL_QUERY2),
+    dict(desc='far_t_and_labelq2', far_threshold=DEFAULT_FAR_T,
+         label_query=LABEL_QUERY2),
+    dict(desc='nscand_and_labelq2', ns_candidate=True,
+         label_query=LABEL_QUERY2),
+    dict(desc='far_t_and_nscand_and_labelq2', far_threshold=DEFAULT_FAR_T,
+         ns_candidate=True, label_query=LABEL_QUERY2),
+]
+@pytest.mark.django_db
+@pytest.fixture
+def superevent_notifications(request):
+    # Get user fixture
+    user = request.getfixturevalue('internal_user')
+
+    # Create notifications
+    notification_pks = []
+    notification_data = copy.deepcopy(SUPEREVENT_NOTIFICATION_DATA) 
+    for notification_dict in notification_data:
+        desc = notification_dict.pop('desc')
+        n = create_notification(
+            user,
+            Notification.NOTIFICATION_CATEGORY_SUPEREVENT,
+            phone='12345678901',
+            notification_description=desc,
+            **notification_dict
+        )
+        notification_pks.append(n.pk)
+
+    return Notification.objects.filter(pk__in=notification_pks)
+
+
+EVENT_NOTIFICATION_DATA = copy.deepcopy(SUPEREVENT_NOTIFICATION_DATA)
+EVENT_NOTIFICATION_DATA += [
+    dict(desc='gps_only', groups=[DEFAULT_GROUP], pipelines=[DEFAULT_PIPELINE],
+         searches=[DEFAULT_SEARCH]),
+    dict(desc='far_t_and_gps', far_threshold=DEFAULT_FAR_T,
+         groups=[DEFAULT_GROUP], pipelines=[DEFAULT_PIPELINE],
+         searches=[DEFAULT_SEARCH]),
+    dict(desc='nscand_and_gps', ns_candidate=True,
+         groups=[DEFAULT_GROUP], pipelines=[DEFAULT_PIPELINE],
+         searches=[DEFAULT_SEARCH]),
+    dict(desc='labels_and_gps', label_names=DEFAULT_LABELS,
+         groups=[DEFAULT_GROUP], pipelines=[DEFAULT_PIPELINE],
+         searches=[DEFAULT_SEARCH]),
+    dict(desc='labelq_and_gps', label_query=DEFAULT_LABEL_QUERY['query'],
+         groups=[DEFAULT_GROUP], pipelines=[DEFAULT_PIPELINE],
+         searches=[DEFAULT_SEARCH]),
+    dict(desc='far_t_and_nscand_and_gps', far_threshold=DEFAULT_FAR_T,
+         ns_candidate=True, groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+    dict(desc='far_t_and_labels_and_gps', far_threshold=DEFAULT_FAR_T,
+         label_names=DEFAULT_LABELS, groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+    dict(desc='far_t_and_labelq_and_gps', far_threshold=DEFAULT_FAR_T,
+         label_query=DEFAULT_LABEL_QUERY['query'], groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+    dict(desc='nscand_and_labels_and_gps', ns_candidate=True,
+         label_names=DEFAULT_LABELS, groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+    dict(desc='nscand_and_labelq_and_gps', ns_candidate=True,
+         label_query=DEFAULT_LABEL_QUERY['query'], groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+    dict(desc='far_t_and_nscand_and_labels_and_gps',
+         far_threshold=DEFAULT_FAR_T, ns_candidate=True,
+         label_names=DEFAULT_LABELS, groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+    dict(desc='far_t_and_nscand_and_labelq_and_gps',
+         far_threshold=DEFAULT_FAR_T, ns_candidate=True,
+         label_query=DEFAULT_LABEL_QUERY['query'], groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+    dict(desc='labelq2_and_gps', label_query=LABEL_QUERY2,
+         groups=[DEFAULT_GROUP], pipelines=[DEFAULT_PIPELINE],
+         searches=[DEFAULT_SEARCH]),
+    dict(desc='far_t_and_labelq2_and_gps', far_threshold=DEFAULT_FAR_T,
+         label_query=LABEL_QUERY2, groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+    dict(desc='nscand_and_labelq2_and_gps', ns_candidate=True,
+         label_query=LABEL_QUERY2, groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+    dict(desc='far_t_and_nscand_and_labelq2_and_gps',
+         far_threshold=DEFAULT_FAR_T, ns_candidate=True,
+         label_query=LABEL_QUERY2, groups=[DEFAULT_GROUP],
+         pipelines=[DEFAULT_PIPELINE], searches=[DEFAULT_SEARCH]),
+]
+@pytest.mark.django_db
+@pytest.fixture
+def event_notifications(request):
+    # Get user fixture
+    user = request.getfixturevalue('internal_user')
+
+    # Create notifications
+    notification_pks = []
+    notification_data = copy.deepcopy(EVENT_NOTIFICATION_DATA) 
+    for notification_dict in notification_data:
+        desc = notification_dict.pop('desc')
+        n = create_notification(
+            user,
+            Notification.NOTIFICATION_CATEGORY_EVENT,
+            phone='12345678901',
+            notification_description=desc,
+            **notification_dict
+        )
+        notification_pks.append(n.pk)
+
+    return Notification.objects.filter(pk__in=notification_pks)
diff --git a/gracedb/alerts/tests/constants.py b/gracedb/alerts/tests/constants.py
new file mode 100644
index 000000000..89c92017a
--- /dev/null
+++ b/gracedb/alerts/tests/constants.py
@@ -0,0 +1,16 @@
+import re
+
+
+# Constants for use in various tests
+DEFAULT_FAR_T = 1  # Hz
+DEFAULT_LABELS = ['L1', 'L2']
+LABEL_QUERY_PARSER = re.compile(r'([a-zA-Z0-9]+)')
+DEFAULT_LABEL_QUERY = {'query': 'L3 & L4'}
+DEFAULT_LABEL_QUERY['labels'] = LABEL_QUERY_PARSER.findall(
+    DEFAULT_LABEL_QUERY['query']
+)
+LABEL_QUERY2 = 'L5 & L6 & ~L7'
+RANDOM_LABEL = 'L12345'
+DEFAULT_GROUP = 'Group1'
+DEFAULT_PIPELINE = 'Pipeline1'
+DEFAULT_SEARCH = 'Search1'
diff --git a/gracedb/alerts/tests/test_alerts.py b/gracedb/alerts/tests/test_alerts.py
new file mode 100644
index 000000000..30096903e
--- /dev/null
+++ b/gracedb/alerts/tests/test_alerts.py
@@ -0,0 +1,127 @@
+import itertools
+try:
+    from unittest import mock
+except ImportError:  # python < 3
+    import mock
+import types
+
+import pytest
+
+from alerts.main import issue_alerts
+from events.models import Event
+from superevents.models import Superevent
+
+
+###############################################################################
+# FIXTURES ####################################################################
+###############################################################################
+# Mock recipient getter dict
+@pytest.fixture
+def mock_rg_dict():
+    def _inner(alert_type='new', recips_exist=True):
+        # Set up mock recipient getter stuff
+        mock_recipients = mock.MagicMock()
+        mock_recipients.exists.return_value = recips_exist
+        mock_rg = mock.MagicMock()
+        mock_rg.get_recipients.return_value = \
+            (mock_recipients, mock_recipients)
+        mock_rg_class = mock.MagicMock()
+        mock_rg_class.return_value = mock_rg
+        mock_rg_dict = {alert_type: mock_rg_class}
+        return mock_rg_dict
+    return _inner
+
+
+###############################################################################
+# TESTS #######################################################################
+###############################################################################
+@pytest.mark.parametrize(
+    "xmpp,email,phone",
+    list(itertools.product((True, False), repeat=3))
+)
+def test_alert_settings(settings, mock_rg_dict, xmpp, email, phone):
+    # Set up settings
+    settings.SEND_XMPP_ALERTS = xmpp
+    settings.SEND_EMAIL_ALERTS = email
+    settings.SEND_PHONE_ALERTS = phone
+
+    # Set up mock superevent object
+    superevent = mock.MagicMock()
+    superevent.is_test.return_value = False
+    superevent.is_mdc.return_value = False
+    preferred_event = mock.MagicMock()
+    type(preferred_event).offline = mock.PropertyMock(return_value=False)
+    superevent.preferred_event = preferred_event
+
+    # Call issue_alerts
+    with mock.patch('alerts.main.issue_xmpp_alerts') as mock_xmpp, \
+         mock.patch('alerts.main.issue_email_alerts') as mock_email, \
+         mock.patch('alerts.main.issue_phone_alerts') as mock_phone, \
+         mock.patch('alerts.main.is_event') as mock_is_event, \
+         mock.patch.dict('alerts.main.ALERT_TYPE_RECIPIENT_GETTERS',
+                         mock_rg_dict(), clear=True):
+
+        mock_is_event.return_value = False
+        issue_alerts(superevent, 'new', None)
+
+    # Check results
+    if xmpp:
+        assert mock_xmpp.call_count == 1
+    if phone:
+        assert mock_phone.call_count == 1
+    if email:
+        assert mock_email.call_count == 1
+    if not (phone or email):
+        assert mock_is_event.call_count == 0 
+
+
+@pytest.mark.parametrize(
+    "is_event,is_mdc,is_test,is_offline",
+    list(itertools.product((True, False), repeat=4))
+)
+def test_no_alerts_for_test_mdc_offline_events_and_superevents(
+    settings, mock_rg_dict, is_event, is_mdc, is_test, is_offline
+):
+    # Set up settings
+    settings.SEND_XMPP_ALERTS = False
+    settings.SEND_EMAIL_ALERTS = True
+    settings.SEND_PHONE_ALERTS = True
+
+    # Set up mock event/superevent object
+    # First, set up event
+    event = mock.MagicMock()
+    type(event).offline = mock.PropertyMock(return_value=is_offline)
+    # If we're doing a superevent, then set the event as its preferred_event
+    if not is_event:
+        es = mock.MagicMock()
+        es.preferred_event = event
+    else:
+        es = event
+    # is_mdc and is_test are handled the same way for both events and
+    # superevents, so we can set it up at this point
+    es.is_mdc.return_value = is_mdc
+    es.is_test.return_value = is_test
+
+    # Instantiate mock_rg_dict
+    mock_rg_dict = mock_rg_dict()
+
+    # Call issue_alerts
+    with mock.patch('alerts.main.issue_xmpp_alerts') as mock_xmpp, \
+         mock.patch('alerts.main.issue_email_alerts') as mock_email, \
+         mock.patch('alerts.main.issue_phone_alerts') as mock_phone, \
+         mock.patch('alerts.main.is_event') as mock_is_event, \
+         mock.patch.dict('alerts.main.ALERT_TYPE_RECIPIENT_GETTERS',
+                         mock_rg_dict, clear=True):
+
+        mock_is_event.return_value = is_event
+        issue_alerts(es, 'new', None)
+
+    # Check results
+    assert es.is_mdc.call_count == 1
+    assert es.is_test.call_count == int(not es.is_mdc())
+    # Whether the recipient_getter class is called or not depends
+    # finally on whether the event is offline or not
+    if (not (es.is_mdc() or es.is_test())):
+        assert mock_rg_dict['new'].call_count == int(not event.offline)
+    else:
+        assert mock_rg_dict['new'].call_count == 0
diff --git a/gracedb/alerts/tests/test_contacts.py b/gracedb/alerts/tests/test_contacts.py
new file mode 100644
index 000000000..5fd9e5c10
--- /dev/null
+++ b/gracedb/alerts/tests/test_contacts.py
@@ -0,0 +1,149 @@
+import pytest
+
+from django.contrib.auth import get_user_model
+
+from alerts.models import Contact, Notification
+from alerts.recipients import CreationRecipientGetter
+
+UserModel = get_user_model()
+
+
+@pytest.mark.django_db
+def test_multiple_contacts(superevent, internal_user):
+    # Set up contacts and notifications
+    c1 = Contact.objects.create(
+        user=internal_user, description='c1', verified=True,
+        phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
+    )
+    c2 = Contact.objects.create(
+        user=internal_user, description='c2', verified=True,
+        email='test@test.com'
+    )
+    n = Notification.objects.create(
+        user=internal_user, description='test',
+        category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
+    )
+    n.contacts.add(*(c1, c2))
+
+    # Get recipients 
+    recipient_getter = CreationRecipientGetter(superevent)
+    email_contacts, phone_contacts = recipient_getter.get_recipients()
+
+    # Check results
+    assert email_contacts.count() == 1
+    assert email_contacts.first().pk == c2.pk
+    assert phone_contacts.count() == 1
+    assert phone_contacts.first().pk == c1.pk
+
+
+@pytest.mark.django_db
+def test_duplicate_contacts(superevent, internal_user):
+    # Set up contacts and notifications
+    c = Contact.objects.create(
+        user=internal_user, description='test', verified=True,
+        phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
+    )
+    n1 = Notification.objects.create(
+        user=internal_user, description='n1',
+        category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
+    )
+    n2 = Notification.objects.create(
+        user=internal_user, description='n2',
+        category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
+    )
+    n1.contacts.add(c)
+    n2.contacts.add(c)
+
+    # Get notifications and check results
+    recipient_getter = CreationRecipientGetter(superevent)
+    notifications = recipient_getter.get_notifications()
+    assert notifications.count() == 2
+    for pk in (n1.pk, n2.pk):
+        assert notifications.filter(pk=pk).exists()
+   
+    # Get recipients and check results 
+    email_contacts, phone_contacts = recipient_getter.get_recipients()
+
+    # Check results
+    assert email_contacts.count() == 0
+    assert phone_contacts.count() == 1
+    assert phone_contacts.first().pk == c.pk
+
+
+@pytest.mark.django_db
+def test_contacts_non_internal_user(superevent, internal_user):
+    # NOTE: this test handles the case where a user with contacts/notifications
+    # already set up leaves the collboration.
+
+    # Create a non-internal user
+    external_user = UserModel.objects.create(username='external.user')
+
+    # Set up contacts and notifications
+    c_internal = Contact.objects.create(
+        user=internal_user, description='test', verified=True,
+        phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
+    )
+    c_external = Contact.objects.create(
+        user=external_user, description='test', verified=True,
+        phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
+    )
+    n_internal = Notification.objects.create(
+        user=internal_user, description='internal',
+        category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
+    )
+    n_internal.contacts.add(c_internal)
+    n_external = Notification.objects.create(
+        user=external_user, description='external',
+        category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
+    )
+    n_external.contacts.add(c_external)
+
+    # Get notifications and check results
+    recipient_getter = CreationRecipientGetter(superevent)
+    notifications = recipient_getter.get_notifications()
+    assert notifications.count() == 2
+    for pk in (n_internal.pk, n_external.pk):
+        assert notifications.filter(pk=pk).exists()
+   
+    # Get recipients and check results 
+    email_contacts, phone_contacts = recipient_getter.get_recipients()
+
+    # Check results
+    assert email_contacts.count() == 0
+    assert phone_contacts.count() == 1
+    assert phone_contacts.first().pk == c_internal.pk
+
+
+@pytest.mark.django_db
+def test_unverified_contact(superevent, internal_user):
+    # NOTE: this test handles the case where a user with contacts/notifications
+    # already set up leaves the collboration.
+
+    # Set up contacts and notifications
+    c_verified = Contact.objects.create(
+        user=internal_user, description='test', verified=True,
+        phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
+    )
+    c_unverified = Contact.objects.create(
+        user=internal_user, description='test', verified=False,
+        phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
+    )
+    n = Notification.objects.create(
+        user=internal_user, description='internal',
+        category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
+    )
+    n.contacts.add(*(c_verified, c_unverified))
+
+    # Get notifications and check results
+    recipient_getter = CreationRecipientGetter(superevent)
+    notifications = recipient_getter.get_notifications()
+    assert notifications.count() == 1
+    assert notifications.first().pk == n.pk
+   
+    # Get recipients and check results 
+    email_contacts, phone_contacts = recipient_getter.get_recipients()
+
+    # Check results
+    assert email_contacts.count() == 0
+    assert phone_contacts.count() == 1
+    assert phone_contacts.first().pk == c_verified.pk
diff --git a/gracedb/alerts/tests/test_recipients.py b/gracedb/alerts/tests/test_recipients.py
index 22b83dbcd..0130d853a 100644
--- a/gracedb/alerts/tests/test_recipients.py
+++ b/gracedb/alerts/tests/test_recipients.py
@@ -1,2368 +1,1842 @@
-import mock
-
-from django.test import override_settings
-
-from alerts.issuers.events import EventAlertIssuer, EventLabelAlertIssuer
-from alerts.issuers.superevents import (
-    SupereventAlertIssuer, SupereventLabelAlertIssuer,
+try:
+    from unittest import mock
+except ImportError:  # python < 3
+    import mock
+import pytest
+import types
+
+from alerts.models import Notification
+from alerts.recipients import (
+    CreationRecipientGetter, UpdateRecipientGetter,
+    LabelAddedRecipientGetter, LabelRemovedRecipientGetter,
 )
-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,
+from .constants import (
+    DEFAULT_FAR_T, DEFAULT_LABELS, DEFAULT_LABEL_QUERY, LABEL_QUERY2,
+    RANDOM_LABEL, DEFAULT_GROUP, DEFAULT_PIPELINE, DEFAULT_SEARCH
 )
-@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,
+
+# NOTE: there are a *LOT* of tests in here. It's definitely overkill.
+#   I'm trying to test *every* possible situation because users tend
+#   to get worked up over problems with notifications.
+#
+# NOTE on debugging: because there are only a few actual test functions, but
+#   they are highly parametrized, it can be hard to debug failing tests. I
+#   suggest looking at the test label in the pytest output (i.e., you should
+#   see something like "notif_descs###", where ### is the test number). Then
+#   go and edit the dataset used in the parametrize decorator to be just that
+#   test.  Ex: if test_event_update_alerts with notif_descs123 is failing,
+#   go and edit EVENT_UPDATE_ALERT_DATA -> EVENT_UPDATE_ALERT_DATA[123:124]
+
+
+###############################################################################
+# TEST DATA ###################################################################
+###############################################################################
+SUPEREVENT_CREATION_ALERT_DATA = [
+    (None, False, ['all']),
+    (None, True, ['all', 'nscand_only']),
+    (DEFAULT_FAR_T*2.0, False, ['all']),
+    (DEFAULT_FAR_T*2.0, True, ['all', 'nscand_only']),
+    (DEFAULT_FAR_T/2.0, False, ['all', 'far_t_only']),
+    (DEFAULT_FAR_T/2.0, True, ['all', 'far_t_only', 'nscand_only',
+                               'far_t_and_nscand']),
+]
+
+EVENT_CREATION_ALERT_DATA = [
+    (None, False, False, ['all']),
+    (None, False, True, ['all', 'gps_only']),
+    (None, True, False, ['all', 'nscand_only']),
+    (None, True, True, ['all', 'nscand_only', 'gps_only', 'nscand_and_gps']),
+    (DEFAULT_FAR_T*2.0, False, False, ['all']),
+    (DEFAULT_FAR_T*2.0, False, True, ['all', 'gps_only']),
+    (DEFAULT_FAR_T*2.0, True, False, ['all', 'nscand_only']),
+    (DEFAULT_FAR_T*2.0, True, True, ['all', 'nscand_only', 'gps_only',
+                                     'nscand_and_gps']),
+    (DEFAULT_FAR_T/2.0, False, False, ['all', 'far_t_only']),
+    (DEFAULT_FAR_T/2.0, False, True, ['all', 'far_t_only', 'gps_only',
+                                      'far_t_and_gps']),
+    (DEFAULT_FAR_T/2.0, True, False, ['all', 'far_t_only', 'nscand_only',
+                                      'far_t_and_nscand']),
+    (DEFAULT_FAR_T/2.0, True, True,
+        ['all', 'far_t_only', 'nscand_only', 'gps_only', 'far_t_and_nscand',
+         'far_t_and_gps', 'nscand_and_gps', 'far_t_and_nscand_and_gps']),
+]
+
+SUPEREVENT_UPDATE_ALERT_DATA = [
+    # FAR = None and constant -------------------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (None, None, False, False, None, []),
+    (None, None, True, True, None, []),
+    ### With labels -----------------------------------
+    (None, None, False, False, DEFAULT_LABELS, []),
+    (None, None, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (None, None, False, False, DEFAULT_LABEL_QUERY['labels'], []),
+    (None, None, True, True, DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (None, None, False, True, None, ['nscand_only']),
+    (None, None, True, False, None, []),
+    ### With labels -----------------------------------
+    (None, None, False, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (None, None, True, False, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (None, None, False, True, DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq']),
+    (None, None, True, False, DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = above threshold and constant --------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, None, ['nscand_only']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True,
+        DEFAULT_LABEL_QUERY['labels'], ['nscand_only',
+                                        'nscand_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = above threshold and increases -------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, None, ['nscand_only']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True,
+        DEFAULT_LABEL_QUERY['labels'], ['nscand_only',
+                                        'nscand_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = below threshold and constant --------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, None,
+        ['nscand_only', 'far_t_and_nscand']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, DEFAULT_LABELS,
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True,
+        DEFAULT_LABEL_QUERY['labels'], ['nscand_only', 'far_t_and_nscand',
+                                        'nscand_and_labelq',
+                                        'far_t_and_nscand_and_labelq']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = below threshold and decreases -------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, None,
+        ['nscand_only', 'far_t_and_nscand']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, DEFAULT_LABELS,
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True,
+        DEFAULT_LABEL_QUERY['labels'], ['nscand_only', 'far_t_and_nscand',
+                                        'nscand_and_labelq',
+                                        'far_t_and_nscand_and_labelq']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = below -> above threshold ------------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, None,
+        ['nscand_only']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = None -> above threshold -------------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (None, DEFAULT_FAR_T*2, False, False, None, []),
+    (None, DEFAULT_FAR_T*2, True, True, None, []),
+    ### With labels -----------------------------------
+    (None, DEFAULT_FAR_T*2, False, False, DEFAULT_LABELS, []),
+    (None, DEFAULT_FAR_T*2, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (None, DEFAULT_FAR_T*2, False, False, DEFAULT_LABEL_QUERY['labels'], []),
+    (None, DEFAULT_FAR_T*2, True, True, DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (None, DEFAULT_FAR_T*2, False, True, None, ['nscand_only']),
+    (None, DEFAULT_FAR_T*2, True, False, None, []),
+    ### With labels -----------------------------------
+    (None, DEFAULT_FAR_T*2, False, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (None, DEFAULT_FAR_T*2, True, False, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (None, DEFAULT_FAR_T*2, False, True, DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq']),
+    (None, DEFAULT_FAR_T*2, True, False, DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = None -> below threshold -------------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (None, DEFAULT_FAR_T/2.0, False, False, None, ['far_t_only']),
+    (None, DEFAULT_FAR_T/2.0, True, True, None,
+        ['far_t_only', 'far_t_and_nscand']),
+    ### With labels -----------------------------------
+    (None, DEFAULT_FAR_T/2.0, False, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels']),
+    (None, DEFAULT_FAR_T/2.0, True, True, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
+         'far_t_and_nscand_and_labels']),
+    ### With labels matching label query --------------
+    (None, DEFAULT_FAR_T/2.0, False, False, DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq']),
+    (None, DEFAULT_FAR_T/2.0, True, True, DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'far_t_and_nscand_and_labelq']),
+    # FAR = None -> below threshold -------------------
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (None, DEFAULT_FAR_T/2.0, False, True, None,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand']),
+    (None, DEFAULT_FAR_T/2.0, True, False, None, ['far_t_only']),
+    ### With labels -----------------------------------
+    (None, DEFAULT_FAR_T/2.0, False, True, DEFAULT_LABELS,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand',
+         'far_t_and_labels', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (None, DEFAULT_FAR_T/2.0, True, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels']),
+    ### With labels matching label query --------------
+    (None, DEFAULT_FAR_T/2.0, False, True, DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand',
+         'far_t_and_labelq', 'nscand_and_labelq',
+         'far_t_and_nscand_and_labelq']),
+    (None, DEFAULT_FAR_T/2.0, True, False, DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq']),
+    # FAR = above -> below threshold ------------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, None,
+        ['far_t_only']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, None,
+        ['far_t_only', 'far_t_and_nscand']),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
+         'far_t_and_nscand_and_labels']),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'far_t_and_nscand_and_labelq']),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, None,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, None,
+        ['far_t_only']),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, DEFAULT_LABELS,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand',
+         'far_t_and_labels', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels']),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand',
+         'far_t_and_labelq', 'nscand_and_labelq',
+         'far_t_and_nscand_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq']),
+]
+
+# Params: old_far,far,old_nscand,nscand,use_default_gps,labels,notif_descs
+EVENT_UPDATE_ALERT_DATA = [
+    # FAR = None and constant -------------------------
+    ## No labels --------------------------------------
+    ### NSCAND = constant -----------------------------
+    (None, None, False, False, False, None, []),
+    (None, None, False, False, True, None, []),
+    (None, None, True, True, False, None, []),
+    (None, None, True, True, True, None, []),
+    ### With labels -----------------------------------
+    (None, None, False, False, False, DEFAULT_LABELS, []),
+    (None, None, False, False, True, DEFAULT_LABELS, []),
+    (None, None, True, True, False, DEFAULT_LABELS, []),
+    (None, None, True, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (None, None, False, False, False, DEFAULT_LABEL_QUERY['labels'], []),
+    (None, None, False, False, True, DEFAULT_LABEL_QUERY['labels'], []),
+    (None, None, True, True, False, DEFAULT_LABEL_QUERY['labels'], []),
+    (None, None, True, True, True, DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (None, None, False, True, False, None, ['nscand_only']),
+    (None, None, False, True, True, None, ['nscand_only', 'nscand_and_gps']),
+    (None, None, True, False, False, None, []),
+    (None, None, True, False, True, None, []),
+    ### With labels -----------------------------------
+    (None, None, False, True, False, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (None, None, False, True, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
+         'nscand_and_labels_and_gps']),
+    (None, None, True, False, False, DEFAULT_LABELS, []),
+    (None, None, True, False, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (None, None, False, True, False, DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq']),
+    (None, None, False, True, True, DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
+         'nscand_and_labelq_and_gps']),
+    (None, None, True, False, False, DEFAULT_LABEL_QUERY['labels'], []),
+    (None, None, True, False, True, DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = above threshold and constant --------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, False, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, True, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, False, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, True, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, False, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, False, None,
+        ['nscand_only']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, True, None,
+        ['nscand_only', 'nscand_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, False, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, False, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
+         'nscand_and_labels_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, False, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
+         'nscand_and_labelq_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = above threshold and increases -------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, False, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, True, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, False, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, True, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, False, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, False, None,
+        ['nscand_only']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, True, None,
+        ['nscand_only', 'nscand_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, False, None, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, False, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
+         'nscand_and_labels_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, False, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
+         'nscand_and_labelq_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = below threshold and constant --------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, True, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, True, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, True, DEFAULT_LABELS,
+        []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, False, None,
+        ['nscand_only', 'far_t_and_nscand']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, True, None,
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_gps',
+         'far_t_and_nscand_and_gps']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, False, DEFAULT_LABELS,
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, True, DEFAULT_LABELS,
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels', 'nscand_and_gps',
+         'far_t_and_nscand_and_gps', 'nscand_and_labels_and_gps', 
+         'far_t_and_nscand_and_labels_and_gps']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, True, DEFAULT_LABELS,
+        []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labelq',
+         'far_t_and_nscand_and_labelq']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labelq',
+         'far_t_and_nscand_and_labelq', 'nscand_and_gps',
+         'far_t_and_nscand_and_gps', 'nscand_and_labelq_and_gps',
+         'far_t_and_nscand_and_labelq_and_gps']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = below threshold and decreases -------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, True, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, True, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, True, DEFAULT_LABELS,
+        []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, False, None,
+        ['nscand_only', 'far_t_and_nscand']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, True, None,
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_gps',
+         'far_t_and_nscand_and_gps']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, False, DEFAULT_LABELS,
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, True, DEFAULT_LABELS,
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels', 'nscand_and_gps',
+         'far_t_and_nscand_and_gps', 'nscand_and_labels_and_gps',
+         'far_t_and_nscand_and_labels_and_gps']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, True, DEFAULT_LABELS,
+        []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labelq',
+         'far_t_and_nscand_and_labelq']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'far_t_and_nscand', 'nscand_and_labelq',
+         'far_t_and_nscand_and_labelq', 'nscand_and_gps',
+         'far_t_and_nscand_and_gps', 'nscand_and_labelq_and_gps',
+         'far_t_and_nscand_and_labelq_and_gps']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = below -> above threshold ------------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, True, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, True, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, True, DEFAULT_LABELS,
+        []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, False, None,
+        ['nscand_only']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, True, None,
+        ['nscand_only', 'nscand_and_gps']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, False, None, []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, True, None, []),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, False, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
+         'nscand_and_labels_and_gps']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, False, DEFAULT_LABELS,
+        []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, True, DEFAULT_LABELS,
+        []),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
+         'nscand_and_labelq_and_gps']),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, False,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    (DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, True,
+        DEFAULT_LABEL_QUERY['labels'], []),
+    # FAR = None -> above threshold -------------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (None, DEFAULT_FAR_T*2, False, False, False, None, []),
+    (None, DEFAULT_FAR_T*2, False, False, True, None, []),
+    (None, DEFAULT_FAR_T*2, True, True, False, None, []),
+    (None, DEFAULT_FAR_T*2, True, True, True, None, []),
+    ### With labels -----------------------------------
+    (None, DEFAULT_FAR_T*2, False, False, False, DEFAULT_LABELS, []),
+    (None, DEFAULT_FAR_T*2, False, False, True, DEFAULT_LABELS, []),
+    (None, DEFAULT_FAR_T*2, True, True, False, DEFAULT_LABELS, []),
+    (None, DEFAULT_FAR_T*2, True, True, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (None, DEFAULT_FAR_T*2, False, False, False, DEFAULT_LABEL_QUERY['labels'],
+        []),
+    (None, DEFAULT_FAR_T*2, False, False, True, DEFAULT_LABEL_QUERY['labels'],
+        []),
+    (None, DEFAULT_FAR_T*2, True, True, False, DEFAULT_LABEL_QUERY['labels'],
+        []),
+    (None, DEFAULT_FAR_T*2, True, True, True, DEFAULT_LABEL_QUERY['labels'],
+        []),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (None, DEFAULT_FAR_T*2, False, True, False, None, ['nscand_only']),
+    (None, DEFAULT_FAR_T*2, False, True, True, None,
+        ['nscand_only', 'nscand_and_gps']),
+    (None, DEFAULT_FAR_T*2, True, False, False, None, []),
+    (None, DEFAULT_FAR_T*2, True, False, True, None, []),
+    ### With labels -----------------------------------
+    (None, DEFAULT_FAR_T*2, False, True, False, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels']),
+    (None, DEFAULT_FAR_T*2, False, True, True, DEFAULT_LABELS,
+        ['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
+         'nscand_and_labels_and_gps']),
+    (None, DEFAULT_FAR_T*2, True, False, False, DEFAULT_LABELS, []),
+    (None, DEFAULT_FAR_T*2, True, False, True, DEFAULT_LABELS, []),
+    ### With labels matching label query --------------
+    (None, DEFAULT_FAR_T*2, False, True, False, DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq']),
+    (None, DEFAULT_FAR_T*2, False, True, True, DEFAULT_LABEL_QUERY['labels'],
+        ['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
+         'nscand_and_labelq_and_gps']),
+    (None, DEFAULT_FAR_T*2, True, False, False, DEFAULT_LABEL_QUERY['labels'],
+        []),
+    (None, DEFAULT_FAR_T*2, True, False, True, DEFAULT_LABEL_QUERY['labels'],
+        []),
+    # FAR = None -> below threshold -------------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (None, DEFAULT_FAR_T/2.0, False, False, False, None, ['far_t_only']),
+    (None, DEFAULT_FAR_T/2.0, False, False, True, None,
+        ['far_t_only', 'far_t_and_gps']),
+    (None, DEFAULT_FAR_T/2.0, True, True, False, None,
+        ['far_t_only', 'far_t_and_nscand']),
+    (None, DEFAULT_FAR_T/2.0, True, True, True, None,
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_gps',
+         'far_t_and_nscand_and_gps']),
+    ### With labels -----------------------------------
+    (None, DEFAULT_FAR_T/2.0, False, False, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels']),
+    (None, DEFAULT_FAR_T/2.0, False, False, True, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels', 'far_t_and_gps',
+         'far_t_and_labels_and_gps']),
+    (None, DEFAULT_FAR_T/2.0, True, True, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (None, DEFAULT_FAR_T/2.0, True, True, True, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
+         'far_t_and_nscand_and_labels', 'far_t_and_gps',
+         'far_t_and_nscand_and_gps', 'far_t_and_labels_and_gps',
+         'far_t_and_nscand_and_labels_and_gps']),
+    ### With labels matching label query --------------
+    (None, DEFAULT_FAR_T/2.0, False, False, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq']),
+    (None, DEFAULT_FAR_T/2.0, False, False, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq', 'far_t_and_gps',
+         'far_t_and_labelq_and_gps']),
+    (None, DEFAULT_FAR_T/2.0, True, True, False, DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'far_t_and_nscand_and_labelq']),
+    (None, DEFAULT_FAR_T/2.0, True, True, True, DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'far_t_and_nscand_and_labelq', 'far_t_and_gps',
+         'far_t_and_nscand_and_gps', 'far_t_and_labelq_and_gps',
+         'far_t_and_nscand_and_labelq_and_gps']),
+    # FAR = None -> below threshold -------------------
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (None, DEFAULT_FAR_T/2.0, False, True, False, None,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand']),
+    (None, DEFAULT_FAR_T/2.0, False, True, True, None,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_gps',
+         'nscand_and_gps', 'far_t_and_nscand_and_gps']),
+    (None, DEFAULT_FAR_T/2.0, True, False, False, None, ['far_t_only']),
+    (None, DEFAULT_FAR_T/2.0, True, False, True, None,
+        ['far_t_only', 'far_t_and_gps']),
+    ### With labels -----------------------------------
+    (None, DEFAULT_FAR_T/2.0, False, True, False, DEFAULT_LABELS,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand',
+         'far_t_and_labels', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (None, DEFAULT_FAR_T/2.0, False, True, True, DEFAULT_LABELS,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand',
+         'far_t_and_labels', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels', 'far_t_and_gps', 'nscand_and_gps',
+         'far_t_and_nscand_and_gps', 'far_t_and_labels_and_gps',
+         'nscand_and_labels_and_gps', 'far_t_and_nscand_and_labels_and_gps']),
+    (None, DEFAULT_FAR_T/2.0, True, False, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels']),
+    (None, DEFAULT_FAR_T/2.0, True, False, True, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels', 'far_t_and_gps',
+         'far_t_and_labels_and_gps']),
+    ### With labels matching label query --------------
+    (None, DEFAULT_FAR_T/2.0, False, True, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'nscand_and_labelq', 'far_t_and_nscand_and_labelq']),
+    (None, DEFAULT_FAR_T/2.0, False, True, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'nscand_and_labelq', 'far_t_and_nscand_and_labelq',
+         'far_t_and_gps', 'nscand_and_gps', 'far_t_and_nscand_and_gps',
+         'far_t_and_labelq_and_gps', 'nscand_and_labelq_and_gps',
+         'far_t_and_nscand_and_labelq_and_gps']),
+    (None, DEFAULT_FAR_T/2.0, True, False, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq']),
+    (None, DEFAULT_FAR_T/2.0, True, False, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq', 'far_t_and_gps',
+         'far_t_and_labelq_and_gps']),
+    # FAR = above -> below threshold ------------------
+    ## NSCAND = constant ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, False, None,
+        ['far_t_only']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, True, None,
+        ['far_t_only', 'far_t_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, False, None,
+        ['far_t_only', 'far_t_and_nscand']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, True, None,
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_gps',
+         'far_t_and_nscand_and_gps']),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, True, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels', 'far_t_and_gps',
+         'far_t_and_labels_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, True, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
+         'far_t_and_nscand_and_labels', 'far_t_and_gps',
+         'far_t_and_nscand_and_gps', 'far_t_and_labels_and_gps',
+         'far_t_and_nscand_and_labels_and_gps']),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq', 'far_t_and_gps',
+         'far_t_and_labelq_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'far_t_and_nscand_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'far_t_and_nscand_and_labelq', 'far_t_and_gps',
+         'far_t_and_nscand_and_gps', 'far_t_and_labelq_and_gps',
+         'far_t_and_nscand_and_labelq_and_gps']),
+    ## NSCAND = changing ------------------------------
+    ### No labels -------------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, False, None,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, True, None,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand',
+         'far_t_and_gps', 'nscand_and_gps', 'far_t_and_nscand_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, False, None,
+        ['far_t_only']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, True, None,
+        ['far_t_only', 'far_t_and_gps']),
+    ### With labels -----------------------------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, False, DEFAULT_LABELS,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand',
+         'far_t_and_labels', 'nscand_and_labels',
+         'far_t_and_nscand_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, True, DEFAULT_LABELS,
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labels',
+         'nscand_and_labels', 'far_t_and_nscand_and_labels', 'far_t_and_gps',
+         'nscand_and_gps', 'far_t_and_nscand_and_gps',
+         'far_t_and_labels_and_gps', 'nscand_and_labels_and_gps',
+         'far_t_and_nscand_and_labels_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, False, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, True, DEFAULT_LABELS,
+        ['far_t_only', 'far_t_and_labels', 'far_t_and_gps',
+         'far_t_and_labels_and_gps']),
+    ### With labels matching label query --------------
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'nscand_and_labelq', 'far_t_and_nscand_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labelq',
+         'nscand_and_labelq', 'far_t_and_nscand_and_labelq', 'far_t_and_gps',
+         'nscand_and_gps', 'far_t_and_nscand_and_gps',
+         'far_t_and_labelq_and_gps', 'nscand_and_labelq_and_gps',
+         'far_t_and_nscand_and_labelq_and_gps']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, False,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq']),
+    (DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, True,
+        DEFAULT_LABEL_QUERY['labels'],
+        ['far_t_only', 'far_t_and_labelq', 'far_t_and_gps',
+         'far_t_and_labelq_and_gps']),
+]
+
+SUPEREVENT_LABEL_ADDED_ALERT_DATA = [
+    # Label criteria not met --------------------------
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABELS[0], None, []),
+    (None, True, DEFAULT_LABELS[0], None, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], None, []),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], None, []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], None, []),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], None, []),
+    # Label criteria met ------------------------------
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
+        ['labels_only']),
+    (None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
+        ['nscand_and_labels', 'labels_only']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
+        ['labels_only']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
+        ['nscand_and_labels', 'labels_only']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
+        ['far_t_and_labels', 'labels_only']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
+        ['far_t_and_labels', 'nscand_and_labels', 'labels_only',
+         'far_t_and_nscand_and_labels']),
+    # Label criteria met, some additional labels previously added
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
+        ['labels_only']),
+    (None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
+        ['nscand_and_labels', 'labels_only']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL], ['labels_only']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL],
+        ['nscand_and_labels', 'labels_only']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL],
+        ['far_t_and_labels', 'labels_only']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL],
+        ['far_t_and_labels', 'nscand_and_labels', 'labels_only',
+         'far_t_and_nscand_and_labels']),
+    # Label criteria previously met -------------------
+    ## FAR = None -------------------------------------
+    (None, False, RANDOM_LABEL, DEFAULT_LABELS, []),
+    (None, True, RANDOM_LABEL, DEFAULT_LABELS, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABELS, []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABELS, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABELS, []),
+    # Label query criteria not met --------------------
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABEL_QUERY['labels'][0], None, []),
+    (None, True, DEFAULT_LABEL_QUERY['labels'][0], None, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, []),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, []),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, []),
+    # Label query criteria met -------------------------
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], ['labelq_only']),
+    (None, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:],
+        ['nscand_and_labelq', 'labelq_only']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], ['labelq_only']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:],
+        ['nscand_and_labelq', 'labelq_only']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:],
+        ['far_t_and_labelq', 'labelq_only']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:],
+        ['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
+         'far_t_and_nscand_and_labelq']),
+    # Label query criteria met, some additional labels previously added
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
+        ['labelq_only']),
+    (None, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
+        ['nscand_and_labelq', 'labelq_only']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
+        ['labelq_only']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
+        ['nscand_and_labelq', 'labelq_only']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
+        ['far_t_and_labelq', 'labelq_only']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
+        ['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
+         'far_t_and_nscand_and_labelq']),
+    # Label query criteria previously met -------------
+    ## FAR = None -------------------------------------
+    (None, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], []),
+    (None, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], []),
+]
+
+# Params: far,nscand,new_label,old_labels,use_default_gps,notif_descs
+EVENT_LABEL_ADDED_ALERT_DATA = [
+    # Label criteria not met --------------------------
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABELS[0], None, False, []),
+    (None, False, DEFAULT_LABELS[0], None, True, []),
+    (None, True, DEFAULT_LABELS[0], None, False, []),
+    (None, True, DEFAULT_LABELS[0], None, True, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], None, False, []),
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], None, True, []),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], None, False, []),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], None, True, []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], None, False, []),
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], None, True, []),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], None, False, []),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], None, True, []),
+    # Label criteria met ------------------------------
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
+        ['labels_only']),
+    (None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
+        ['labels_only', 'labels_and_gps']),
+    (None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
+        ['nscand_and_labels', 'labels_only']),
+    (None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
+        ['nscand_and_labels', 'labels_only', 'nscand_and_labels_and_gps',
+         'labels_and_gps']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
+        ['labels_only']),
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
+        ['labels_only', 'labels_and_gps']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
+        ['nscand_and_labels', 'labels_only']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
+        ['nscand_and_labels', 'labels_only', 'nscand_and_labels_and_gps',
+         'labels_and_gps']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
+        ['far_t_and_labels', 'labels_only']),
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
+        ['far_t_and_labels', 'labels_only', 'far_t_and_labels_and_gps',
+         'labels_and_gps']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
+        ['far_t_and_labels', 'nscand_and_labels', 'labels_only',
+         'far_t_and_nscand_and_labels']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
+        ['far_t_and_labels', 'nscand_and_labels', 'labels_only',
+         'far_t_and_nscand_and_labels', 'far_t_and_labels_and_gps',
+         'nscand_and_labels_and_gps', 'labels_and_gps',
+         'far_t_and_nscand_and_labels_and_gps']),
+    # Label criteria met, some additional labels previously added
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
+        False, ['labels_only']),
+    (None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
+        True, ['labels_only', 'labels_and_gps']),
+    (None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
+        False, ['nscand_and_labels', 'labels_only']),
+    (None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
+        True, ['nscand_and_labels', 'labels_only', 'labels_and_gps',
+               'nscand_and_labels_and_gps']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL], False, ['labels_only']),
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL], True,
+        ['labels_only', 'labels_and_gps']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL], False,
+        ['nscand_and_labels', 'labels_only']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL], True,
+        ['nscand_and_labels', 'labels_only', 'labels_and_gps',
+         'nscand_and_labels_and_gps']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL], False,
+        ['far_t_and_labels', 'labels_only']),
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL], True,
+        ['far_t_and_labels', 'labels_only', 'labels_and_gps',
+         'far_t_and_labels_and_gps']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL], False,
+        ['far_t_and_labels', 'nscand_and_labels', 'labels_only',
+         'far_t_and_nscand_and_labels']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0],
+        DEFAULT_LABELS[1:] + [RANDOM_LABEL], True,
+        ['far_t_and_labels', 'nscand_and_labels', 'labels_only',
+         'far_t_and_nscand_and_labels', 'far_t_and_labels_and_gps',
+         'nscand_and_labels_and_gps', 'labels_and_gps',
+         'far_t_and_nscand_and_labels_and_gps']),
+    # Label criteria previously met -------------------
+    ## FAR = None -------------------------------------
+    (None, False, RANDOM_LABEL, DEFAULT_LABELS, False, []),
+    (None, False, RANDOM_LABEL, DEFAULT_LABELS, True, []),
+    (None, True, RANDOM_LABEL, DEFAULT_LABELS, False, []),
+    (None, True, RANDOM_LABEL, DEFAULT_LABELS, True, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABELS, False, []),
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABELS, True, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABELS, False, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABELS, True, []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABELS, False, []),
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABELS, True, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABELS, False, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABELS, True, []),
+    # Label query criteria not met --------------------
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABEL_QUERY['labels'][0], None, False, []),
+    (None, False, DEFAULT_LABEL_QUERY['labels'][0], None, True, []),
+    (None, True, DEFAULT_LABEL_QUERY['labels'][0], None, False, []),
+    (None, True, DEFAULT_LABEL_QUERY['labels'][0], None, True, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, False,
+        []),
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, True,
+        []),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, False,
+        []),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, True,
+        []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, False,
+        []),
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, True,
+        []),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, False,
+        []),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, True,
+        []),
+    # Label query criteria met -------------------------
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], False, ['labelq_only']),
+    (None, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], True,
+        ['labelq_only', 'labelq_and_gps']),
+    (None, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], False,
+        ['nscand_and_labelq', 'labelq_only']),
+    (None, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], True,
+        ['nscand_and_labelq', 'labelq_only', 'labelq_and_gps',
+         'nscand_and_labelq_and_gps']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], False, ['labelq_only']),
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], True,
+        ['labelq_only', 'labelq_and_gps']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], False,
+        ['nscand_and_labelq', 'labelq_only']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], True,
+        ['nscand_and_labelq', 'labelq_only', 'labelq_and_gps',
+         'nscand_and_labelq_and_gps']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], False,
+        ['far_t_and_labelq', 'labelq_only']),
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], True,
+        ['far_t_and_labelq', 'labelq_only', 'labelq_and_gps',
+         'far_t_and_labelq_and_gps']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], False,
+        ['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
+         'far_t_and_nscand_and_labelq']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:], True,
+        ['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
+         'far_t_and_nscand_and_labelq', 'far_t_and_labelq_and_gps',
+         'nscand_and_labelq_and_gps', 'labelq_and_gps',
+         'far_t_and_nscand_and_labelq_and_gps']),
+    # Label query criteria met, some additional labels previously added
+    ## FAR = None -------------------------------------
+    (None, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
+        ['labelq_only']),
+    (None, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
+        ['labelq_only', 'labelq_and_gps']),
+    (None, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
+        ['nscand_and_labelq', 'labelq_only']),
+    (None, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
+        ['nscand_and_labelq', 'labelq_only', 'labelq_and_gps',
+         'nscand_and_labelq_and_gps']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
+        ['labelq_only']),
+    (DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
+        ['labelq_only', 'labelq_and_gps']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
+        ['nscand_and_labelq', 'labelq_only']),
+    (DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
+        ['nscand_and_labelq', 'labelq_only', 'labelq_and_gps',
+         'nscand_and_labelq_and_gps']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
+        ['far_t_and_labelq', 'labelq_only']),
+    (DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
+        ['far_t_and_labelq', 'labelq_only', 'labelq_and_gps',
+         'far_t_and_labelq_and_gps']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
+        ['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
+         'far_t_and_nscand_and_labelq']),
+    (DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
+        DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
+        ['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
+         'far_t_and_nscand_and_labelq', 'far_t_and_labelq_and_gps',
+         'nscand_and_labelq_and_gps', 'labelq_and_gps',
+         'far_t_and_nscand_and_labelq_and_gps']),
+    # Label query criteria previously met -------------
+    ## FAR = None -------------------------------------
+    (None, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], False, []),
+    (None, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], True, []),
+    (None, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], False, []),
+    (None, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], True, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        False, []),
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        True, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        False, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        True, []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        False, []),
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        True, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        False, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
+        True, []),
+]
+
+SUPEREVENT_LABEL_REMOVED_ALERT_DATA = [
+    # Label criteria not met --------------------------
+    ## FAR = None -------------------------------------
+    (None, False, RANDOM_LABEL, None, []),
+    (None, True, RANDOM_LABEL, None, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, None, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, None, []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, None, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, None, []),
+    # Label criteria met ------------------------------
+    ## FAR = None -------------------------------------
+    (None, False, 'L7', ['L5', 'L6'], ['labelq2_only']),
+    (None, True, 'L7', ['L5', 'L6'],
+        ['nscand_and_labelq2', 'labelq2_only']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6'],
+        ['labelq2_only']),
+    (DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6'],
+        ['nscand_and_labelq2', 'labelq2_only']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6'],
+        ['far_t_and_labelq2', 'labelq2_only']),
+    (DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6'],
+        ['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
+         'far_t_and_nscand_and_labelq2']),
+    # Label criteria met, some additional labels previously added
+    ## FAR = None -------------------------------------
+    (None, False, 'L7', ['L5', 'L6', RANDOM_LABEL],
+        ['labelq2_only']),
+    (None, True, 'L7', ['L5', 'L6', RANDOM_LABEL],
+        ['nscand_and_labelq2', 'labelq2_only']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL],
+        ['labelq2_only']),
+    (DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL],
+        ['nscand_and_labelq2', 'labelq2_only']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL],
+        ['far_t_and_labelq2', 'labelq2_only']),
+    (DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL],
+        ['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
+         'far_t_and_nscand_and_labelq2']),
+    # Label criteria previously met -------------------
+    ## FAR = None -------------------------------------
+    (None, False, RANDOM_LABEL, ['L5', 'L6'], []),
+    (None, True, RANDOM_LABEL, ['L5', 'L6'], []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, ['L5', 'L6'], []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, ['L5', 'L6'], []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, ['L5', 'L6'], []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, ['L5', 'L6'], []),
+]
+
+# Params: far,nscand,removed_label,labels,use_default_gps,notif_descs
+EVENT_LABEL_REMOVED_ALERT_DATA = [
+    # Label criteria not met --------------------------
+    ## FAR = None -------------------------------------
+    (None, False, RANDOM_LABEL, None, False, []),
+    (None, False, RANDOM_LABEL, None, True, []),
+    (None, True, RANDOM_LABEL, None, False, []),
+    (None, True, RANDOM_LABEL, None, True, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, None, False, []),
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, None, True, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, None, False, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, None, True, []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, None, False, []),
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, None, True, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, None, False, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, None, True, []),
+    # Label criteria met ------------------------------
+    ## FAR = None -------------------------------------
+    (None, False, 'L7', ['L5', 'L6'], False, ['labelq2_only']),
+    (None, False, 'L7', ['L5', 'L6'], True,
+        ['labelq2_only', 'labelq2_and_gps']),
+    (None, True, 'L7', ['L5', 'L6'], False,
+        ['nscand_and_labelq2', 'labelq2_only']),
+    (None, True, 'L7', ['L5', 'L6'], True,
+        ['nscand_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
+         'nscand_and_labelq2_and_gps']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6'], False,
+        ['labelq2_only']),
+    (DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6'], True,
+        ['labelq2_only', 'labelq2_and_gps']),
+    (DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6'], False,
+        ['nscand_and_labelq2', 'labelq2_only']),
+    (DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6'], True,
+        ['nscand_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
+         'nscand_and_labelq2_and_gps']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6'], False,
+        ['far_t_and_labelq2', 'labelq2_only']),
+    (DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6'], True,
+        ['far_t_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
+         'far_t_and_labelq2_and_gps']),
+    (DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6'], False,
+        ['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
+         'far_t_and_nscand_and_labelq2']),
+    (DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6'], True,
+        ['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
+         'far_t_and_nscand_and_labelq2', 'labelq2_and_gps',
+         'far_t_and_labelq2_and_gps', 'nscand_and_labelq2_and_gps',
+         'far_t_and_nscand_and_labelq2_and_gps']),
+    # Label criteria met, some additional labels previously added
+    ## FAR = None -------------------------------------
+    (None, False, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
+        ['labelq2_only']),
+    (None, False, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
+        ['labelq2_only', 'labelq2_and_gps',]),
+    (None, True, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
+        ['nscand_and_labelq2', 'labelq2_only']),
+    (None, True, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
+        ['nscand_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
+         'nscand_and_labelq2_and_gps']),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
+        ['labelq2_only']),
+    (DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
+        ['labelq2_only', 'labelq2_and_gps']),
+    (DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
+        ['nscand_and_labelq2', 'labelq2_only']),
+    (DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
+        ['nscand_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
+         'nscand_and_labelq2_and_gps']),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
+        ['far_t_and_labelq2', 'labelq2_only']),
+    (DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
+        ['far_t_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
+         'far_t_and_labelq2_and_gps']),
+    (DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
+        ['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
+         'far_t_and_nscand_and_labelq2']),
+    (DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
+        ['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
+         'far_t_and_nscand_and_labelq2', 'labelq2_and_gps',
+         'far_t_and_labelq2_and_gps', 'nscand_and_labelq2_and_gps',
+         'far_t_and_nscand_and_labelq2_and_gps']),
+    # Label criteria previously met -------------------
+    ## FAR = None -------------------------------------
+    (None, False, RANDOM_LABEL, ['L5', 'L6'], False, []),
+    (None, False, RANDOM_LABEL, ['L5', 'L6'], True, []),
+    (None, True, RANDOM_LABEL, ['L5', 'L6'], False, []),
+    (None, True, RANDOM_LABEL, ['L5', 'L6'], True, []),
+    ## FAR > threshold --------------------------------
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, ['L5', 'L6'], False, []),
+    (DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, ['L5', 'L6'], True, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, ['L5', 'L6'], False, []),
+    (DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, ['L5', 'L6'], True, []),
+    ## FAR < threshold --------------------------------
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, ['L5', 'L6'], False, []),
+    (DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, ['L5', 'L6'], True, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, ['L5', 'L6'], False, []),
+    (DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, ['L5', 'L6'], True, []),
+]
+
+
+###############################################################################
+# TESTS #######################################################################
+###############################################################################
+# Superevent tests ------------------------------------------------------------
+@pytest.mark.parametrize("far,nscand,notif_descs",
+                         SUPEREVENT_CREATION_ALERT_DATA)
+@pytest.mark.django_db
+def test_superevent_creation_alerts(
+    superevent, superevent_notifications,
+    far, nscand, notif_descs,
+):
+
+    # Set up superevent state
+    superevent.preferred_event.far = far
+    superevent.preferred_event.is_ns_candidate = \
+        types.MethodType(lambda self: nscand, superevent.preferred_event)
+
+    # Set up recipient getter
+    recipient_getter = CreationRecipientGetter(superevent)
+    recipient_getter.queryset = superevent_notifications
+
+    # Get notifications
+    matched_notifications = recipient_getter.get_notifications()
+
+    # Test results
+    for desc in notif_descs:
+        assert matched_notifications.filter(description=desc).exists()
+    assert matched_notifications.count() == len(notif_descs)
+
+
+@pytest.mark.parametrize("old_far,far,old_nscand,nscand,labels,notif_descs",
+                         SUPEREVENT_UPDATE_ALERT_DATA)
+@pytest.mark.django_db
+def test_superevent_update_alerts(
+    superevent, superevent_notifications,
+    old_far, far, old_nscand, nscand, labels, notif_descs,
+):
+
+    # Set up superevent state
+    superevent.preferred_event.far = far
+    superevent.preferred_event.is_ns_candidate = \
+        types.MethodType(lambda self: nscand, superevent.preferred_event)
+    if labels is not None:
+        for label_name in labels:
+            label, _ = Label.objects.get_or_create(name=label_name)
+            superevent.labelling_set.create(label=label,
+                                            creator=superevent.submitter)
+
+    # Set up recipient getter
+    recipient_getter = UpdateRecipientGetter(superevent, old_far=old_far,
+                                             old_nscand=old_nscand)
+    recipient_getter.queryset = superevent_notifications
+
+    # Get notifications
+    matched_notifications = recipient_getter.get_notifications()
+
+    # Test results
+    assert matched_notifications.count() == len(notif_descs)
+    for desc in notif_descs:
+        assert matched_notifications.filter(description=desc).exists()
+
+
+@pytest.mark.parametrize("far,nscand,new_label,old_labels,notif_descs",
+                         SUPEREVENT_LABEL_ADDED_ALERT_DATA)
+@pytest.mark.django_db
+def test_superevent_label_added_alerts(
+    superevent, superevent_notifications,
+    far, nscand, new_label, old_labels, notif_descs,
+):
+
+    # Set up superevent state
+    superevent.preferred_event.far = far
+    superevent.preferred_event.is_ns_candidate = \
+        types.MethodType(lambda self: nscand, superevent.preferred_event)
+    if old_labels is not None:
+        for label_name in old_labels:
+            label, _ = Label.objects.get_or_create(name=label_name)
+            superevent.labelling_set.create(label=label,
+                                            creator=superevent.submitter)
+    # Add new label
+    label_obj, _ = Label.objects.get_or_create(name=new_label)
+    superevent.labelling_set.create(label=label_obj,
+                                    creator=superevent.submitter)
+
+    # Set up recipient getter
+    recipient_getter = LabelAddedRecipientGetter(superevent, label=label_obj)
+    recipient_getter.queryset = superevent_notifications
+
+    # Get notifications
+    matched_notifications = recipient_getter.get_notifications()
+
+    # Test results
+    assert matched_notifications.count() == len(notif_descs)
+    for desc in notif_descs:
+        assert matched_notifications.filter(description=desc).exists()
+
+
+@pytest.mark.parametrize("far,nscand,removed_label,labels,notif_descs",
+                         SUPEREVENT_LABEL_REMOVED_ALERT_DATA)
+@pytest.mark.django_db
+def test_superevent_label_removed_alerts(
+    superevent, superevent_notifications,
+    far, nscand, removed_label, labels, notif_descs,
+):
+    # NOTE: labels being removed should never result in alerts for
+    # notifications which specify a label criteria (instead of a label query)
+
+    # Set up superevent state
+    superevent.preferred_event.far = far
+    superevent.preferred_event.is_ns_candidate = \
+        types.MethodType(lambda self: nscand, superevent.preferred_event)
+    if labels is not None:
+        for label_name in labels:
+            label, _ = Label.objects.get_or_create(name=label_name)
+            superevent.labelling_set.create(label=label,
+                                            creator=superevent.submitter)
+
+    # Add new label
+    label_obj, _ = Label.objects.get_or_create(name=removed_label)
+
+    # Set up recipient getter
+    recipient_getter = LabelRemovedRecipientGetter(superevent, label=label_obj)
+    recipient_getter.queryset = superevent_notifications
+
+    # Get notifications
+    matched_notifications = recipient_getter.get_notifications()
+
+    # Test results
+    assert matched_notifications.count() == len(notif_descs)
+    for desc in notif_descs:
+        assert matched_notifications.filter(description=desc).exists()
+
+
+# Event tests -----------------------------------------------------------------
+@pytest.mark.parametrize("far,nscand,use_default_gps,notif_descs",
+                         EVENT_CREATION_ALERT_DATA)
+@pytest.mark.django_db
+def test_event_creation_alerts(
+    event, event_notifications, far, nscand, use_default_gps, notif_descs,
+):
+
+    # Set up event state
+    event.far = far
+    event.is_ns_candidate = types.MethodType(lambda self: nscand, event)
+    if use_default_gps:
+        event.group = Group.objects.get(name=DEFAULT_GROUP)
+        event.pipeline = Pipeline.objects.get(name=DEFAULT_PIPELINE)
+        event.search = Search.objects.get(name=DEFAULT_SEARCH)
+        event.save()
+
+    # Set up recipient getter
+    recipient_getter = CreationRecipientGetter(event)
+    recipient_getter.queryset = event_notifications
+
+    # Get notifications
+    matched_notifications = recipient_getter.get_notifications()
+
+    # Test results
+    for desc in notif_descs:
+        assert matched_notifications.filter(description=desc).exists()
+    assert matched_notifications.count() == len(notif_descs)
+
+
+@pytest.mark.parametrize(
+    "old_far,far,old_nscand,nscand,use_default_gps,labels,notif_descs",
+    EVENT_UPDATE_ALERT_DATA
 )
-@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,
+@pytest.mark.django_db
+def test_event_update_alerts(
+    event, event_notifications, old_far, far, old_nscand, nscand,
+    use_default_gps, labels, notif_descs,
+):
+    # Set up event state
+    event.far = far
+    event.is_ns_candidate = types.MethodType(lambda self: nscand, event)
+    if use_default_gps:
+        event.group = Group.objects.get(name=DEFAULT_GROUP)
+        event.pipeline = Pipeline.objects.get(name=DEFAULT_PIPELINE)
+        event.search = Search.objects.get(name=DEFAULT_SEARCH)
+        event.save()
+    if labels is not None:
+        for label_name in labels:
+            label, _ = Label.objects.get_or_create(name=label_name)
+            event.labelling_set.create(label=label, creator=event.submitter)
+
+    # Set up recipient getter
+    recipient_getter = UpdateRecipientGetter(event, old_far=old_far,
+                                             old_nscand=old_nscand)
+    recipient_getter.queryset = event_notifications
+
+    # Get notifications
+    matched_notifications = recipient_getter.get_notifications()
+
+    # Test results
+    assert matched_notifications.count() == len(notif_descs)
+    for desc in notif_descs:
+        assert matched_notifications.filter(description=desc).exists()
+
+
+@pytest.mark.parametrize(
+    "far,nscand,new_label,old_labels,use_default_gps,notif_descs",
+    EVENT_LABEL_ADDED_ALERT_DATA
 )
-@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,
+@pytest.mark.django_db
+def test_event_label_added_alerts(
+    event, event_notifications, far, nscand, new_label, old_labels,
+    use_default_gps, notif_descs,
+):
+    # Set up event state
+    event.far = far
+    event.is_ns_candidate = types.MethodType(lambda self: nscand, event)
+    if use_default_gps:
+        event.group = Group.objects.get(name=DEFAULT_GROUP)
+        event.pipeline = Pipeline.objects.get(name=DEFAULT_PIPELINE)
+        event.search = Search.objects.get(name=DEFAULT_SEARCH)
+        event.save()
+    if old_labels is not None:
+        for label_name in old_labels:
+            label, _ = Label.objects.get_or_create(name=label_name)
+            event.labelling_set.create(label=label, creator=event.submitter)
+
+    # Add new label
+    label_obj, _ = Label.objects.get_or_create(name=new_label)
+    event.labelling_set.create(label=label_obj, creator=event.submitter)
+
+    # Set up recipient getter
+    recipient_getter = LabelAddedRecipientGetter(event, label=label_obj)
+    recipient_getter.queryset = event_notifications
+
+    # Get notifications
+    matched_notifications = recipient_getter.get_notifications()
+
+    # Test results
+    assert matched_notifications.count() == len(notif_descs)
+    for desc in notif_descs:
+        assert matched_notifications.filter(description=desc).exists()
+
+
+@pytest.mark.parametrize(
+    "far,nscand,removed_label,labels,use_default_gps,notif_descs",
+    EVENT_LABEL_REMOVED_ALERT_DATA
 )
-@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()
+@pytest.mark.django_db
+def test_event_label_removed_alerts(
+    event, event_notifications, far, nscand, removed_label, labels,
+    use_default_gps, notif_descs,
+):
+    # Set up event state
+    event.far = far
+    event.is_ns_candidate = types.MethodType(lambda self: nscand, event)
+    if use_default_gps:
+        event.group = Group.objects.get(name=DEFAULT_GROUP)
+        event.pipeline = Pipeline.objects.get(name=DEFAULT_PIPELINE)
+        event.search = Search.objects.get(name=DEFAULT_SEARCH)
+        event.save()
+    if labels is not None:
+        for label_name in labels:
+            label, _ = Label.objects.get_or_create(name=label_name)
+            event.labelling_set.create(label=label, creator=event.submitter)
+
+    # Add new label
+    label_obj, _ = Label.objects.get_or_create(name=removed_label)
+
+    # Set up recipient getter
+    recipient_getter = LabelRemovedRecipientGetter(event, label=label_obj)
+    recipient_getter.queryset = event_notifications
+
+    # Get notifications
+    matched_notifications = recipient_getter.get_notifications()
+
+    # Test results
+    assert matched_notifications.count() == len(notif_descs)
+    for desc in notif_descs:
+        assert matched_notifications.filter(description=desc).exists()
+
+
+# Other tests -----------------------------------------------------------------
+@pytest.mark.django_db
+def test_complex_label_query(superevent):
+    # NOTE: L1 & ~L2 | L3 == L1 & (~L2 | L3)
+    n = Notification.objects.create(
+        label_query='L1 & ~L2 | L3',
+        user=superevent.submitter,
+        description='test notification'
+    )
+
+    # Set up superevent labels
+    for label_name in ['L1', 'L2', 'L3']:
+        l, _ = Label.objects.get_or_create(name=label_name)
+        n.labels.add(l)
+
+    # Test label added recipients for L1 being added
+    l1 = Label.objects.get(name='L1')
+    superevent.labelling_set.create(creator=superevent.submitter, label=l1)
+    recipient_getter = LabelAddedRecipientGetter(superevent, label=l1)
+    matched_notifications = recipient_getter.get_notifications()
+    assert matched_notifications.count() == 1
+    assert matched_notifications.first().description == n.description
+
+    # Test label added recipients for L3 being added (L1 already added)
+    l3 = Label.objects.get(name='L3')
+    superevent.labelling_set.create(creator=superevent.submitter, label=l3)
+    recipient_getter = LabelAddedRecipientGetter(superevent, label=l3)
+    matched_notifications = recipient_getter.get_notifications()
+    assert matched_notifications.count() == 1
+    assert matched_notifications.first().description == n.description
+
+    # Test label added recipients for L3 being added (no L1)
+    l3 = Label.objects.get(name='L3')
+    superevent.labels.clear()
+    superevent.labelling_set.create(creator=superevent.submitter, label=l3)
+    recipient_getter = LabelAddedRecipientGetter(superevent, label=l3)
+    matched_notifications = recipient_getter.get_notifications()
+    assert matched_notifications.count() == 0
+
+    # Test label added recipients for L1 being added (with L2)
+    l1 = Label.objects.get(name='L1')
+    l2 = Label.objects.get(name='L2')
+    superevent.labels.clear()
+    superevent.labelling_set.create(creator=superevent.submitter, label=l2)
+    superevent.labelling_set.create(creator=superevent.submitter, label=l1)
+    recipient_getter = LabelAddedRecipientGetter(superevent, label=l1)
+    matched_notifications = recipient_getter.get_notifications()
+    assert matched_notifications.count() == 0
+
+    # Test label added recipients for L3 being added (with L1 and L2)
+    l1 = Label.objects.get(name='L1')
+    l2 = Label.objects.get(name='L2')
+    l3 = Label.objects.get(name='L3')
+    superevent.labels.clear()
+    superevent.labelling_set.create(creator=superevent.submitter, label=l1)
+    superevent.labelling_set.create(creator=superevent.submitter, label=l2)
+    superevent.labelling_set.create(creator=superevent.submitter, label=l3)
+    recipient_getter = LabelAddedRecipientGetter(superevent, label=l3)
+    matched_notifications = recipient_getter.get_notifications()
+    assert matched_notifications.count() == 1
+    assert matched_notifications.first().description == n.description
+
+    # Test label removed recipients for L2 being removed (with L1)
+    l1 = Label.objects.get(name='L1')
+    l2 = Label.objects.get(name='L2')
+    superevent.labels.clear()
+    superevent.labelling_set.create(creator=superevent.submitter, label=l1)
+    recipient_getter = LabelRemovedRecipientGetter(superevent, label=l2)
+    matched_notifications = recipient_getter.get_notifications()
+    assert matched_notifications.count() == 1
+    assert matched_notifications.first().description == n.description
+
+    # Test label removed recipients for L3 being removed (with L1)
+    l1 = Label.objects.get(name='L1')
+    l3 = Label.objects.get(name='L3')
+    superevent.labels.clear()
+    superevent.labelling_set.create(creator=superevent.submitter, label=l1)
+    recipient_getter = LabelRemovedRecipientGetter(superevent, label=l3)
+    matched_notifications = recipient_getter.get_notifications()
+    assert matched_notifications.count() == 1
+    assert matched_notifications.first().description == n.description
+
+    # Test label removed recipients for L2 being removed (with L1 and L3)
+    l1 = Label.objects.get(name='L1')
+    l2 = Label.objects.get(name='L2')
+    l3 = Label.objects.get(name='L3')
+    superevent.labels.clear()
+    superevent.labelling_set.create(creator=superevent.submitter, label=l1)
+    superevent.labelling_set.create(creator=superevent.submitter, label=l3)
+    recipient_getter = LabelAddedRecipientGetter(superevent, label=l2)
+    matched_notifications = recipient_getter.get_notifications()
+    # TODO: this shouldn't match, since it was already triggered on the
+    # previous state, where L1, L2, and L3 were all applied (but it does)
+    #assert matched_notifications.count() == 0
+
+
+@pytest.mark.django_db
+def test_label_removal_with_only_labels_list(superevent):
+    n = Notification.objects.create(
+        user=superevent.submitter,
+        description='test notification'
+    )
+
+    # Set up superevent labels
+    for label_name in ['L1', 'L2']:
+        l, _ = Label.objects.get_or_create(name=label_name)
+        n.labels.add(l)
+
+    # Test label added recipients for L1 being added
+    l1 = Label.objects.get(name='L1')
+    l2 = Label.objects.get(name='L2')
+    l3, _ = Label.objects.get_or_create(name='L3')
+    superevent.labelling_set.create(creator=superevent.submitter, label=l1)
+    superevent.labelling_set.create(creator=superevent.submitter, label=l2)
+    recipient_getter = LabelRemovedRecipientGetter(superevent, label=l3)
+    matched_notifications = recipient_getter.get_notifications()
+    assert matched_notifications.count() == 0
+
+
+@pytest.mark.parametrize("old_far,new_far,far_t,match",
+                         [(1, 0.5, 0.5, False), (0.5, 0.25, 0.5, True)])
+@pytest.mark.django_db
+def test_update_alert_far_threshold_edge(superevent, old_far, new_far, far_t,
+                                         match):
+    n = Notification.objects.create(
+        user=superevent.submitter,
+        far_threshold=far_t,
+        description='test notification'
+    )
+
+    # Set up superevent state
+    superevent.preferred_event.far = new_far
+
+    # Set up recipient getter
+    recipient_getter = UpdateRecipientGetter(superevent, old_far=old_far,
+                                             old_nscand=False)
+
+    # Get notifications
+    matched_notifications = recipient_getter.get_notifications()
+    if match:
+        assert matched_notifications.count() == 1
+        assert matched_notifications.first().description == n.description
+    else:
+        assert matched_notifications.count() == 0
-- 
GitLab