From a4e45b8aaff2841114935595555a738175f10182 Mon Sep 17 00:00:00 2001 From: Stanislav Khlud Date: Wed, 18 Sep 2024 13:32:50 +0700 Subject: [PATCH] Add mail check --- README.md | 11 +++++++ health_check/contrib/mail/__init__.py | 0 health_check/contrib/mail/apps.py | 12 ++++++++ health_check/contrib/mail/backends.py | 28 +++++++++++++++++ tests/test_mail.py | 44 +++++++++++++++++++++++++++ tests/testapp/settings.py | 1 + 6 files changed, 96 insertions(+) create mode 100644 health_check/contrib/mail/__init__.py create mode 100644 health_check/contrib/mail/apps.py create mode 100644 health_check/contrib/mail/backends.py create mode 100644 tests/test_mail.py diff --git a/README.md b/README.md index 833fd270..523e018a 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ The following health checks are bundled with this project: - Celery ping - RabbitMQ - Migrations +- Mail Writing your own custom health checks is also very quick and easy. @@ -74,6 +75,7 @@ Add the `health_check` applications to your `INSTALLED_APPS`: 'health_check.contrib.s3boto3_storage', # requires boto3 and S3BotoStorage backend 'health_check.contrib.rabbitmq', # requires RabbitMQ broker 'health_check.contrib.redis', # requires Redis broker + 'health_check.contrib.mail', ] ``` @@ -90,6 +92,15 @@ one of these checks, set its value to `None`. } ``` +(Optional) If using the `mail` app, you can configure timeout +threshold settings; otherwise below defaults are assumed. + +```python + HEALTH_CHECK = { + 'MAIL_TIMEOUT': 15, # seconds + } +``` + To use Health Check Subsets, Specify a subset name and associate it with the relevant health check services to utilize Health Check Subsets. ```python HEALTH_CHECK = { diff --git a/health_check/contrib/mail/__init__.py b/health_check/contrib/mail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/health_check/contrib/mail/apps.py b/health_check/contrib/mail/apps.py new file mode 100644 index 00000000..292c7125 --- /dev/null +++ b/health_check/contrib/mail/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + +from health_check.plugins import plugin_dir + + +class HealthCheckConfig(AppConfig): + name = "health_check.contrib.mail" + + def ready(self): + from .backends import MailHealthCheck + + plugin_dir.register(MailHealthCheck) diff --git a/health_check/contrib/mail/backends.py b/health_check/contrib/mail/backends.py new file mode 100644 index 00000000..3ab2cd0d --- /dev/null +++ b/health_check/contrib/mail/backends.py @@ -0,0 +1,28 @@ +import logging + +from django.core.mail import get_connection + +from health_check.backends import BaseHealthCheckBackend +from health_check.conf import HEALTH_CHECK +from health_check.exceptions import ServiceUnavailable + +logger = logging.getLogger(__name__) + + +class MailHealthCheck(BaseHealthCheckBackend): + """Check that mail backend is working.""" + + def check_status(self) -> None: + """Open and close connection email server.""" + try: + connection = get_connection(fail_silently=False) + connection.timeout = HEALTH_CHECK.get("MAIL_TIMEOUT", 15) + logger.debug("Trying to open connection to mail backend.") + connection.open() + connection.close() + logger.debug("Connection established. Mail backend is healthy.") + except Exception as error: + self.add_error( + error=ServiceUnavailable(error), + cause=error, + ) diff --git a/tests/test_mail.py b/tests/test_mail.py new file mode 100644 index 00000000..58c9c8ab --- /dev/null +++ b/tests/test_mail.py @@ -0,0 +1,44 @@ +from unittest import mock + +from health_check.contrib.mail.backends import MailHealthCheck + + +class TestMailHealthCheck: + """Test mail health check.""" + + @mock.patch("health_check.contrib.mail.backends.get_connection") + def test_mail_conn_ok(self, mocked_backend): + """Test everything is OK.""" + + # instantiates the class + mail_health_checker = MailHealthCheck() + + # invokes the method check_status() + mail_health_checker.check_status() + assert len(mail_health_checker.errors) == 0, mail_health_checker.errors + + # mock assertions + assert mocked_backend.return_value.timeout == 15 + mocked_backend.return_value.open.assert_called_once() + mocked_backend.return_value.close.assert_called_once() + + @mock.patch("health_check.contrib.mail.backends.get_connection") + def test_mail_conn_refused(self, mocked_backend): + """Test case then connection refused.""" + + mocked_backend.return_value.open.side_effect = ConnectionRefusedError( + "Refused connection" + ) + # instantiates the class + mail_health_checker = MailHealthCheck() + + # invokes the method check_status() + mail_health_checker.check_status() + assert len(mail_health_checker.errors) == 1, mail_health_checker.errors + assert ( + mail_health_checker.errors[0].message + == mocked_backend.return_value.open.side_effect + ) + + # mock assertions + mocked_backend.return_value.open.assert_called_once() diff --git a/tests/testapp/settings.py b/tests/testapp/settings.py index 620e7190..bcdfb6ad 100644 --- a/tests/testapp/settings.py +++ b/tests/testapp/settings.py @@ -30,6 +30,7 @@ "health_check.contrib.migrations", "health_check.contrib.celery_ping", "health_check.contrib.s3boto_storage", + "health_check.contrib.mail", "tests", )