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
@@ -384,6 +385,7 @@ class SendUnsentScheduledEmailsTest(TestCase):
384385 def setUp (self ):
385386 G (Medium , name = 'email' )
386387
388+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
387389 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
388390 @patch .object (Event , 'render' , spec_set = True )
389391 def test_sends_all_scheduled_emails_no_email_addresses (self , render_mock , address_mock ):
@@ -394,6 +396,7 @@ def test_sends_all_scheduled_emails_no_email_addresses(self, render_mock, addres
394396 EntityEmailerInterface .send_unsent_scheduled_emails ()
395397 self .assertEqual (len (mail .outbox ), 0 )
396398
399+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
397400 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
398401 @patch .object (Event , 'render' , spec_set = True )
399402 def test_sends_all_scheduled_emails (self , render_mock , address_mock ):
@@ -407,6 +410,7 @@ def test_sends_all_scheduled_emails(self, render_mock, address_mock):
407410
408411 self .assertEqual (2 , mock_connection .return_value .__enter__ .return_value .send_messages .call_count )
409412
413+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
410414 @patch ('entity_emailer.interface.pre_send' )
411415 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
412416 @patch .object (Event , 'render' , spec_set = True )
@@ -439,6 +443,7 @@ def test_send_signals(self, render_mock, address_mock, mock_pre_send):
439443 })
440444 self .assertIsInstance (kwargs ['message' ], EmailMultiAlternatives )
441445
446+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
442447 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
443448 @patch .object (Event , 'render' , spec_set = True )
444449 def test_sends_email_with_specified_from_address (self , render_mock , address_mock ):
@@ -453,6 +458,7 @@ def test_sends_email_with_specified_from_address(self, render_mock, address_mock
453458 args = mock_connection .return_value .__enter__ .return_value .send_messages .call_args
454459 self .assertEqual (args [0 ][0 ][0 ].from_email , from_address )
455460
461+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
456462 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
457463 @patch .object (Event , 'render' , spec_set = True )
458464 def test_sends_no_future_emails (self , render_mock , address_mock ):
@@ -462,6 +468,7 @@ def test_sends_no_future_emails(self, render_mock, address_mock):
462468 EntityEmailerInterface .send_unsent_scheduled_emails ()
463469 self .assertEqual (len (mail .outbox ), 0 )
464470
471+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
465472 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
466473 @patch .object (Event , 'render' , spec_set = True )
467474 def test_sends_no_sent_emails (self , render_mock , address_mock ):
@@ -471,6 +478,7 @@ def test_sends_no_sent_emails(self, render_mock, address_mock):
471478 EntityEmailerInterface .send_unsent_scheduled_emails ()
472479 self .assertEqual (len (mail .outbox ), 0 )
473480
481+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
474482 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
475483 @patch .object (Event , 'render' , spec_set = True )
476484 def test_updates_times (self , render_mock , address_mock ):
@@ -481,6 +489,7 @@ def test_updates_times(self, render_mock, address_mock):
481489 sent_email = Email .objects .filter (sent__isnull = False )
482490 self .assertEqual (sent_email .count (), 1 )
483491
492+ @override_settings (DISABLE_DURABILITY_CHECKING = True )
484493 @patch ('entity_emailer.interface.email_exception' )
485494 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
486495 @patch .object (Event , 'render' , spec_set = True )
@@ -506,19 +515,26 @@ def test_exceptions(self, render_mock, address_mock, mock_email_exception):
506515 with patch (settings .EMAIL_BACKEND ) as mock_connection :
507516 EntityEmailerInterface .send_unsent_scheduled_emails ()
508517
509- # Assert that both emails were marked as sent
510- self .assertEqual (Email .objects .filter (sent__isnull = False ).count (), 2 )
518+ # Assert that only one email was marked as sent
519+ self .assertEqual (Email .objects .filter (sent__isnull = False ).count (), 1 )
511520
512521 # Assert that only one email is actually sent through backend
513522 self .assertEquals (1 , mock_connection .call_count )
514- # Assert that one email raised an exception
515- exception_email = Email .objects .get (sent__isnull = False , exception__isnull = False )
523+ # Assert that one email raised an exception and that the tries count was updated
524+ exception_email = Email .objects .get (exception__isnull = False , num_tries = 1 )
516525 self .assertIsNotNone (exception_email )
517526 self .assertTrue ('Exception: test' in exception_email .exception )
518527
528+ def test_send_unsent_emails_is_durable (self ):
529+ """
530+ Verify that the process to send unsent emails is durable and will not be rolled-back on exit from any caller
531+ """
532+ self .assertRaises (utils .ProgrammingError , EntityEmailerInterface .send_unsent_scheduled_emails )
533+
534+ @override_settings (DISABLE_DURABILITY_CHECKING = True , ENTITY_EMAILER_MAX_SEND_MESSAGE_TRIES = 2 )
519535 @patch .object (Event , 'render' , spec_set = True )
520536 @patch ('entity_emailer.interface.get_subscribed_email_addresses' )
521- def test_send_exceptions (self , mock_get_subscribed_addresses , mock_render ):
537+ def test_send_exceptions_and_retry (self , mock_get_subscribed_addresses , mock_render ):
522538 """
523539 Verifies that when a single email raises an exception from within the backend, the batch is still
524540 updated as sent and the failed email is saved with the exception
@@ -545,16 +561,44 @@ def to_dict(self):
545561
546562 EntityEmailerInterface .send_unsent_scheduled_emails ()
547563
548- # Verify that both emails are marked as sent
549- self .assertEquals (2 , Email .objects .filter (sent__isnull = False ).count ())
564+ # Verify that only one email is marked as sent
565+ self .assertEquals (1 , Email .objects .filter (sent__isnull = False ).count ())
550566 # Verify that the failed email was saved with the exception
551- actual_failed_email = Email .objects .get (sent__isnull = False , exception__isnull = False )
567+ actual_failed_email = Email .objects .get (exception__isnull = False , num_tries = 1 )
552568 self .assertEquals (failed_email .id , actual_failed_email .id )
553569 self .assertEquals (
554570 'test: {}' .format (json .dumps ({'message' : 'test' })),
555571 actual_failed_email .exception
556572 )
557573
574+ # Verify that a subsequent attempt to send unscheduled emails will retry the failed email
575+ with patch (settings .EMAIL_BACKEND ) as mock_connection :
576+ # Mock side effect for sending email
577+ mock_connection .return_value .__enter__ .return_value .send_messages .side_effect = (
578+ TestEmailSendMessageException ('test' )
579+ )
580+
581+ EntityEmailerInterface .send_unsent_scheduled_emails ()
582+
583+ # Verify only called once, with the failed email
584+ self .assertEquals (1 , mock_connection .return_value .__enter__ .return_value .send_messages .call_count )
585+
586+ # Verify num tries updated
587+ actual_failed_email = Email .objects .get (exception__isnull = False , num_tries = 2 )
588+ self .assertEquals (failed_email .id , actual_failed_email .id )
589+
590+ # Verify that a subsequent attempt to send unscheduled emails will find no emails to send
591+ with patch (settings .EMAIL_BACKEND ) as mock_connection :
592+
593+ EntityEmailerInterface .send_unsent_scheduled_emails ()
594+
595+ # Verify only called once, with the failed email
596+ self .assertEquals (0 , mock_connection .return_value .__enter__ .return_value .send_messages .call_count )
597+
598+ # Verify num tries not updated
599+ actual_failed_email = Email .objects .get (exception__isnull = False , num_tries = 2 )
600+ self .assertEquals (failed_email .id , actual_failed_email .id )
601+
558602
559603class CreateEmailObjectTest (TestCase ):
560604 def test_no_html (self ):
0 commit comments