Repository: django-oscar/django-oscar

Issue:
Product notifications for anonymous users can potentially be used to spam someone
There's nothing to stop a baddie bombarding a victim with lots of product alert notification emails. 
It'd be better if django-oscar only sent a maximum of one product alert to an email address until the user confirms their email, after which they could set up more.


Discussion:

Resolving patch:
diff --git a/src/oscar/apps/customer/abstract_models.py b/src/oscar/apps/customer/abstract_models.py
--- a/src/oscar/apps/customer/abstract_models.py
+++ b/src/oscar/apps/customer/abstract_models.py
@@ -340,7 +340,7 @@ class AbstractProductAlert(models.Model):
     key = models.CharField(_("Key"), max_length=128, blank=True, db_index=True)
 
     # An alert can have two different statuses for authenticated
-    # users ``ACTIVE`` and ``INACTIVE`` and anonymous users have an
+    # users ``ACTIVE`` and ``CANCELLED`` and anonymous users have an
     # additional status ``UNCONFIRMED``. For anonymous users a confirmation
     # and unsubscription key are generated when an instance is saved for
     # the first time and can be used to confirm and unsubscribe the
@@ -379,7 +379,7 @@ def can_be_confirmed(self):
 
     @property
     def can_be_cancelled(self):
-        return self.status == self.ACTIVE
+        return self.status in (self.ACTIVE, self.UNCONFIRMED)
 
     @property
     def is_cancelled(self):
diff --git a/src/oscar/apps/customer/forms.py b/src/oscar/apps/customer/forms.py
--- a/src/oscar/apps/customer/forms.py
+++ b/src/oscar/apps/customer/forms.py
@@ -406,6 +406,16 @@ def clean(self):
             else:
                 raise forms.ValidationError(_(
                     "There is already an active stock alert for %s") % email)
+
+            # Check that the email address hasn't got other unconfirmed alerts.
+            # If they do then we don't want to spam them with more until they
+            # have confirmed or cancelled the existing alert.
+            if ProductAlert.objects.filter(email__iexact=email,
+                                    status=ProductAlert.UNCONFIRMED).count():
+                raise forms.ValidationError(_(
+                    "%s has been sent a confirmation email for another product "
+                    "alert on this site. Please confirm or cancel that request "
+                    "before signing up for more alerts.") % email)
         elif user_is_authenticated(self.user):
             try:
                 ProductAlert.objects.get(product=self.product,


Test patch:
diff --git a/tests/functional/customer/test_alert.py b/tests/functional/customer/test_alert.py
--- a/tests/functional/customer/test_alert.py
+++ b/tests/functional/customer/test_alert.py
@@ -3,6 +3,8 @@
 import warnings
 
 from django_webtest import WebTest
+
+from django.contrib.auth.models import AnonymousUser
 from django.core.urlresolvers import reverse
 from django.core import mail
 from django.test import TestCase
@@ -10,25 +12,39 @@
 
 from oscar.apps.customer.alerts.utils import (send_alert_confirmation,
     send_product_alerts)
+from oscar.apps.customer.forms import ProductAlertForm
 from oscar.apps.customer.models import ProductAlert
-from oscar.test.factories import create_product, create_stockrecord
-from oscar.test.factories import UserFactory
+from oscar.test.factories import (
+    create_product, create_stockrecord, ProductAlertFactory, UserFactory)
 
 
 class TestAUser(WebTest):
 
+    def setUp(self):
+        self.user = UserFactory()
+        self.product = create_product(num_in_stock=0)
+
     def test_can_create_a_stock_alert(self):
-        user = UserFactory()
-        product = create_product(num_in_stock=0)
-        product_page = self.app.get(product.get_absolute_url(), user=user)
+        product_page = self.app.get(self.product.get_absolute_url(), user=self.user)
         form = product_page.forms['alert_form']
         form.submit()
 
-        alerts = ProductAlert.objects.filter(user=user)
+        alerts = ProductAlert.objects.filter(user=self.user)
         self.assertEqual(1, len(alerts))
         alert = alerts[0]
         self.assertEqual(ProductAlert.ACTIVE, alert.status)
-        self.assertEqual(alert.product, product)
+        self.assertEqual(alert.product, self.product)
+
+    def test_cannot_create_multiple_alerts_for_one_product(self):
+        alert = ProductAlertFactory(user=self.user, product=self.product,
+                                    status = ProductAlert.ACTIVE)
+        # Alert form should not allow creation of additional alerts.
+        form = ProductAlertForm(user=self.user, product=self.product, data={})
+
+        self.assertFalse(form.is_valid())
+        self.assertIn(
+            "You already have an active alert for this product",
+            form.errors['__all__'][0])
 
 
 class TestAUserWithAnActiveStockAlert(WebTest):
@@ -87,6 +103,47 @@ def test_can_create_a_stock_alert(self):
         self.assertEqual(ProductAlert.UNCONFIRMED, alert.status)
         self.assertEqual(alert.product, product)
 
+    def test_can_cancel_unconfirmed_stock_alert(self):
+        alert = ProductAlertFactory(
+            user=None, email='john@smith.com', status=ProductAlert.UNCONFIRMED)
+        self.app.get(
+            reverse('customer:alerts-cancel-by-key', kwargs={'key': alert.key}))
+        alert.refresh_from_db()
+        self.assertTrue(alert.is_cancelled)
+
+    def test_cannot_create_multiple_alerts_for_one_product(self):
+        product = create_product(num_in_stock=0)
+        alert = ProductAlertFactory(user=None, product=product,
+                                    email='john@smith.com')
+        alert.status = ProductAlert.ACTIVE
+        alert.save()
+
+        # Alert form should not allow creation of additional alerts.
+        form = ProductAlertForm(user=AnonymousUser(), product=product,
+                                data={'email': 'john@smith.com'})
+
+        self.assertFalse(form.is_valid())
+        self.assertIn(
+            "There is already an active stock alert for john@smith.com",
+            form.errors['__all__'][0])
+
+    def test_cannot_create_multiple_unconfirmed_alerts(self):
+        # Create an unconfirmed alert
+        alert = ProductAlertFactory(
+            user=None, email='john@smith.com', status=ProductAlert.UNCONFIRMED)
+
+        # Alert form should not allow creation of additional alerts.
+        form = ProductAlertForm(
+            user=AnonymousUser(),
+            product=create_product(num_in_stock=0),
+            data={'email': 'john@smith.com'},
+        )
+
+        self.assertFalse(form.is_valid())
+        self.assertIn(
+            "john@smith.com has been sent a confirmation email for another "
+            "product alert on this site.", form.errors['__all__'][0])
+
 
 class TestHurryMode(TestCase):