55from django .core import mail
66from django .core .mail import EmailMultiAlternatives
77from django .core .management import call_command
8+ from django .db import utils
89from django .test import TestCase , SimpleTestCase
910from django .test .utils import override_settings
1011from django_dynamic_fixture import G
@@ -413,6 +414,7 @@ class SendUnsentScheduledEmailsTest(TestCase):
413414 def setUp (self ):
414415 G (Medium , name = 'email' )
415416
417+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
416418 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
417419 @patch .object (Event , 'render' , spec_set = True )
418420 def test_sends_all_scheduled_emails_no_email_addresses (self , render_mock , address_mock ):
@@ -423,6 +425,7 @@ def test_sends_all_scheduled_emails_no_email_addresses(self, render_mock, addres
423425 EntityEmailerInterface .send_unsent_scheduled_emails ()
424426 self .assertEqual (len (mail .outbox ), 0 )
425427
428+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
426429 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
427430 @patch .object (Event , 'render' , spec_set = True )
428431 def test_sends_all_scheduled_emails (self , render_mock , address_mock ):
@@ -436,6 +439,7 @@ def test_sends_all_scheduled_emails(self, render_mock, address_mock):
436439
437440 self .assertEqual (2 , mock_connection .return_value .__enter__ .return_value .send_messages .call_count )
438441
442+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
439443 @patch ('entity_emailer.interface.pre_send' )
440444 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
441445 @patch .object (Event , 'render' , spec_set = True )
@@ -468,6 +472,7 @@ def test_send_signals(self, render_mock, address_mock, mock_pre_send):
468472 })
469473 self .assertIsInstance (kwargs ['message' ], EmailMultiAlternatives )
470474
475+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
471476 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
472477 @patch .object (Event , 'render' , spec_set = True )
473478 def test_sends_email_with_specified_from_address (self , render_mock , address_mock ):
@@ -482,6 +487,7 @@ def test_sends_email_with_specified_from_address(self, render_mock, address_mock
482487 args = mock_connection .return_value .__enter__ .return_value .send_messages .call_args
483488 self .assertEqual (args [0 ][0 ][0 ].from_email , from_address )
484489
490+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
485491 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
486492 @patch .object (Event , 'render' , spec_set = True )
487493 def test_sends_no_future_emails (self , render_mock , address_mock ):
@@ -491,6 +497,7 @@ def test_sends_no_future_emails(self, render_mock, address_mock):
491497 EntityEmailerInterface .send_unsent_scheduled_emails ()
492498 self .assertEqual (len (mail .outbox ), 0 )
493499
500+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
494501 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
495502 @patch .object (Event , 'render' , spec_set = True )
496503 def test_sends_no_sent_emails (self , render_mock , address_mock ):
@@ -500,6 +507,7 @@ def test_sends_no_sent_emails(self, render_mock, address_mock):
500507 EntityEmailerInterface .send_unsent_scheduled_emails ()
501508 self .assertEqual (len (mail .outbox ), 0 )
502509
510+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
503511 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
504512 @patch .object (Event , 'render' , spec_set = True )
505513 def test_updates_times (self , render_mock , address_mock ):
@@ -510,6 +518,7 @@ def test_updates_times(self, render_mock, address_mock):
510518 sent_email = Email .objects .filter (sent__isnull = False )
511519 self .assertEqual (sent_email .count (), 1 )
512520
521+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
513522 @patch ('entity_emailer.interface.email_exception' )
514523 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
515524 @patch .object (Event , 'render' , spec_set = True )
@@ -535,19 +544,26 @@ def test_exceptions(self, render_mock, address_mock, mock_email_exception):
535544 with patch (settings .EMAIL_BACKEND ) as mock_connection :
536545 EntityEmailerInterface .send_unsent_scheduled_emails ()
537546
538- # Assert that both emails were marked as sent
539- self .assertEqual (Email .objects .filter (sent__isnull = False ).count (), 2 )
547+ # Assert that only one email was marked as sent
548+ self .assertEqual (Email .objects .filter (sent__isnull = False ).count (), 1 )
540549
541550 # Assert that only one email is actually sent through backend
542551 self .assertEquals (1 , mock_connection .call_count )
543- # Assert that one email raised an exception
544- exception_email = Email .objects .get (sent__isnull = False , exception__isnull = False )
552+ # Assert that one email raised an exception and that the tries count was updated
553+ exception_email = Email .objects .get (exception__isnull = False , num_tries = 1 )
545554 self .assertIsNotNone (exception_email )
546555 self .assertTrue ('Exception: test' in exception_email .exception )
547556
557+ def test_send_unsent_emails_is_durable (self ):
558+ """
559+ Verify that the process to send unsent emails is durable and will not be rolled-back on exit from any caller
560+ """
561+ self .assertRaises (utils .ProgrammingError , EntityEmailerInterface .send_unsent_scheduled_emails )
562+
563+ @override_settings (DISABLE_DURABILITY_CHECKING = True , ENTITY_EMAILER_MAX_SEND_MESSAGE_TRIES = 2 )
548564 @patch .object (Event , 'render' , spec_set = True )
549565 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
550- def test_send_exceptions (self , mock_get_subscribed_addresses , mock_render ):
566+ def test_send_exceptions_and_retry (self , mock_get_subscribed_addresses , mock_render ):
551567 """
552568 Verifies that when a single email raises an exception from within the backend, the batch is still
553569 updated as sent and the failed email is saved with the exception
@@ -574,16 +590,44 @@ def to_dict(self):
574590
575591 EntityEmailerInterface .send_unsent_scheduled_emails ()
576592
577- # Verify that both emails are marked as sent
578- self .assertEquals (2 , Email .objects .filter (sent__isnull = False ).count ())
593+ # Verify that only one email is marked as sent
594+ self .assertEquals (1 , Email .objects .filter (sent__isnull = False ).count ())
579595 # Verify that the failed email was saved with the exception
580- actual_failed_email = Email .objects .get (sent__isnull = False , exception__isnull = False )
596+ actual_failed_email = Email .objects .get (exception__isnull = False , num_tries = 1 )
581597 self .assertEquals (failed_email .id , actual_failed_email .id )
582598 self .assertEquals (
583599 'test: {}' .format (json .dumps ({'message' : 'test' })),
584600 actual_failed_email .exception
585601 )
586602
603+ # Verify that a subsequent attempt to send unscheduled emails will retry the failed email
604+ with patch (settings .EMAIL_BACKEND ) as mock_connection :
605+ # Mock side effect for sending email
606+ mock_connection .return_value .__enter__ .return_value .send_messages .side_effect = (
607+ TestEmailSendMessageException ('test' )
608+ )
609+
610+ EntityEmailerInterface .send_unsent_scheduled_emails ()
611+
612+ # Verify only called once, with the failed email
613+ self .assertEquals (1 , mock_connection .return_value .__enter__ .return_value .send_messages .call_count )
614+
615+ # Verify num tries updated
616+ actual_failed_email = Email .objects .get (exception__isnull = False , num_tries = 2 )
617+ self .assertEquals (failed_email .id , actual_failed_email .id )
618+
619+ # Verify that a subsequent attempt to send unscheduled emails will find no emails to send
620+ with patch (settings .EMAIL_BACKEND ) as mock_connection :
621+
622+ EntityEmailerInterface .send_unsent_scheduled_emails ()
623+
624+ # Verify only called once, with the failed email
625+ self .assertEquals (0 , mock_connection .return_value .__enter__ .return_value .send_messages .call_count )
626+
627+ # Verify num tries not updated
628+ actual_failed_email = Email .objects .get (exception__isnull = False , num_tries = 2 )
629+ self .assertEquals (failed_email .id , actual_failed_email .id )
630+
587631
588632class CreateEmailObjectTest (TestCase ):
589633 def test_no_html (self ):
0 commit comments