From e24cf0464db42793ce3da99d98f93d9c136cc66d Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Tue, 17 Dec 2024 16:16:30 +0000 Subject: [PATCH] Add tests for send_email task --- mail/celery_tasks.py | 10 +-- mail/tests/test_celery_tasks.py | 122 +++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/mail/celery_tasks.py b/mail/celery_tasks.py index 517a24d3..0a57dcb6 100644 --- a/mail/celery_tasks.py +++ b/mail/celery_tasks.py @@ -75,7 +75,7 @@ def _log_error(message, lite_usage_data_id): logger.error("Failed to send LITE UsageData [{%s}] to LITE API -> {%s}", lite_usage_data_id, message) -MAX_ATTEMPTS = 3 +MAX_RETRIES = 3 RETRY_BACKOFF = 180 LOCK_EXPIRE = 60 * 10 # secs (10 min) CELERY_SEND_LICENCE_UPDATES_TASK_NAME = "mail.celery_tasks.send_licence_details_to_hmrc" @@ -100,7 +100,7 @@ def on_failure(self, exc, task_id, args, kwargs, einfo): ConnectionResetError, SMTPException, ), - max_retries=MAX_ATTEMPTS, + max_retries=MAX_RETRIES, retry_backoff=RETRY_BACKOFF, base=SendEmailBaseTask, serializer="pickle", @@ -217,7 +217,7 @@ def on_failure(self, exc, task_id, args, kwargs, einfo): @shared_task( autoretry_for=(EdifactValidationError,), - max_retries=MAX_ATTEMPTS, + max_retries=MAX_RETRIES, retry_backoff=RETRY_BACKOFF, base=SendLicenceDetailsBaseTask, ) @@ -272,7 +272,7 @@ def send_licence_details_to_hmrc(): @shared_task( autoretry_for=(Exception,), - max_retries=MAX_ATTEMPTS, + max_retries=MAX_RETRIES, retry_backoff=True, base=SendUsageDataBaseTask, ) @@ -348,7 +348,7 @@ def send_licence_usage_figures_to_lite_api(lite_usage_data_id): # Scan Inbox for SPIRE and HMRC Emails @shared_task( autoretry_for=(Exception,), - max_retries=MAX_ATTEMPTS, + max_retries=MAX_RETRIES, retry_backoff=RETRY_BACKOFF, ) def manage_inbox(): diff --git a/mail/tests/test_celery_tasks.py b/mail/tests/test_celery_tasks.py index 2a14fe0d..4be717cc 100644 --- a/mail/tests/test_celery_tasks.py +++ b/mail/tests/test_celery_tasks.py @@ -1,6 +1,9 @@ +import concurrent.futures import email.mime.multipart +import time from datetime import datetime, timezone from email.mime.multipart import MIMEMultipart +from smtplib import SMTPException from unittest import mock from unittest.mock import MagicMock @@ -8,7 +11,13 @@ from django.test import TestCase, override_settings from parameterized import parameterized -from mail.celery_tasks import get_lite_api_url, manage_inbox, notify_users_of_rejected_licences +from mail.celery_tasks import ( + MAX_RETRIES, + get_lite_api_url, + manage_inbox, + notify_users_of_rejected_licences, + send_email_task, +) from mail.enums import ExtractTypeEnum, ReceptionStatusEnum, SourceEnum from mail.libraries.email_message_dto import EmailMessageDto from mail.libraries.routing_controller import check_and_route_emails @@ -223,3 +232,114 @@ def test_processing_of_licence_reply_with_rejected_licences( self.assertEqual(message["From"], emails_data[index]["sender"]) self.assertEqual(message["To"], emails_data[index]["recipients"]) self.assertEqual(message["Subject"], emails_data[index]["subject"]) + + +class SendEmailTestTests(TestCase): + @pytest.fixture(autouse=True) + def inject_fixtures(self, caplog): + self.caplog = caplog + + @mock.patch("mail.celery_tasks.cache") + @mock.patch("mail.servers.get_smtp_connection") + def test_sends_email(self, mock_get_smtp_connection, mock_cache): + mock_conn = mock_get_smtp_connection() + message = { + "From": "from@example.com", + "To": "to@example.com", + } + send_email_task.apply(args=[message]) + mock_conn.send_message.assert_called_with(message) + mock_conn.quit.assert_called() + mock_cache.lock.assert_called_with("global_send_email_lock", timeout=600) + + @parameterized.expand( + [ + (ConnectionResetError,), + (SMTPException,), + ] + ) + @mock.patch("mail.celery_tasks.cache") + @mock.patch("mail.servers.get_smtp_connection") + def test_sends_email_failed_then_succeeds(self, exception_class, mock_get_smtp_connection, mock_cache): + mock_conn = mock_get_smtp_connection() + message = { + "From": "from@example.com", + "To": "to@example.com", + } + mock_conn.send_message.side_effect = [exception_class(), None] + send_email_task.apply(args=[message]) + mock_conn.send_message.assert_called_with(message) + self.assertEqual(mock_conn.send_message.call_count, 2) + self.assertEqual(mock_conn.quit.call_count, 2) + mock_cache.lock.assert_called_with("global_send_email_lock", timeout=600) + + @parameterized.expand( + [ + (ConnectionResetError,), + (SMTPException,), + ] + ) + @mock.patch("mail.celery_tasks.cache") + @mock.patch("mail.servers.get_smtp_connection") + def test_sends_email_max_retry_failures(self, exception_class, mock_get_smtp_connection, mock_cache): + mock_conn = mock_get_smtp_connection() + message = { + "From": "from@example.com", + "To": "to@example.com", + } + mock_conn.send_message.side_effect = exception_class() + send_email_task.apply(args=[message]) + mock_conn.send_message.assert_called_with(message) + self.assertEqual( + mock_conn.send_message.call_count, + MAX_RETRIES + 1, + ) + self.assertEqual(mock_conn.quit.call_count, MAX_RETRIES + 1) + mock_cache.lock.assert_called_with("global_send_email_lock", timeout=600) + + @mock.patch("mail.servers.get_smtp_connection") + def test_locking(self, mock_get_smtp_connection): + results = [] + + def _sleepy(message): + call = {} + call["start"] = { + "message": message, + "time": time.monotonic(), + } + time.sleep(1) + call["end"] = { + "message": message, + "time": time.monotonic(), + } + results.append(call) + + mock_conn = mock_get_smtp_connection() + mock_conn.send_message.side_effect = _sleepy + + with concurrent.futures.ThreadPoolExecutor() as executor: + message_1 = { + "From": "from1@example.com", + "To": "to1@example.com", + } + future_1 = executor.submit(send_email_task.apply, args=[message_1]) + + message_2 = { + "From": "from2@example.com", + "To": "to2@example.com", + } + future_2 = executor.submit(send_email_task.apply, args=[message_2]) + + future_1.result() + future_2.result() + + first_call, second_call = results + + assert first_call["start"]["message"] == {"From": "from1@example.com", "To": "to1@example.com"} + assert first_call["end"]["message"] == {"From": "from1@example.com", "To": "to1@example.com"} + + assert second_call["start"]["message"] == {"From": "from2@example.com", "To": "to2@example.com"} + assert second_call["end"]["message"] == {"From": "from2@example.com", "To": "to2@example.com"} + + assert second_call["start"]["time"] > first_call["end"]["time"] + assert second_call["start"]["time"] > first_call["start"]["time"] + 1