From 009cbce0b1e3aec702df8ce1404f11edc61b80f2 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Thu, 22 Aug 2024 23:03:31 +0330 Subject: [PATCH 01/30] :hammer::zap: refactor(validators): Improve config_validators to handle make log_dir --- django_logging/validators/config_validators.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/django_logging/validators/config_validators.py b/django_logging/validators/config_validators.py index d3781f6..7ef0b4a 100644 --- a/django_logging/validators/config_validators.py +++ b/django_logging/validators/config_validators.py @@ -25,13 +25,17 @@ def validate_directory(path: str, config_name: str) -> List[Error]: ) ) elif not os.path.exists(path): - errors.append( - Error( - f"The path specified in {config_name} does not exist.", - hint=f"Ensure the path set in {config_name} exists.", - id=f"django_logging.E002_{config_name}", + try: + os.mkdir(os.path.join(os.getcwd(), path)) + except Exception as e: + errors.append( + Error( + f"The path specified in {config_name} is not a valid path.", + hint=f"Ensure the path set in {config_name} is valid.", + id=f"django_logging.E002_{config_name}", + ) ) - ) + elif not os.path.isdir(path): errors.append( Error( From 0e385ab53a2134e131a6b5446e27f9a4d8a6c685 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Thu, 22 Aug 2024 23:04:58 +0330 Subject: [PATCH 02/30] :zap: Update(filters) imports in init file --- django_logging/filters/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django_logging/filters/__init__.py b/django_logging/filters/__init__.py index e69de29..b92338c 100644 --- a/django_logging/filters/__init__.py +++ b/django_logging/filters/__init__.py @@ -0,0 +1 @@ +from .level_filter import LoggingLevelFilter From 2b4b32b6bb405ecda9561a44a85c9689f805c5ac Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Thu, 22 Aug 2024 23:25:03 +0330 Subject: [PATCH 03/30] :zap::hammer: refactor(commands): Update default_log_dir using DefaultLoggingSettings Update conditions in finally block --- django_logging/management/commands/send_logs.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/django_logging/management/commands/send_logs.py b/django_logging/management/commands/send_logs.py index b4fe7c2..c94acb5 100644 --- a/django_logging/management/commands/send_logs.py +++ b/django_logging/management/commands/send_logs.py @@ -9,6 +9,7 @@ from django.conf import settings from django_logging.validators.email_settings_validator import check_email_settings +from django_logging.constants import DefaultLoggingSettings logger = logging.getLogger(__name__) @@ -46,8 +47,10 @@ def handle(self, *args, **kwargs): """ email = kwargs["email"] + default_settings = DefaultLoggingSettings() + log_dir = settings.DJANGO_LOGGING.get( - "LOG_DIR", os.path.join(os.getcwd(), "logs") + "LOG_DIR", os.path.join(os.getcwd(), default_settings.log_dir) ) if not os.path.exists(log_dir): @@ -86,9 +89,10 @@ def handle(self, *args, **kwargs): self.stdout.write(self.style.ERROR(f"Failed to send logs: {e}")) logger.error(f"Failed to send logs: {e}") finally: - # Clean up the temporary file - os.remove(zip_path) - logger.info("Temporary zip file cleaned up successfully.") + # Clean up the temporary file if exists + if os.path.exists(zip_path): + os.remove(zip_path) + logger.info("Temporary zip file cleaned up successfully.") def validate_email_settings(self): """ From cc8c5b391c85d132a0ce5c28ac73362d938b790c Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Thu, 22 Aug 2024 23:43:05 +0330 Subject: [PATCH 04/30] :zap: Update(utils) setup_logging TypeAnnotations & imports --- django_logging/utils/setup_logging.py | 29 +++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/django_logging/utils/setup_logging.py b/django_logging/utils/setup_logging.py index 2b3d452..55796e6 100644 --- a/django_logging/utils/setup_logging.py +++ b/django_logging/utils/setup_logging.py @@ -2,24 +2,29 @@ from django.core.exceptions import ImproperlyConfigured +from django_logging.constants.settings_types import ( + LOG_LEVELS_TYPE, LOG_DIR_TYPE, LogFileFormatsType, + LOG_CONSOLE_LEVEL_TYPE, LOG_CONSOLE_FORMAT_TYPE, + LOG_CONSOLE_COLORIZE_TYPE, LOG_DATE_FORMAT_TYPE, FormatOption, +) from django_logging.settings.conf import LogConfig, LogManager -from typing import List, Optional, Union, Dict +from typing import List from django_logging.constants.ansi_colors import AnsiColors -from django_logging.utils.get_config import is_auto_initialization_enabled +from django_logging.utils.get_config import is_auto_initialization_enabled, is_initialization_message_enabled def set_logging( - log_levels: List[str], - log_dir: str, - log_file_formats: Dict[str, Union[int, str]], - console_level: str, - console_format: Optional[Union[int, str]], - colorize_console: bool, - log_date_format: str, + log_levels: LOG_LEVELS_TYPE, + log_dir: LOG_DIR_TYPE, + log_file_formats: LogFileFormatsType, + console_level: LOG_CONSOLE_LEVEL_TYPE, + console_format: LOG_CONSOLE_FORMAT_TYPE, + colorize_console: LOG_CONSOLE_COLORIZE_TYPE, + log_date_format: LOG_DATE_FORMAT_TYPE, log_email_notifier_enable: bool, - log_email_notifier_log_levels: List[str], - log_email_notifier_log_format: Union[int, str], + log_email_notifier_log_levels: LOG_LEVELS_TYPE, + log_email_notifier_log_format: FormatOption, ) -> None: """ Sets up the logging configuration. @@ -63,8 +68,6 @@ def set_logging( return if os.environ.get("RUN_MAIN") == "true": - from django_logging.utils.get_config import is_initialization_message_enabled - if is_initialization_message_enabled(): from logging import getLogger From 89d35dc74794e6bcec3eac5ad18f5ad3d42ce7ff Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Thu, 22 Aug 2024 23:55:47 +0330 Subject: [PATCH 05/30] :zap: Update(validators) config validator make dir --- django_logging/validators/config_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_logging/validators/config_validators.py b/django_logging/validators/config_validators.py index 7ef0b4a..43d470d 100644 --- a/django_logging/validators/config_validators.py +++ b/django_logging/validators/config_validators.py @@ -26,7 +26,7 @@ def validate_directory(path: str, config_name: str) -> List[Error]: ) elif not os.path.exists(path): try: - os.mkdir(os.path.join(os.getcwd(), path)) + os.makedirs(os.path.dirname(path), exist_ok=True) except Exception as e: errors.append( Error( From 58d64c6d5298b3fd72940d6b52d43dd25c3048cf Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 00:07:17 +0330 Subject: [PATCH 06/30] :zap::hammer: refactor(settings) conf TypeAnnotations & Update naming --- django_logging/settings/conf.py | 42 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/django_logging/settings/conf.py b/django_logging/settings/conf.py index 906a6ee..b4e2816 100644 --- a/django_logging/settings/conf.py +++ b/django_logging/settings/conf.py @@ -1,9 +1,19 @@ import logging import logging.config import os -from typing import List, Dict, Optional, Union +from typing import List, Dict, Optional from django_logging.constants import FORMAT_OPTIONS, DefaultLoggingSettings +from django_logging.constants.settings_types import ( + LOG_LEVELS_TYPE, + LOG_DIR_TYPE, + LogFileFormatsType, + LOG_CONSOLE_FORMAT_TYPE, + LOG_DATE_FORMAT_TYPE, + LOG_CONSOLE_COLORIZE_TYPE, + FormatOption, + LogLevel, +) from django_logging.filters.level_filter import LoggingLevelFilter @@ -18,16 +28,16 @@ class LogConfig: def __init__( self, - log_levels: List[str], - log_dir: str, - log_file_formats: Dict[str, Union[int, str]], - console_level: str, - console_format: Optional[Union[int, str]], - colorize_console: bool, - log_date_format: str, + log_levels: LOG_LEVELS_TYPE, + log_dir: LOG_DIR_TYPE, + log_file_formats: LogFileFormatsType, + console_level: LOG_CONSOLE_FORMAT_TYPE, + console_format: FormatOption, + colorize_console: LOG_CONSOLE_COLORIZE_TYPE, + log_date_format: LOG_DATE_FORMAT_TYPE, log_email_notifier_enable: bool, - log_email_notifier_log_levels: List[str], - log_email_notifier_log_format: Union[int, str], + log_email_notifier_log_levels: LOG_LEVELS_TYPE, + log_email_notifier_log_format: FormatOption, ) -> None: self.log_levels = log_levels @@ -45,9 +55,7 @@ def __init__( log_email_notifier_log_format ) - def _resolve_file_formats( - self, log_file_formats: Dict[str, Union[int, str]] - ) -> Dict: + def _resolve_file_formats(self, log_file_formats: LogFileFormatsType) -> Dict: resolved_formats = {} for level in self.log_levels: format_option = log_file_formats.get(level, None) @@ -77,7 +85,7 @@ def remove_ansi_escape_sequences(log_message: str) -> str: return ansi_escape.sub("", log_message) @staticmethod - def resolve_format(_format: Union[int, str], use_colors: bool = False) -> str: + def resolve_format(_format: FormatOption, use_colors: bool = False) -> str: if _format: if isinstance(_format, int): resolved_format = FORMAT_OPTIONS.get(_format, FORMAT_OPTIONS[1]) @@ -118,7 +126,7 @@ def create_log_files(self) -> None: open(log_file_path, "w").close() self.log_files[log_level] = log_file_path - def get_log_file(self, log_level: str) -> Optional[str]: + def get_log_file(self, log_level: LogLevel) -> Optional[str]: """ Retrieves the file path for a given log level. @@ -132,7 +140,7 @@ def get_log_file(self, log_level: str) -> Optional[str]: def set_conf(self) -> None: """Sets the logging configuration using the generated log files.""" - defaults = DefaultLoggingSettings() + default_settings = DefaultLoggingSettings() handlers = { level.lower(): { "class": "logging.FileHandler", @@ -166,7 +174,7 @@ def set_conf(self) -> None: "()": LoggingLevelFilter, "logging_level": getattr(logging, level), } - for level in defaults.log_levels + for level in default_settings.log_levels } formatters = { From 441c8e63d47422ffd1289a305a732e97e3089e65 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 10:18:04 +0330 Subject: [PATCH 07/30] :zap::hammer::fire: refactor(Types): Update TypeAnnotations & Remove unnecessary type aliases --- django_logging/constants/defaults.py | 30 ++++++++----------- django_logging/constants/settings_types.py | 16 ++++------ .../management/commands/send_logs.py | 3 +- django_logging/settings/conf.py | 21 +++++++------ django_logging/utils/setup_logging.py | 20 ++++++------- .../validators/config_validators.py | 6 ++-- 6 files changed, 43 insertions(+), 53 deletions(-) diff --git a/django_logging/constants/defaults.py b/django_logging/constants/defaults.py index 7c3d9ba..7f98b06 100644 --- a/django_logging/constants/defaults.py +++ b/django_logging/constants/defaults.py @@ -3,28 +3,24 @@ from django_logging.constants.settings_types import ( LogFileFormatsType, - LOG_DIR_TYPE, - LOG_LEVELS_TYPE, - LOG_CONSOLE_FORMAT_TYPE, - LOG_CONSOLE_LEVEL_TYPE, - LOG_CONSOLE_COLORIZE_TYPE, - LOG_DATE_FORMAT_TYPE, - INITIALIZATION_MESSAGE_ENABLE_TYPE, + LogDir, + LogLevel, + LogLevels, + FormatOption, + LogDateFormat, LogEmailNotifierType, ) @dataclass(frozen=True) class DefaultLoggingSettings: - log_dir: LOG_DIR_TYPE = field( - default_factory=lambda: os.path.join(os.getcwd(), "logs") - ) - log_levels: LOG_LEVELS_TYPE = field( + log_dir: LogDir = field(default_factory=lambda: os.path.join(os.getcwd(), "logs")) + log_levels: LogLevels = field( default_factory=lambda: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] ) - log_date_format: LOG_DATE_FORMAT_TYPE = "%Y-%m-%d %H:%M:%S" - auto_initialization_enable: INITIALIZATION_MESSAGE_ENABLE_TYPE = True - initialization_message_enable: INITIALIZATION_MESSAGE_ENABLE_TYPE = True + log_date_format: LogDateFormat = "%Y-%m-%d %H:%M:%S" + auto_initialization_enable: bool = True + initialization_message_enable: bool = True log_file_formats: LogFileFormatsType = field( default_factory=lambda: { "DEBUG": 1, @@ -34,9 +30,9 @@ class DefaultLoggingSettings: "CRITICAL": 1, } ) - log_console_level: LOG_CONSOLE_LEVEL_TYPE = "DEBUG" - log_console_format: LOG_CONSOLE_FORMAT_TYPE = 1 - log_console_colorize: LOG_CONSOLE_COLORIZE_TYPE = True + log_console_level: LogLevel = "DEBUG" + log_console_format: FormatOption = 1 + log_console_colorize: bool = True log_email_notifier: LogEmailNotifierType = field( default_factory=lambda: { "ENABLE": False, diff --git a/django_logging/constants/settings_types.py b/django_logging/constants/settings_types.py index 9f4d44d..436a1c7 100644 --- a/django_logging/constants/settings_types.py +++ b/django_logging/constants/settings_types.py @@ -11,9 +11,6 @@ class LogEmailNotifierType(TypedDict, total=False): USE_TEMPLATE: bool -LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] - - class LogFileFormatsType(TypedDict, total=False): DEBUG: FormatOption INFO: FormatOption @@ -23,11 +20,8 @@ class LogFileFormatsType(TypedDict, total=False): # Type Aliases for other configurations -LOG_DIR_TYPE = str -LOG_LEVELS_TYPE = List[str] -LOG_DATE_FORMAT_TYPE = str -AUTO_INITIALIZATION_ENABLE_TYPE = bool -INITIALIZATION_MESSAGE_ENABLE_TYPE = bool -LOG_CONSOLE_LEVEL_TYPE = LogLevel -LOG_CONSOLE_FORMAT_TYPE = FormatOption -LOG_CONSOLE_COLORIZE_TYPE = bool +LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +LogDir = str +LogLevels = List[LogLevel] +NotifierLogLevels = List[Literal["ERROR", "CRITICAL"]] +LogDateFormat = str diff --git a/django_logging/management/commands/send_logs.py b/django_logging/management/commands/send_logs.py index c94acb5..cf2af76 100644 --- a/django_logging/management/commands/send_logs.py +++ b/django_logging/management/commands/send_logs.py @@ -8,6 +8,7 @@ from django.core.management.base import BaseCommand from django.conf import settings +from django_logging.constants.settings_types import LogDir from django_logging.validators.email_settings_validator import check_email_settings from django_logging.constants import DefaultLoggingSettings @@ -49,7 +50,7 @@ def handle(self, *args, **kwargs): default_settings = DefaultLoggingSettings() - log_dir = settings.DJANGO_LOGGING.get( + log_dir: LogDir = settings.DJANGO_LOGGING.get( "LOG_DIR", os.path.join(os.getcwd(), default_settings.log_dir) ) diff --git a/django_logging/settings/conf.py b/django_logging/settings/conf.py index b4e2816..a629ae5 100644 --- a/django_logging/settings/conf.py +++ b/django_logging/settings/conf.py @@ -5,14 +5,13 @@ from django_logging.constants import FORMAT_OPTIONS, DefaultLoggingSettings from django_logging.constants.settings_types import ( - LOG_LEVELS_TYPE, - LOG_DIR_TYPE, + LogLevels, + LogDir, LogFileFormatsType, - LOG_CONSOLE_FORMAT_TYPE, - LOG_DATE_FORMAT_TYPE, - LOG_CONSOLE_COLORIZE_TYPE, + LogDateFormat, FormatOption, LogLevel, + NotifierLogLevels ) from django_logging.filters.level_filter import LoggingLevelFilter @@ -28,15 +27,15 @@ class LogConfig: def __init__( self, - log_levels: LOG_LEVELS_TYPE, - log_dir: LOG_DIR_TYPE, + log_levels: LogLevels, + log_dir: LogDir, log_file_formats: LogFileFormatsType, - console_level: LOG_CONSOLE_FORMAT_TYPE, + console_level: LogLevel, console_format: FormatOption, - colorize_console: LOG_CONSOLE_COLORIZE_TYPE, - log_date_format: LOG_DATE_FORMAT_TYPE, + colorize_console: bool, + log_date_format: LogDateFormat, log_email_notifier_enable: bool, - log_email_notifier_log_levels: LOG_LEVELS_TYPE, + log_email_notifier_log_levels: NotifierLogLevels, log_email_notifier_log_format: FormatOption, ) -> None: diff --git a/django_logging/utils/setup_logging.py b/django_logging/utils/setup_logging.py index 55796e6..585ea92 100644 --- a/django_logging/utils/setup_logging.py +++ b/django_logging/utils/setup_logging.py @@ -3,9 +3,9 @@ from django.core.exceptions import ImproperlyConfigured from django_logging.constants.settings_types import ( - LOG_LEVELS_TYPE, LOG_DIR_TYPE, LogFileFormatsType, - LOG_CONSOLE_LEVEL_TYPE, LOG_CONSOLE_FORMAT_TYPE, - LOG_CONSOLE_COLORIZE_TYPE, LOG_DATE_FORMAT_TYPE, FormatOption, + LogLevels, LogDir, LogFileFormatsType, + LogLevel, LogDateFormat, FormatOption, + NotifierLogLevels ) from django_logging.settings.conf import LogConfig, LogManager @@ -15,15 +15,15 @@ def set_logging( - log_levels: LOG_LEVELS_TYPE, - log_dir: LOG_DIR_TYPE, + log_levels: LogLevels, + log_dir: LogDir, log_file_formats: LogFileFormatsType, - console_level: LOG_CONSOLE_LEVEL_TYPE, - console_format: LOG_CONSOLE_FORMAT_TYPE, - colorize_console: LOG_CONSOLE_COLORIZE_TYPE, - log_date_format: LOG_DATE_FORMAT_TYPE, + console_level: LogLevel, + console_format: FormatOption, + colorize_console: bool, + log_date_format: LogDateFormat, log_email_notifier_enable: bool, - log_email_notifier_log_levels: LOG_LEVELS_TYPE, + log_email_notifier_log_levels: NotifierLogLevels, log_email_notifier_log_format: FormatOption, ) -> None: """ diff --git a/django_logging/validators/config_validators.py b/django_logging/validators/config_validators.py index 43d470d..692fe15 100644 --- a/django_logging/validators/config_validators.py +++ b/django_logging/validators/config_validators.py @@ -9,7 +9,7 @@ from django_logging.constants import LOG_FORMAT_SPECIFIERS, FORMAT_OPTIONS from django_logging.constants.settings_types import ( FormatOption, - LOG_LEVELS_TYPE, + LogLevels, LogEmailNotifierType, ) @@ -48,9 +48,9 @@ def validate_directory(path: str, config_name: str) -> List[Error]: def validate_log_levels( - log_levels: LOG_LEVELS_TYPE, + log_levels: LogLevels, config_name: str, - valid_levels: LOG_LEVELS_TYPE, + valid_levels: LogLevels, ) -> List[Error]: errors = [] if not isinstance(log_levels, list): From f029124b95e220e82563f8f48d5ec4b068860c31 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 12:36:33 +0330 Subject: [PATCH 08/30] :zap::art::hammer: refactor: Update module names & Improve structure --- django_logging/apps.py | 8 ++++---- django_logging/constants/__init__.py | 6 +++--- .../constants/{settings_types.py => config_types.py} | 0 .../constants/{defaults.py => default_settings.py} | 2 +- .../{format_options.py => log_format_options.py} | 0 .../{format_specifiers.py => log_format_specifiers.py} | 0 .../{email_settings.py => required_email_settings.py} | 0 django_logging/filters/__init__.py | 2 +- .../filters/{level_filter.py => log_level_filter.py} | 0 django_logging/formatters/__init__.py | 2 +- .../{colorized_formatter.py => colored_formatter.py} | 2 +- django_logging/handlers/email_handler.py | 4 ++-- django_logging/management/commands/send_logs.py | 2 +- django_logging/settings/conf.py | 4 ++-- .../utils/{colorizer.py => console_colorizer.py} | 0 django_logging/utils/context_manager.py | 8 ++++---- django_logging/utils/{get_config.py => get_conf.py} | 2 +- django_logging/utils/log_email_notifier/__init__.py | 2 ++ .../utils/{ => log_email_notifier}/log_and_notify.py | 10 +++++----- .../notifier.py} | 0 django_logging/utils/{setup_logging.py => set_conf.py} | 6 +++--- django_logging/validators/config_validators.py | 2 +- django_logging/validators/email_settings_validator.py | 2 +- 23 files changed, 33 insertions(+), 31 deletions(-) rename django_logging/constants/{settings_types.py => config_types.py} (100%) rename django_logging/constants/{defaults.py => default_settings.py} (95%) rename django_logging/constants/{format_options.py => log_format_options.py} (100%) rename django_logging/constants/{format_specifiers.py => log_format_specifiers.py} (100%) rename django_logging/constants/{email_settings.py => required_email_settings.py} (100%) rename django_logging/filters/{level_filter.py => log_level_filter.py} (100%) rename django_logging/formatters/{colorized_formatter.py => colored_formatter.py} (90%) rename django_logging/utils/{colorizer.py => console_colorizer.py} (100%) rename django_logging/utils/{get_config.py => get_conf.py} (99%) create mode 100644 django_logging/utils/log_email_notifier/__init__.py rename django_logging/utils/{ => log_email_notifier}/log_and_notify.py (89%) rename django_logging/utils/{email_notifier.py => log_email_notifier/notifier.py} (100%) rename django_logging/utils/{setup_logging.py => set_conf.py} (93%) diff --git a/django_logging/apps.py b/django_logging/apps.py index 9978189..b054097 100644 --- a/django_logging/apps.py +++ b/django_logging/apps.py @@ -9,9 +9,9 @@ class DjangoLoggingConfig(AppConfig): def ready(self) -> None: from django_logging.settings import checks - from django_logging.utils.setup_logging import set_logging - from django_logging.utils.get_config import get_conf - conf = get_conf() + from django_logging.utils.set_conf import set_config + from django_logging.utils.get_conf import get_config + conf = get_config() # Set the logging configuration - set_logging(*conf) + set_config(*conf) diff --git a/django_logging/constants/__init__.py b/django_logging/constants/__init__.py index a88a79d..56dcc41 100644 --- a/django_logging/constants/__init__.py +++ b/django_logging/constants/__init__.py @@ -1,3 +1,3 @@ -from .format_options import FORMAT_OPTIONS -from .defaults import DefaultLoggingSettings -from .format_specifiers import LOG_FORMAT_SPECIFIERS +from .log_format_options import FORMAT_OPTIONS +from .default_settings import DefaultLoggingSettings +from .log_format_specifiers import LOG_FORMAT_SPECIFIERS diff --git a/django_logging/constants/settings_types.py b/django_logging/constants/config_types.py similarity index 100% rename from django_logging/constants/settings_types.py rename to django_logging/constants/config_types.py diff --git a/django_logging/constants/defaults.py b/django_logging/constants/default_settings.py similarity index 95% rename from django_logging/constants/defaults.py rename to django_logging/constants/default_settings.py index 7f98b06..927b9a7 100644 --- a/django_logging/constants/defaults.py +++ b/django_logging/constants/default_settings.py @@ -1,7 +1,7 @@ import os from dataclasses import dataclass, field -from django_logging.constants.settings_types import ( +from django_logging.constants.config_types import ( LogFileFormatsType, LogDir, LogLevel, diff --git a/django_logging/constants/format_options.py b/django_logging/constants/log_format_options.py similarity index 100% rename from django_logging/constants/format_options.py rename to django_logging/constants/log_format_options.py diff --git a/django_logging/constants/format_specifiers.py b/django_logging/constants/log_format_specifiers.py similarity index 100% rename from django_logging/constants/format_specifiers.py rename to django_logging/constants/log_format_specifiers.py diff --git a/django_logging/constants/email_settings.py b/django_logging/constants/required_email_settings.py similarity index 100% rename from django_logging/constants/email_settings.py rename to django_logging/constants/required_email_settings.py diff --git a/django_logging/filters/__init__.py b/django_logging/filters/__init__.py index b92338c..4aaebae 100644 --- a/django_logging/filters/__init__.py +++ b/django_logging/filters/__init__.py @@ -1 +1 @@ -from .level_filter import LoggingLevelFilter +from .log_level_filter import LoggingLevelFilter diff --git a/django_logging/filters/level_filter.py b/django_logging/filters/log_level_filter.py similarity index 100% rename from django_logging/filters/level_filter.py rename to django_logging/filters/log_level_filter.py diff --git a/django_logging/formatters/__init__.py b/django_logging/formatters/__init__.py index 04cdd33..17a440c 100644 --- a/django_logging/formatters/__init__.py +++ b/django_logging/formatters/__init__.py @@ -1 +1 @@ -from .colorized_formatter import ColorizedFormatter +from .colored_formatter import ColorizedFormatter diff --git a/django_logging/formatters/colorized_formatter.py b/django_logging/formatters/colored_formatter.py similarity index 90% rename from django_logging/formatters/colorized_formatter.py rename to django_logging/formatters/colored_formatter.py index 9ab7390..37ef585 100644 --- a/django_logging/formatters/colorized_formatter.py +++ b/django_logging/formatters/colored_formatter.py @@ -1,6 +1,6 @@ import logging from django_logging.settings.conf import LogConfig -from django_logging.utils.colorizer import colorize_log_format +from django_logging.utils.console_colorizer import colorize_log_format class ColorizedFormatter(logging.Formatter): diff --git a/django_logging/handlers/email_handler.py b/django_logging/handlers/email_handler.py index 5ffaa24..9a7f3f4 100644 --- a/django_logging/handlers/email_handler.py +++ b/django_logging/handlers/email_handler.py @@ -3,8 +3,8 @@ from django.conf import settings from django.template import engines from django.utils.timezone import now -from django_logging.utils.email_notifier import send_email_async -from django_logging.utils.get_config import use_email_notifier_template +from django_logging.utils.log_email_notifier import send_email_async +from django_logging.utils.get_conf import use_email_notifier_template from django_logging.middleware import RequestLogMiddleware diff --git a/django_logging/management/commands/send_logs.py b/django_logging/management/commands/send_logs.py index cf2af76..9c24207 100644 --- a/django_logging/management/commands/send_logs.py +++ b/django_logging/management/commands/send_logs.py @@ -8,7 +8,7 @@ from django.core.management.base import BaseCommand from django.conf import settings -from django_logging.constants.settings_types import LogDir +from django_logging.constants.config_types import LogDir from django_logging.validators.email_settings_validator import check_email_settings from django_logging.constants import DefaultLoggingSettings diff --git a/django_logging/settings/conf.py b/django_logging/settings/conf.py index a629ae5..bdfb24d 100644 --- a/django_logging/settings/conf.py +++ b/django_logging/settings/conf.py @@ -4,7 +4,7 @@ from typing import List, Dict, Optional from django_logging.constants import FORMAT_OPTIONS, DefaultLoggingSettings -from django_logging.constants.settings_types import ( +from django_logging.constants.config_types import ( LogLevels, LogDir, LogFileFormatsType, @@ -13,7 +13,7 @@ LogLevel, NotifierLogLevels ) -from django_logging.filters.level_filter import LoggingLevelFilter +from django_logging.filters.log_level_filter import LoggingLevelFilter class LogConfig: diff --git a/django_logging/utils/colorizer.py b/django_logging/utils/console_colorizer.py similarity index 100% rename from django_logging/utils/colorizer.py rename to django_logging/utils/console_colorizer.py diff --git a/django_logging/utils/context_manager.py b/django_logging/utils/context_manager.py index e993b89..1d73f73 100644 --- a/django_logging/utils/context_manager.py +++ b/django_logging/utils/context_manager.py @@ -4,16 +4,16 @@ from django.conf import settings from django_logging.settings.conf import LogConfig, LogManager -from django_logging.utils.get_config import get_conf, is_auto_initialization_enabled +from django_logging.utils.get_conf import get_config, is_auto_initialization_enabled @contextmanager -def config_context() -> LogManager: +def config_setup() -> LogManager: """ Context manager to temporarily apply a custom logging configuration. Raises: - ValueError: If 'django_logging' is in INSTALLED_APPS. + ValueError: If 'AUTO_INITIALIZATION_ENABLE' in DJNAGO_LOGGING is set to True. Yields: LogManager: The log manager instance with the custom configuration. @@ -29,7 +29,7 @@ def config_context() -> LogManager: original_handlers = logger.handlers.copy() try: - conf = get_conf() + conf = get_config() log_config = LogConfig(*conf) log_manager = LogManager(log_config) log_manager.create_log_files() diff --git a/django_logging/utils/get_config.py b/django_logging/utils/get_conf.py similarity index 99% rename from django_logging/utils/get_config.py rename to django_logging/utils/get_conf.py index ba4c767..8a4d300 100644 --- a/django_logging/utils/get_config.py +++ b/django_logging/utils/get_conf.py @@ -5,7 +5,7 @@ from django_logging.constants import DefaultLoggingSettings -def get_conf() -> List: +def get_config() -> List: """ Retrieve logging configuration from Django settings. diff --git a/django_logging/utils/log_email_notifier/__init__.py b/django_logging/utils/log_email_notifier/__init__.py new file mode 100644 index 0000000..d172e6f --- /dev/null +++ b/django_logging/utils/log_email_notifier/__init__.py @@ -0,0 +1,2 @@ +from .notifier import send_email_async +from .log_and_notify import log_and_notify_admin diff --git a/django_logging/utils/log_and_notify.py b/django_logging/utils/log_email_notifier/log_and_notify.py similarity index 89% rename from django_logging/utils/log_and_notify.py rename to django_logging/utils/log_email_notifier/log_and_notify.py index d879af3..f94276f 100644 --- a/django_logging/utils/log_and_notify.py +++ b/django_logging/utils/log_email_notifier/log_and_notify.py @@ -4,19 +4,19 @@ from django.conf import settings -from django_logging.constants.format_options import FORMAT_OPTIONS -from django_logging.utils.email_notifier import send_email_async -from django_logging.utils.get_config import get_conf +from django_logging.constants.log_format_options import FORMAT_OPTIONS +from django_logging.utils.log_email_notifier import send_email_async +from django_logging.utils.get_conf import get_config from django_logging.handlers import EmailHandler from django_logging.settings.conf import LogConfig -def log_and_notify( +def log_and_notify_admin( logger, level: int, message: str, extra: Optional[Dict] = None ) -> None: # Get the caller's frame to capture the correct module, file, and line number frame = inspect.currentframe().f_back - logging_settings = get_conf() + logging_settings = get_config() email_notifier_enable = getattr( logging_settings, "log_email_notifier_enable", False ) diff --git a/django_logging/utils/email_notifier.py b/django_logging/utils/log_email_notifier/notifier.py similarity index 100% rename from django_logging/utils/email_notifier.py rename to django_logging/utils/log_email_notifier/notifier.py diff --git a/django_logging/utils/setup_logging.py b/django_logging/utils/set_conf.py similarity index 93% rename from django_logging/utils/setup_logging.py rename to django_logging/utils/set_conf.py index 585ea92..95ae4bd 100644 --- a/django_logging/utils/setup_logging.py +++ b/django_logging/utils/set_conf.py @@ -2,7 +2,7 @@ from django.core.exceptions import ImproperlyConfigured -from django_logging.constants.settings_types import ( +from django_logging.constants.config_types import ( LogLevels, LogDir, LogFileFormatsType, LogLevel, LogDateFormat, FormatOption, NotifierLogLevels @@ -11,10 +11,10 @@ from typing import List from django_logging.constants.ansi_colors import AnsiColors -from django_logging.utils.get_config import is_auto_initialization_enabled, is_initialization_message_enabled +from django_logging.utils.get_conf import is_auto_initialization_enabled, is_initialization_message_enabled -def set_logging( +def set_config( log_levels: LogLevels, log_dir: LogDir, log_file_formats: LogFileFormatsType, diff --git a/django_logging/validators/config_validators.py b/django_logging/validators/config_validators.py index 692fe15..fe5d294 100644 --- a/django_logging/validators/config_validators.py +++ b/django_logging/validators/config_validators.py @@ -7,7 +7,7 @@ from django.core.checks import Error from django_logging.constants import LOG_FORMAT_SPECIFIERS, FORMAT_OPTIONS -from django_logging.constants.settings_types import ( +from django_logging.constants.config_types import ( FormatOption, LogLevels, LogEmailNotifierType, diff --git a/django_logging/validators/email_settings_validator.py b/django_logging/validators/email_settings_validator.py index 851e479..5f2c57a 100644 --- a/django_logging/validators/email_settings_validator.py +++ b/django_logging/validators/email_settings_validator.py @@ -1,7 +1,7 @@ from typing import List from django.conf import settings from django.core.checks import Error -from django_logging.constants.email_settings import EMAIL_REQUIRED_SETTINGS +from django_logging.constants.required_email_settings import EMAIL_REQUIRED_SETTINGS def check_email_settings() -> List[Error]: From 51f9e362b780c53e7d7acb359509b3ead98c0f6b Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 12:46:41 +0330 Subject: [PATCH 09/30] :zap::hammer: refactor(validators): Update email validator and EMAIL_REQUIRED_SETTINGS Refactored email_settings_validator to dynamically handle include ADMIN_EMAIL to required settings with require_admi_email flag --- django_logging/constants/required_email_settings.py | 3 ++- django_logging/management/commands/send_logs.py | 2 +- django_logging/validators/email_settings_validator.py | 10 ++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/django_logging/constants/required_email_settings.py b/django_logging/constants/required_email_settings.py index 6bb9783..f39bcb3 100644 --- a/django_logging/constants/required_email_settings.py +++ b/django_logging/constants/required_email_settings.py @@ -5,5 +5,6 @@ "EMAIL_HOST_PASSWORD", "EMAIL_USE_TLS", "DEFAULT_FROM_EMAIL", - "ADMIN_EMAIL" ] + +NOTIFIER_EXTRA_REQUIRED_SETTING = "ADMIN_EMAIL" diff --git a/django_logging/management/commands/send_logs.py b/django_logging/management/commands/send_logs.py index 9c24207..b390a08 100644 --- a/django_logging/management/commands/send_logs.py +++ b/django_logging/management/commands/send_logs.py @@ -101,7 +101,7 @@ def validate_email_settings(self): Raises ImproperlyConfigured if any of the required email settings are missing. """ - errors = check_email_settings() + errors = check_email_settings(require_admin_email=False) if errors: logger.error(errors) raise ImproperlyConfigured(errors) diff --git a/django_logging/validators/email_settings_validator.py b/django_logging/validators/email_settings_validator.py index 5f2c57a..a835240 100644 --- a/django_logging/validators/email_settings_validator.py +++ b/django_logging/validators/email_settings_validator.py @@ -1,16 +1,22 @@ from typing import List from django.conf import settings from django.core.checks import Error -from django_logging.constants.required_email_settings import EMAIL_REQUIRED_SETTINGS +from django_logging.constants.required_email_settings import ( + EMAIL_REQUIRED_SETTINGS, + NOTIFIER_EXTRA_REQUIRED_SETTING, +) -def check_email_settings() -> List[Error]: +def check_email_settings(require_admin_email=True) -> List[Error]: """ Check if all required email settings are present in the settings file. Returns a list of errors if any of the required email settings are missing. """ errors: List[Error] = [] + if require_admin_email: + EMAIL_REQUIRED_SETTINGS.append(NOTIFIER_EXTRA_REQUIRED_SETTING) + missed_settings = [ setting for setting in EMAIL_REQUIRED_SETTINGS From 099f4b8c125cec5d6e11bd097258834deb765353 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 15:11:32 +0330 Subject: [PATCH 10/30] :zap: Update EmailNotifier imports to avoid circular import --- django_logging/handlers/email_handler.py | 2 +- django_logging/utils/log_email_notifier/__init__.py | 2 -- django_logging/utils/log_email_notifier/log_and_notify.py | 2 +- django_logging/utils/log_email_notifier/notifier.py | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/django_logging/handlers/email_handler.py b/django_logging/handlers/email_handler.py index 9a7f3f4..379b66c 100644 --- a/django_logging/handlers/email_handler.py +++ b/django_logging/handlers/email_handler.py @@ -3,7 +3,7 @@ from django.conf import settings from django.template import engines from django.utils.timezone import now -from django_logging.utils.log_email_notifier import send_email_async +from django_logging.utils.log_email_notifier.notifier import send_email_async from django_logging.utils.get_conf import use_email_notifier_template from django_logging.middleware import RequestLogMiddleware diff --git a/django_logging/utils/log_email_notifier/__init__.py b/django_logging/utils/log_email_notifier/__init__.py index d172e6f..e69de29 100644 --- a/django_logging/utils/log_email_notifier/__init__.py +++ b/django_logging/utils/log_email_notifier/__init__.py @@ -1,2 +0,0 @@ -from .notifier import send_email_async -from .log_and_notify import log_and_notify_admin diff --git a/django_logging/utils/log_email_notifier/log_and_notify.py b/django_logging/utils/log_email_notifier/log_and_notify.py index f94276f..d42f819 100644 --- a/django_logging/utils/log_email_notifier/log_and_notify.py +++ b/django_logging/utils/log_email_notifier/log_and_notify.py @@ -5,7 +5,7 @@ from django.conf import settings from django_logging.constants.log_format_options import FORMAT_OPTIONS -from django_logging.utils.log_email_notifier import send_email_async +from django_logging.utils.log_email_notifier.notifier import send_email_async from django_logging.utils.get_conf import get_config from django_logging.handlers import EmailHandler from django_logging.settings.conf import LogConfig diff --git a/django_logging/utils/log_email_notifier/notifier.py b/django_logging/utils/log_email_notifier/notifier.py index 9fb9eed..2401c90 100644 --- a/django_logging/utils/log_email_notifier/notifier.py +++ b/django_logging/utils/log_email_notifier/notifier.py @@ -1,6 +1,6 @@ import logging import threading -import smtplib +from smtplib import SMTP from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart @@ -19,7 +19,7 @@ def send_email(): msg.attach(MIMEText(body, "html")) try: - server = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT) + server = SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT) server.starttls() server.login(settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD) server.sendmail( From 0cc705640e980f70ff474ffdb6c36519a8de5068 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 15:29:53 +0330 Subject: [PATCH 11/30] :rotating_light::white_check_mark::heavy_check_mark: test: send_logs command include & pass tests for send_logs command --- django_logging/tests/commands/__init__.py | 0 .../tests/commands/test_send_logs.py | 132 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 django_logging/tests/commands/__init__.py create mode 100644 django_logging/tests/commands/test_send_logs.py diff --git a/django_logging/tests/commands/__init__.py b/django_logging/tests/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_logging/tests/commands/test_send_logs.py b/django_logging/tests/commands/test_send_logs.py new file mode 100644 index 0000000..51336b9 --- /dev/null +++ b/django_logging/tests/commands/test_send_logs.py @@ -0,0 +1,132 @@ +import os +import tempfile +import shutil + +from unittest.mock import patch, ANY + +from django.core.exceptions import ImproperlyConfigured +from django.core.management import call_command +from django.test import TestCase + + +class SendLogsCommandTests(TestCase): + + @patch( + "django_logging.management.commands.send_logs.Command.validate_email_settings" + ) + @patch("django_logging.management.commands.send_logs.shutil.make_archive") + @patch("django_logging.management.commands.send_logs.EmailMessage") + def test_handle_success( + self, mock_email_message, mock_make_archive, mock_validate_email_settings + ): + # Setup + temp_log_dir = tempfile.mkdtemp() + temp_file = tempfile.NamedTemporaryFile(delete=False) + temp_file.close() + mock_make_archive.return_value = temp_file.name + + # Mock settings + with self.settings(DJANGO_LOGGING={"LOG_DIR": temp_log_dir}): + # Execute command + call_command("send_logs", "test@example.com") + + # Assert + mock_validate_email_settings.assert_called_once() + mock_make_archive.assert_called_once_with(ANY, "zip", temp_log_dir) + mock_email_message.assert_called_once() + + # Cleanup + shutil.rmtree(temp_log_dir) + + ( + os.remove(temp_file.name + ".zip") + if os.path.exists(temp_file.name + ".zip") + else None + ) + + @patch( + "django_logging.management.commands.send_logs.Command.validate_email_settings" + ) + @patch("django_logging.management.commands.send_logs.EmailMessage.send") + def test_handle_email_send_failure( + self, mock_email_send, mock_validate_email_settings + ): + # Setup + temp_log_dir = tempfile.mkdtemp() + mock_email_send.side_effect = Exception("Email send failed") + + # Mock settings + with self.settings(DJANGO_LOGGING={"LOG_DIR": temp_log_dir}): + with self.assertLogs( + "django_logging.management.commands.send_logs", level="ERROR" + ) as cm: + call_command("send_logs", "test@example.com") + + # Assert + self.assertIn( + "ERROR:django_logging.management.commands.send_logs:Failed to send logs: Email send failed", + cm.output, + ) + + # Cleanup + shutil.rmtree(temp_log_dir) + + @patch( + "django_logging.management.commands.send_logs.Command.validate_email_settings" + ) + def test_handle_missing_log_dir(self, mock_validate_email_settings): + # Mock settings with a non-existent log directory + non_existent_dir = "/non/existent/directory" + with self.settings(DJANGO_LOGGING={"LOG_DIR": non_existent_dir}): + with self.assertLogs( + "django_logging.management.commands.send_logs", level="ERROR" + ) as cm: + # Call the command and check that no exception is raised but logs are captured + call_command("send_logs", "test@example.com") + + # Check if the correct error message is logged + self.assertIn( + f'ERROR:django_logging.management.commands.send_logs:Log directory "{non_existent_dir}" does not exist.', + cm.output, + ) + + # Check that validate_email_settings was not called + mock_validate_email_settings.assert_not_called() + + @patch( + "django_logging.management.commands.send_logs.check_email_settings", + return_value=None, + ) + def test_validate_email_settings_success(self, mock_check_email_settings): + call_command("send_logs", "test@example.com") + mock_check_email_settings.assert_called_once() + + @patch( + "django_logging.management.commands.send_logs.check_email_settings", + return_value="Missing config", + ) + def test_validate_email_settings_failure(self, mock_check_email_settings): + with self.assertRaises(ImproperlyConfigured): + call_command("send_logs", "test@example.com") + + @patch( + "django_logging.management.commands.send_logs.Command.validate_email_settings" + ) + @patch("django_logging.management.commands.send_logs.shutil.make_archive") + def test_cleanup_on_failure(self, mock_make_archive, mock_validate_email_settings): + # Setup + temp_log_dir = tempfile.mkdtemp() + temp_file = tempfile.NamedTemporaryFile(delete=False) + temp_file.close() + mock_make_archive.side_effect = Exception("Archive failed") + + # Mock settings + with self.settings(DJANGO_LOGGING={"LOG_DIR": temp_log_dir}): + with self.assertRaises(Exception): + call_command("send_logs", "test@example.com") + + # Assert + self.assertFalse(os.path.exists(temp_file.name + ".zip")) + + # Cleanup + shutil.rmtree(temp_log_dir) From 0242c80b567c41b8bbbfcca300559b30d43e1ed1 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 15:31:50 +0330 Subject: [PATCH 12/30] :rotating_light::white_check_mark::heavy_check_mark: test: log_level_filter module test passed for LoggingLevelFilter class --- django_logging/tests/filters/__init__.py | 0 .../tests/filters/test_log_level_filter.py | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 django_logging/tests/filters/__init__.py create mode 100644 django_logging/tests/filters/test_log_level_filter.py diff --git a/django_logging/tests/filters/__init__.py b/django_logging/tests/filters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_logging/tests/filters/test_log_level_filter.py b/django_logging/tests/filters/test_log_level_filter.py new file mode 100644 index 0000000..aa1839e --- /dev/null +++ b/django_logging/tests/filters/test_log_level_filter.py @@ -0,0 +1,46 @@ +import logging +import pytest +from django_logging.filters import LoggingLevelFilter + + +@pytest.fixture +def log_record(): + """Fixture to create a dummy log record.""" + return logging.LogRecord( + name="test", + level=logging.DEBUG, + pathname=__file__, + lineno=10, + msg="Test message", + args=(), + exc_info=None, + ) + + +def test_logging_level_filter_initialization(): + """Test that LoggingLevelFilter initializes with the correct logging level.""" + filter_instance = LoggingLevelFilter(logging.INFO) + assert filter_instance.logging_level == logging.INFO, ( + f"Expected logging_level to be {logging.INFO}, " + f"got {filter_instance.logging_level}" + ) + + +def test_logging_level_filter_passes_matching_level(log_record): + """Test that the filter passes log records with the matching level.""" + log_record.levelno = logging.DEBUG + filter_instance = LoggingLevelFilter(logging.DEBUG) + + assert filter_instance.filter(log_record), ( + f"Expected filter to return True for log level {logging.DEBUG}, " f"got False" + ) + + +def test_logging_level_filter_blocks_non_matching_level(log_record): + """Test that the filter blocks log records with a non-matching level.""" + log_record.levelno = logging.WARNING + filter_instance = LoggingLevelFilter(logging.ERROR) + + assert not filter_instance.filter(log_record), ( + f"Expected filter to return False for log level {logging.WARNING}, " f"got True" + ) From 946066766a971e0d48d60fa01e2521bd08f0199c Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 15:35:13 +0330 Subject: [PATCH 13/30] :rotating_light::white_check_mark::heavy_check_mark: test: colored_formatter module Implemented test to ensure correct behavior of ColoredFormatter make tests passed --- django_logging/tests/formatters/__init__.py | 0 .../formatters/test_colored_formatter.py | 74 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 django_logging/tests/formatters/__init__.py create mode 100644 django_logging/tests/formatters/test_colored_formatter.py diff --git a/django_logging/tests/formatters/__init__.py b/django_logging/tests/formatters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_logging/tests/formatters/test_colored_formatter.py b/django_logging/tests/formatters/test_colored_formatter.py new file mode 100644 index 0000000..5e8d47a --- /dev/null +++ b/django_logging/tests/formatters/test_colored_formatter.py @@ -0,0 +1,74 @@ +import logging +import pytest +from unittest.mock import patch +from django_logging.formatters import ColorizedFormatter + + +@pytest.fixture +def log_record(): + """Fixture to create a dummy log record.""" + return logging.LogRecord( + name="test", + level=logging.DEBUG, + pathname=__file__, + lineno=10, + msg="Test message", + args=(), + exc_info=None, + ) + + +@pytest.fixture +def formatter(): + """Fixture to create a ColoredFormatter instance with a specific format.""" + return ColorizedFormatter(fmt="%(levelname)s: %(message)s") + + +@patch( + "django_logging.formatters.colored_formatter.colorize_log_format", autospec=True +) +@patch( + "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", + side_effect=lambda fmt: fmt, +) +def test_format_applies_colorization( + mock_remove_ansi, mock_colorize, formatter, log_record +): + """Test that the format method applies colorization.""" + # Mock the colorize_log_format to return a predictable format + mock_colorize.return_value = "%(levelname)s: %(message)s" + + formatter.format(log_record) + + # Ensuring colorization should have been triggered + mock_colorize.assert_called_once_with( + "%(levelname)s: %(message)s", log_record.levelname + ) + + +@patch( + "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", + side_effect=lambda fmt: fmt, +) +def test_format_resets_to_original_format(mock_remove_ansi, formatter, log_record): + """Test that the format method resets the format string after formatting.""" + original_format = formatter._style._fmt + formatter.format(log_record) + assert ( + formatter._style._fmt == original_format + ), f"Expected format string to reset to original format, but got {formatter._style._fmt}" + + +@patch( + "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", + side_effect=lambda fmt: fmt, +) +def test_format_returns_formatted_output(formatter, log_record): + """Test that the format method returns the correctly formatted output.""" + expected_output = f"{logging.getLevelName(log_record.levelno)}: {log_record.msg}" + formatted_output = formatter.format(log_record) + + # Directly comparing the output assuming it might include color codes + assert formatted_output.startswith( + f"DEBUG: Test message" + ), f"Expected formatted output to start with '{expected_output}', but got '{formatted_output}'" From 7148c0d91072e689bd357a430735536cd1923da2 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 15:36:32 +0330 Subject: [PATCH 14/30] :rotating_light::white_check_mark::heavy_check_mark: test: email_handler module Implemented test to ensure correct behavior of EmailHandler make tests passed --- django_logging/tests/handlers/__init__.py | 0 .../tests/handlers/test_email_handler.py | 112 ++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 django_logging/tests/handlers/__init__.py create mode 100644 django_logging/tests/handlers/test_email_handler.py diff --git a/django_logging/tests/handlers/__init__.py b/django_logging/tests/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_logging/tests/handlers/test_email_handler.py b/django_logging/tests/handlers/test_email_handler.py new file mode 100644 index 0000000..ec32f38 --- /dev/null +++ b/django_logging/tests/handlers/test_email_handler.py @@ -0,0 +1,112 @@ +import logging +import pytest +from unittest.mock import patch, MagicMock, ANY + +from django.conf import settings + +from django_logging.handlers.email_handler import EmailHandler + + +@pytest.fixture +def log_record(): + """Fixture to create a dummy log record.""" + return logging.LogRecord( + name="test", + level=logging.ERROR, + pathname=__file__, + lineno=10, + msg="Test message", + args=(), + exc_info=None, + ) + + +@pytest.fixture +def email_handler(): + """Fixture to create an EmailHandler instance.""" + return EmailHandler() + + +@patch("django_logging.handlers.email_handler.send_email_async") +@patch("django_logging.handlers.email_handler.EmailHandler.render_template") +@patch( + "django_logging.handlers.email_handler.use_email_notifier_template", + return_value=True, +) +def test_emit_with_html_template( + mock_use_template, mock_render_template, mock_send_email, email_handler, log_record +): + """Test the emit method with HTML template.""" + mock_render_template.return_value = "Formatted Log" + + email_handler.emit(log_record) + + mock_render_template.assert_called_once_with("Test message", None) + mock_send_email.assert_called_once_with( + "New Log Record: ERROR", "Formatted Log", [settings.ADMIN_EMAIL] + ) + + +@patch("django_logging.handlers.email_handler.send_email_async") +@patch( + "django_logging.handlers.email_handler.use_email_notifier_template", + return_value=False, +) +def test_emit_without_html_template(mock_use_template, mock_send_email, log_record): + """Test the emit method without HTML template.""" + email_handler = EmailHandler() + email_handler.emit(log_record) + + mock_send_email.assert_called_once_with( + "New Log Record: ERROR", "Test message", [settings.ADMIN_EMAIL] + ) + + +@patch("django_logging.handlers.email_handler.EmailHandler.handleError") +@patch( + "django_logging.handlers.email_handler.send_email_async", + side_effect=Exception("Email send failed"), +) +def test_emit_handles_exception( + mock_send_email, mock_handle_error, email_handler, log_record +): + """Test that emit handles exceptions properly.""" + email_handler.emit(log_record) + + mock_handle_error.assert_called_once_with(log_record) + + +@patch( + "django_logging.handlers.email_handler.RequestLogMiddleware.get_ip_address", + return_value="127.0.0.1", +) +@patch( + "django_logging.handlers.email_handler.RequestLogMiddleware.get_user_agent", + return_value="Mozilla/5.0", +) +@patch("django_logging.handlers.email_handler.engines") +def test_render_template(mock_engines, mock_get_user_agent, mock_get_ip_address): + """Test the render_template method.""" + mock_template = MagicMock() + mock_template.render.return_value = "Formatted Log" + mock_django_engine = MagicMock() + mock_django_engine.get_template.return_value = mock_template + mock_engines.__getitem__.return_value = mock_django_engine + + email_handler = EmailHandler() + + mock_request = MagicMock() + rendered_output = email_handler.render_template("Test message", mock_request) + + mock_django_engine.get_template.assert_called_once_with( + "email_notifier_template.html" + ) + mock_template.render.assert_called_once_with( + { + "message": "Test message", + "time": ANY, # The actual time is not critical to the test + "browser_type": "Mozilla/5.0", + "ip_address": "127.0.0.1", + } + ) + assert rendered_output == "Formatted Log" From 0b7d2072eb103a18e7fb19d2ba2f568c120e3f81 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 15:38:23 +0330 Subject: [PATCH 15/30] :rotating_light::white_check_mark::heavy_check_mark: test: request_middleware module Implemented test to ensure correct behavior of RequestMiddleWare and correct modifying request make tests passed --- django_logging/tests/middleware/__init__.py | 0 .../middleware/test_request_middleware.py | 74 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 django_logging/tests/middleware/__init__.py create mode 100644 django_logging/tests/middleware/test_request_middleware.py diff --git a/django_logging/tests/middleware/__init__.py b/django_logging/tests/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_logging/tests/middleware/test_request_middleware.py b/django_logging/tests/middleware/test_request_middleware.py new file mode 100644 index 0000000..f3ea5e1 --- /dev/null +++ b/django_logging/tests/middleware/test_request_middleware.py @@ -0,0 +1,74 @@ +import logging +from unittest.mock import Mock + +import pytest +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser +from django.http import HttpResponse +from django.test import RequestFactory +from django_logging.middleware import RequestLogMiddleware + + +@pytest.fixture +def request_factory(): + return RequestFactory() + + +@pytest.fixture +def get_response(): + def _get_response(request): + return HttpResponse("Test Response") + + return _get_response + + +@pytest.fixture +def middleware(get_response): + return RequestLogMiddleware(get_response) + + +def test_authenticated_user_logging(middleware, request_factory, caplog): + request = request_factory.get("/test-path") + + UserModel = get_user_model() + username_field = UserModel.USERNAME_FIELD + + request.user = Mock() + request.user.is_authenticated = True + setattr(request.user, username_field, "test_user") + + with caplog.at_level(logging.INFO): + middleware(request) + + assert "Request Info" in caplog.text + assert "test-path" in caplog.text + assert "test_user" in caplog.text + assert request.ip_address + assert request.browser_type + + +def test_anonymous_user_logging(middleware, request_factory, caplog): + request = request_factory.get("/test-path") + request.user = AnonymousUser() + + with caplog.at_level(logging.INFO): + middleware(request) + + assert "Request Info" in caplog.text + assert "Anonymous" in caplog.text + + +def test_ip_address_extraction(middleware, request_factory): + request = request_factory.get("/test-path", HTTP_X_FORWARDED_FOR="192.168.1.1") + + middleware(request) + + assert request.ip_address == "192.168.1.1" + + +def test_user_agent_extraction(middleware, request_factory): + request = request_factory.get("/test-path", HTTP_USER_AGENT="Mozilla/5.0") + + middleware(request) + + assert request.browser_type == "Mozilla/5.0" From a4597be2e1bdf50459402052408ba1c62257dc2a Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 15:40:58 +0330 Subject: [PATCH 16/30] :rotating_light::white_check_mark::heavy_check_mark: test: test_checks and test_conf Implemented test to ensure correct validation in checks file and correct configuration process of conf file make tests passed --- django_logging/tests/settings/__init__.py | 0 django_logging/tests/settings/test_checks.py | 154 +++++++++++++++++++ django_logging/tests/settings/test_conf.py | 126 +++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 django_logging/tests/settings/__init__.py create mode 100644 django_logging/tests/settings/test_checks.py create mode 100644 django_logging/tests/settings/test_conf.py diff --git a/django_logging/tests/settings/__init__.py b/django_logging/tests/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_logging/tests/settings/test_checks.py b/django_logging/tests/settings/test_checks.py new file mode 100644 index 0000000..86a36a7 --- /dev/null +++ b/django_logging/tests/settings/test_checks.py @@ -0,0 +1,154 @@ +from unittest.mock import patch + +import pytest +from django.core.checks import Error +from django.conf import settings +from django_logging.settings.checks import check_logging_settings + + +@pytest.fixture +def reset_settings(): + """Fixture to reset Django settings after each test.""" + original_settings = settings.DJANGO_LOGGING + yield + settings.DJANGO_LOGGING = original_settings + + +def test_valid_logging_settings(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_DIR": "logs", + "LOG_FILE_LEVELS": ["DEBUG", "INFO", "ERROR"], + "LOG_FILE_FORMATS": {"DEBUG": "%(levelname)s: %(message)s"}, + "LOG_CONSOLE_FORMAT": "%(message)s", + "LOG_CONSOLE_LEVEL": "DEBUG", + "LOG_CONSOLE_COLORIZE": True, + "LOG_DATE_FORMAT": "%Y-%m-%d %H:%M:%S", + "AUTO_INITIALIZATION_ENABLE": True, + "INITIALIZATION_MESSAGE_ENABLE": True, + "LOG_EMAIL_NOTIFIER": { + "ENABLE": True, + "NOTIFY_ERROR": True, + "NOTIFY_CRITICAL": False, + "USE_TEMPLATE": True, + "LOG_FORMAT": 1, + }, + } + errors = check_logging_settings(None) + assert not errors + + +def test_invalid_log_dir(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_DIR": 1, + } + errors = check_logging_settings(None) + assert any(error.id == "django_logging.E001_LOG_DIR" for error in errors) + + +def test_invalid_log_file_levels(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_FILE_LEVELS": ["invalid"], + } + errors = check_logging_settings(None) + assert any(error.id == "django_logging.E007_LOG_FILE_LEVELS" for error in errors) + + +def test_invalid_log_file_formats(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_FILE_FORMATS": { + "DEBUG": "%(levelname)s: %(invalid)s", + "invalid": "%(message)s", + }, + } + errors = check_logging_settings(None) + assert any( + error.id == "django_logging.E011_LOG_FILE_FORMATS['DEBUG']" for error in errors + ) + assert any(error.id == "django_logging.E019_LOG_FILE_FORMATS" for error in errors) + + settings.DJANGO_LOGGING = {"LOG_FILE_FORMATS": ["invalid type"]} + errors = check_logging_settings(None) + assert any(error.id == "django_logging.E020_LOG_FILE_FORMATS" for error in errors) + + +def test_invalid_log_console_format(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_CONSOLE_FORMAT": "invalid", + } + errors = check_logging_settings(None) + assert any(error.id == "django_logging.E010_LOG_CONSOLE_FORMAT" for error in errors) + + +def test_invalid_log_console_level(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_CONSOLE_LEVEL": 10, + } + errors = check_logging_settings(None) + assert any(error.id == "django_logging.E006_LOG_CONSOLE_LEVEL" for error in errors) + + +def test_invalid_log_console_colorize(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_CONSOLE_COLORIZE": "not_a_boolean", + } + errors = check_logging_settings(None) + assert any( + error.id == "django_logging.E014_LOG_CONSOLE_COLORIZE" for error in errors + ) + + +def test_invalid_log_date_format(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_DATE_FORMAT": "%invalid_format", + } + errors = check_logging_settings(None) + assert any(error.id == "django_logging.E016_LOG_DATE_FORMAT" for error in errors) + + +def test_invalid_auto_initialization_enable(reset_settings): + settings.DJANGO_LOGGING = { + "AUTO_INITIALIZATION_ENABLE": "not_a_boolean", + } + errors = check_logging_settings(None) + assert any( + error.id == "django_logging.E014_AUTO_INITIALIZATION_ENABLE" for error in errors + ) + + +def test_invalid_initialization_message_enable(reset_settings): + settings.DJANGO_LOGGING = { + "INITIALIZATION_MESSAGE_ENABLE": "not_a_boolean", + } + errors = check_logging_settings(None) + assert any( + error.id == "django_logging.E014_INITIALIZATION_MESSAGE_ENABLE" + for error in errors + ) + + +def test_invalid_log_email_notifier(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_EMAIL_NOTIFIER": { + "ENABLE": "not_a_boolean", + }, + } + errors = check_logging_settings(None) + assert any( + error.id == "django_logging.E018_LOG_EMAIL_NOTIFIER['ENABLE']" + for error in errors + ) + + +def test_missing_email_settings(reset_settings): + settings.DJANGO_LOGGING = { + "LOG_EMAIL_NOTIFIER": { + "ENABLE": True, + }, + } + # Mocking check_email_settings to return errors + with patch("django_logging.settings.checks.check_email_settings") as mock_check: + mock_check.return_value = [ + Error("EMAIL_BACKEND not set.", id="django_logging.E010_EMAIL_SETTINGS") + ] + errors = check_logging_settings(None) + assert any(error.id == "django_logging.E010_EMAIL_SETTINGS" for error in errors) diff --git a/django_logging/tests/settings/test_conf.py b/django_logging/tests/settings/test_conf.py new file mode 100644 index 0000000..0d59ad4 --- /dev/null +++ b/django_logging/tests/settings/test_conf.py @@ -0,0 +1,126 @@ +import pytest +from unittest import mock +import os + +from django_logging.settings.conf import LogConfig, LogManager +from django_logging.constants import FORMAT_OPTIONS + + +@pytest.fixture +def log_config(): + return LogConfig( + log_levels=["INFO", "WARNING", "ERROR"], + log_dir="/tmp/logs", + log_file_formats={"INFO": 1, "WARNING": None, "ERROR": "%(message)s"}, + console_level="INFO", + console_format=1, + colorize_console=False, + log_date_format="%Y-%m-%d %H:%M:%S", + log_email_notifier_enable=True, + log_email_notifier_log_levels=["ERROR"], + log_email_notifier_log_format=1, + ) + + +@pytest.fixture +def log_manager(log_config): + return LogManager(log_config) + + +def test_resolve_format(): + # Test with int format option + resolved_format_option = LogConfig.resolve_format(1, use_colors=False) + resolved_none_format = LogConfig.resolve_format(None, use_colors=False) + + assert resolved_format_option == FORMAT_OPTIONS[1] + assert resolved_none_format + + # Test with str format option + resolved_format = LogConfig.resolve_format("%(message)s", use_colors=False) + assert resolved_format == "%(message)s" + + # Test with color enabled + resolved_format = LogConfig.resolve_format("%(message)s", use_colors=True) + assert resolved_format == "%(message)s" + + +def test_remove_ansi_escape_sequences(): + ansi_string = "\x1b[31mERROR\x1b[0m" + clean_string = LogConfig.remove_ansi_escape_sequences(ansi_string) + assert clean_string == "ERROR" + + +def test_create_log_files(log_manager): + with mock.patch("os.makedirs") as makedirs_mock, mock.patch( + "os.path.exists" + ) as path_exists_mock, mock.patch("builtins.open", mock.mock_open()) as open_mock: + # Mock path_exists to return False so that it always attempts to create the file + path_exists_mock.return_value = False + log_manager.log_config.log_levels = ["INFO", "ERROR"] + log_manager.log_files = {} + + log_manager.create_log_files() + + # Check that the directories were created + makedirs_mock.assert_called_with("/tmp/logs", exist_ok=True) + + # Verify the log files are created + assert open_mock.call_count == len(log_manager.log_config.log_levels) + + # Verify file creation paths + for log_level in log_manager.log_config.log_levels: + expected_file_path = os.path.join("/tmp/logs", f"{log_level.lower()}.log") + open_mock.assert_any_call(expected_file_path, "w") + + log_manager.log_files[log_level] = expected_file_path + + +def test_set_conf(log_manager): + with mock.patch("logging.config.dictConfig") as dictConfig_mock: + log_manager.create_log_files() + log_manager.set_conf() + + # Check that the logging config was set + assert dictConfig_mock.called + + config = dictConfig_mock.call_args[0][0] + + # Verify the structure of the config + assert "version" in config + assert "formatters" in config + assert "handlers" in config + assert "loggers" in config + assert "root" in config + + # Check formatters + assert "info" in config["formatters"] + assert "error" in config["formatters"] + assert "console" in config["formatters"] + assert "email" in config["formatters"] + + # Check handlers + assert "info" in config["handlers"] + assert "error" in config["handlers"] + assert "console" in config["handlers"] + assert "email_error" in config["handlers"] + + # Check loggers + assert "info" in config["loggers"] + assert "error" in config["loggers"] + + # Check the root logger + assert "handlers" in config["root"] + assert "disable_existing_loggers" in config + + +def test_log_manager_get_log_file(log_manager): + with mock.patch("os.makedirs"), mock.patch("builtins.open", mock.mock_open()): + + log_manager.create_log_files() + + # Check retrieving the log files + assert log_manager.get_log_file("INFO") == "/tmp/logs\\info.log" + assert log_manager.get_log_file("ERROR") == "/tmp/logs\\error.log" + + # Check for a non-existent log level + assert log_manager.get_log_file("DEBUG") is None From 8fa7b1d7b859ad5ad39eb6e45f49c45382bb26df Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 15:43:27 +0330 Subject: [PATCH 17/30] :rotating_light::white_check_mark::heavy_check_mark: test(utils): test_context_manager Implemented test to ensure correct temporary configuration using config_setup make tests passed --- .../tests/utils/test_context_manager.py | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 django_logging/tests/utils/test_context_manager.py diff --git a/django_logging/tests/utils/test_context_manager.py b/django_logging/tests/utils/test_context_manager.py new file mode 100644 index 0000000..2e89219 --- /dev/null +++ b/django_logging/tests/utils/test_context_manager.py @@ -0,0 +1,118 @@ +import logging +from unittest import mock +import pytest + +from django_logging.utils.context_manager import ( + config_setup, + _restore_logging_config, +) + + +@pytest.fixture +def mock_logger(): + """Fixture to create a mock logger for testing.""" + logger = logging.getLogger() + with mock.patch.object(logger, "manager", new_callable=mock.Mock): + yield logger + + +def test_config_setup_auto_initialization_enabled(): + """Test that ValueError is raised when auto-initialization is enabled.""" + with mock.patch( + "django_logging.utils.context_manager.is_auto_initialization_enabled", + return_value=True, + ): + with pytest.raises(ValueError) as excinfo: + with config_setup(): "" + + assert ( + str(excinfo.value) + == "you most set 'AUTO_INITIALIZATION_ENABLE' to False in DJANGO_LOGGING in your settings" + ) + + +def test_config_setup_applies_custom_config(mock_logger): + """Test that the custom logging configuration is applied.""" + with mock.patch( + "django_logging.utils.context_manager.is_auto_initialization_enabled", + return_value=False, + ): + with mock.patch( + "django_logging.utils.context_manager.get_config", + return_value=( + ["INFO"], + "/tmp/logs", + {"INFO": 1}, + "DEBUG", + 2, + False, + "", + False, + [], + 1, + ), + ): + with mock.patch( + "django_logging.utils.context_manager.LogManager" + ) as MockLogManager: + mock_log_manager = MockLogManager.return_value + + with config_setup() as log_manager: + # Assert that the custom log manager is used + assert log_manager is mock_log_manager + + # Assert the log files are created and config is set + mock_log_manager.create_log_files.assert_called_once() + mock_log_manager.set_conf.assert_called_once() + + +def test_config_context_restores_original_config(mock_logger): + """Test that the original logging configuration is restored after context exit.""" + original_config = mock_logger.manager.loggerDict + original_level = mock_logger.level + original_handlers = mock_logger.handlers + + with mock.patch( + "django_logging.utils.context_manager.is_auto_initialization_enabled", + return_value=False, + ): + with mock.patch( + "django_logging.utils.context_manager.get_config", + return_value=( + ["INFO"], + "/tmp/logs", + {"INFO": 1}, + "DEBUG", + 2, + False, + "", + False, + [], + 1, + ), + ): + with mock.patch("django_logging.utils.context_manager.LogManager"): + with config_setup(): + # Change the logger's configuration + mock_logger.level = logging.ERROR + mock_logger.handlers.append(logging.NullHandler()) + + # After exiting the context, original config should be restored + assert mock_logger.manager.loggerDict == original_config + assert mock_logger.level == original_level + assert mock_logger.handlers == original_handlers + + +def test_restore_logging_config(mock_logger): + """Test the _restore_logging_config helper function.""" + original_config = mock_logger.manager.loggerDict + original_level = mock_logger.level + original_handlers = mock_logger.handlers + + _restore_logging_config( + mock_logger, original_config, original_level, original_handlers + ) + + assert mock_logger.manager.loggerDict == original_config + assert mock_logger.level == original_level + assert mock_logger.handlers == original_handlers From 060212bf85dcdefb82960e5566a2adcc28ddbfc4 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 15:48:37 +0330 Subject: [PATCH 18/30] :rotating_light::hammer::white_check_mark::heavy_check_mark: test(utils): test_email_notifier Refactored notifier/send_email_async to set a threading event to make test wait until thread pass --------- Implemented test to ensure correct behavior of email notifier make tests passed --- .../tests/utils/test_email_notifier.py | 103 ++++++++++++++++++ .../utils/log_email_notifier/notifier.py | 6 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 django_logging/tests/utils/test_email_notifier.py diff --git a/django_logging/tests/utils/test_email_notifier.py b/django_logging/tests/utils/test_email_notifier.py new file mode 100644 index 0000000..464d4f3 --- /dev/null +++ b/django_logging/tests/utils/test_email_notifier.py @@ -0,0 +1,103 @@ +import logging +import pytest +import threading +from unittest.mock import patch, MagicMock +from django_logging.utils.log_email_notifier.notifier import send_email_async + + +@pytest.fixture +def mock_smtp(): + with patch("django_logging.utils.log_email_notifier.notifier.SMTP") as mock_smtp: + yield mock_smtp + + +@pytest.fixture +def mock_settings(): + with patch("django_logging.utils.log_email_notifier.notifier.settings") as mock_settings: + mock_settings.DEFAULT_FROM_EMAIL = "from@example.com" + mock_settings.EMAIL_HOST = "smtp.example.com" + mock_settings.EMAIL_PORT = 587 + mock_settings.EMAIL_HOST_USER = "user@example.com" + mock_settings.EMAIL_HOST_PASSWORD = "password" + yield mock_settings + + +@pytest.fixture +def mock_logger(): + with patch("django_logging.utils.log_email_notifier.notifier.logger.info") as mock_info, patch( + "django_logging.utils.log_email_notifier.notifier.logger.warning" + ) as mock_warning: + yield mock_info, mock_warning + + +# Test for successful email sending +def test_send_email_async_success(mock_smtp, mock_settings, mock_logger): + mock_info, mock_warning = mock_logger + + # Create an event to signal when the email has been sent + email_sent_event = threading.Event() + + # Pass the event to the send_email_async function + send_email_async( + "Test Subject", "Test Body", ["to@example.com"], event=email_sent_event + ) + + # Wait for the event to be set, indicating the email was sent + email_sent_event.wait() + + # Ensure the SMTP server was called with the correct parameters + mock_smtp.assert_called_once_with( + mock_settings.EMAIL_HOST, mock_settings.EMAIL_PORT + ) + mock_smtp_instance = mock_smtp.return_value + + # Check if login was called with the correct credentials + mock_smtp_instance.login.assert_called_once_with( + mock_settings.EMAIL_HOST_USER, mock_settings.EMAIL_HOST_PASSWORD + ) + + # Capture the arguments passed to sendmail + sendmail_args = mock_smtp_instance.sendmail.call_args[0] + + # Verify the email content + expected_from = mock_settings.DEFAULT_FROM_EMAIL + expected_to = ["to@example.com"] + expected_subject = "Test Subject" + expected_body = "Test Body" + + # Check that the 'From' and 'To' fields are correct + assert sendmail_args[0] == expected_from + assert sendmail_args[1] == expected_to + + # Parse the actual email content and compare it to the expected content + actual_email_content = sendmail_args[2] + assert f"Subject: {expected_subject}" in actual_email_content + assert expected_body in actual_email_content + + # Check if the SMTP server was properly quit + mock_smtp_instance.quit.assert_called_once() + + # Ensure the success log message was written + mock_info.assert_called_once_with( + "Log Record has been sent to ADMIN EMAIL successfully." + ) + mock_warning.assert_not_called() + + +def test_send_email_async_failure(mock_smtp, mock_settings, mock_logger): + mock_info, mock_warning = mock_logger + mock_smtp.side_effect = Exception("SMTP failure") + + email_sent_event = threading.Event() + + send_email_async( + "Test Subject", "Test Body", ["to@example.com"], event=email_sent_event + ) + + email_sent_event.wait() + + # Ensure the failure log message was written + mock_warning.assert_called_once_with( + "Email Notifier failed to send Log Record: SMTP failure" + ) + mock_info.assert_not_called() diff --git a/django_logging/utils/log_email_notifier/notifier.py b/django_logging/utils/log_email_notifier/notifier.py index 2401c90..5bca62f 100644 --- a/django_logging/utils/log_email_notifier/notifier.py +++ b/django_logging/utils/log_email_notifier/notifier.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) -def send_email_async(subject, body, recipient_list): +def send_email_async(subject, body, recipient_list, event=None): def send_email(): msg = MIMEMultipart() msg["From"] = settings.DEFAULT_FROM_EMAIL @@ -31,6 +31,10 @@ def send_email(): except Exception as e: logger.warning(f"Email Notifier failed to send Log Record: {e}") + finally: + if event: + event.set() # set event that waits until email send. (used for Tests) + # Start a new thread to send the email asynchronously email_thread = threading.Thread(target=send_email) email_thread.start() From 384dd8c4e721f77f69237409742ed5005fca6d43 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 16:29:27 +0330 Subject: [PATCH 19/30] :rotating_light::white_check_mark::heavy_check_mark: test(utils): test_get_conf test_set_conf _test_log_and_notify test_get_conf: -Added comprehensive test coverage for the get_config function, ensuring that all logging configurations are correctly retrieved from Django settings with fallbacks to default values. test_log_and_notify: -Implemented tests for the log_and_notify_admin function to verify that log messages are properly logged and email notifications are sent when enabled. -Tested different scenarios including valid configurations, missing settings, and email notifier behavior. test_set_conf: -Developed tests for the set_conf module to validate that the logging system is initialized correctly based on the provided configurations. -Verified the correct setup of loggers, handlers, formatters, and filters to ensure consistent logging behavior. --- django_logging/tests/utils/__init__.py | 0 django_logging/tests/utils/test_get_conf.py | 84 ++++++++++ .../tests/utils/test_log_and_notify.py | 144 +++++++++++++++++ django_logging/tests/utils/test_set_conf.py | 149 ++++++++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 django_logging/tests/utils/__init__.py create mode 100644 django_logging/tests/utils/test_get_conf.py create mode 100644 django_logging/tests/utils/test_log_and_notify.py create mode 100644 django_logging/tests/utils/test_set_conf.py diff --git a/django_logging/tests/utils/__init__.py b/django_logging/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_logging/tests/utils/test_get_conf.py b/django_logging/tests/utils/test_get_conf.py new file mode 100644 index 0000000..ba57f50 --- /dev/null +++ b/django_logging/tests/utils/test_get_conf.py @@ -0,0 +1,84 @@ +import pytest +from unittest.mock import patch +from django.conf import settings +from django_logging.utils.get_conf import ( + get_config, + use_email_notifier_template, + is_auto_initialization_enabled, + is_initialization_message_enabled, +) + + +@pytest.fixture +def mock_settings(): + """Fixture to mock Django settings.""" + mock_settings = { + "DJANGO_LOGGING": { + "LOG_FILE_LEVELS": ["DEBUG", "INFO"], + "LOG_DIR": "/custom/log/dir", + "LOG_FILE_FORMATS": { + "DEBUG": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + }, + "LOG_CONSOLE_LEVEL": "WARNING", + "LOG_CONSOLE_FORMAT": "%(levelname)s - %(message)s", + "LOG_CONSOLE_COLORIZE": True, + "LOG_DATE_FORMAT": "%Y-%m-%d", + "LOG_EMAIL_NOTIFIER": { + "ENABLE": True, + "NOTIFY_ERROR": True, + "NOTIFY_CRITICAL": False, + "LOG_FORMAT": "custom_format", + }, + } + } + with patch.object(settings, "DJANGO_LOGGING", mock_settings["DJANGO_LOGGING"]): + yield mock_settings + + +def test_get_conf(mock_settings): + expected = [ + ["DEBUG", "INFO"], # log_levels + "/custom/log/dir", # log_dir + { + "DEBUG": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + }, # log_file_formats + "WARNING", # console_level + "%(levelname)s - %(message)s", # console_format + True, # colorize_console + "%Y-%m-%d", # log_date_format + True, # log_email_notifier_enable + ["ERROR", None], # log_email_notifier_log_levels + "custom_format", # log_email_notifier_log_format + ] + result = get_config() + assert result == expected + + +def test_use_email_notifier_template(mock_settings): + # By default, USE_TEMPLATE is True + assert use_email_notifier_template() is True + + # Test with USE_TEMPLATE set to False + mock_settings["DJANGO_LOGGING"]["LOG_EMAIL_NOTIFIER"]["USE_TEMPLATE"] = False + with patch.object(settings, "DJANGO_LOGGING", mock_settings["DJANGO_LOGGING"]): + assert use_email_notifier_template() is False + + +def test_is_auto_initialization_enabled(mock_settings): + # By default, AUTO_INITIALIZATION_ENABLE is True + assert is_auto_initialization_enabled() is True + + # Test with AUTO_INITIALIZATION_ENABLE set to False + mock_settings["DJANGO_LOGGING"]["AUTO_INITIALIZATION_ENABLE"] = False + with patch.object(settings, "DJANGO_LOGGING", mock_settings["DJANGO_LOGGING"]): + assert is_auto_initialization_enabled() is False + + +def test_is_initialization_message_enabled(mock_settings): + # By default, INITIALIZATION_MESSAGE_ENABLE is True + assert is_initialization_message_enabled() is True + + # Test with INITIALIZATION_MESSAGE_ENABLE set to False + mock_settings["DJANGO_LOGGING"]["INITIALIZATION_MESSAGE_ENABLE"] = False + with patch.object(settings, "DJANGO_LOGGING", mock_settings["DJANGO_LOGGING"]): + assert is_initialization_message_enabled() is False diff --git a/django_logging/tests/utils/test_log_and_notify.py b/django_logging/tests/utils/test_log_and_notify.py new file mode 100644 index 0000000..38b2e14 --- /dev/null +++ b/django_logging/tests/utils/test_log_and_notify.py @@ -0,0 +1,144 @@ +import logging +import pytest +from unittest.mock import patch, MagicMock +from django.conf import settings +from django_logging.utils.log_email_notifier.log_and_notify import log_and_notify_admin + + +# Helper function to mock LogConfig +def mock_log_config(email_notifier_enable=True): + return MagicMock( + log_email_notifier_enable=email_notifier_enable, + log_email_notifier_log_format=1, + ) + + +@pytest.fixture +def mock_logger(): + return MagicMock() + + +@pytest.fixture +def mock_settings(): + settings.ADMIN_EMAIL = "admin@example.com" + yield + del settings.ADMIN_EMAIL + + +# Test: Email notifier is disabled +def test_log_and_notify_email_notifier_disabled(mock_logger): + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.get_config", + return_value=mock_log_config(False), + ): + with pytest.raises(ValueError, match="Email notifier is disabled"): + log_and_notify_admin(mock_logger, logging.ERROR, "Test message") + + +# Test: Successful log and email notification to admin +def test_log_and_notify_admin_success(mock_logger, mock_settings): + log_record = logging.LogRecord( + name="test_logger", + level=logging.ERROR, + pathname="file.py", + lineno=42, + msg="Test message", + args=None, + exc_info=None, + ) + + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.get_config", + return_value=mock_log_config(True), + ): + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.inspect.currentframe" + ) as mock_frame: + mock_frame.return_value.f_back = MagicMock( + f_code=MagicMock(co_filename="file.py", co_name="function"), + f_lineno=42, + ) + + with patch.object(mock_logger, "makeRecord", return_value=log_record): + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.EmailHandler.render_template", + return_value="Formatted email body", + ): + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.send_email_async" + ) as mock_send_email: + log_and_notify_admin(mock_logger, logging.ERROR, "Test message") + + # Ensure the log was handled + mock_logger.handle.assert_called_once_with(log_record) + + # Ensure email was sent + mock_send_email.assert_called_once_with( + "New Log Record: ERROR", + "Formatted email body", + ["admin@example.com"], + ) + + +# Test: Logging failure due to invalid parameters +def test_log_and_notify_admin_logging_failure(mock_logger, mock_settings): + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.get_config", + return_value=mock_log_config(True), + ): + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.inspect.currentframe" + ) as mock_frame: + mock_frame.return_value.f_back = MagicMock( + f_code=MagicMock(co_filename="file.py", co_name="function"), + f_lineno=42, + ) + + # Simulate an error during logger.makeRecord + mock_logger.makeRecord.side_effect = TypeError("Invalid parameter") + + with pytest.raises( + ValueError, match="Failed to log message due to invalid param" + ): + log_and_notify_admin(mock_logger, logging.ERROR, "Test message") + + +# Test: Missing ADMIN_EMAIL setting +def test_log_and_notify_admin_missing_admin_email(mock_logger): + # Simulate the absence of ADMIN_EMAIL in settings + settings.ADMIN_EMAIL = None + + log_record = logging.LogRecord( + name="test_logger", + level=logging.ERROR, + pathname="file.py", + lineno=42, + msg="Test message", + args=None, + exc_info=None, + ) + + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.get_config", + return_value=mock_log_config(True), + ): + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.inspect.currentframe" + ) as mock_frame: + mock_frame.return_value.f_back = MagicMock( + f_code=MagicMock(co_filename="file.py", co_name="function"), + f_lineno=42, + ) + + with patch.object(mock_logger, "makeRecord", return_value=log_record): + with patch( + "django_logging.utils.log_email_notifier.log_and_notify.EmailHandler.render_template", + return_value="Formatted email body", + ): + with pytest.raises(ValueError) as exc_info: + log_and_notify_admin(mock_logger, logging.ERROR, "Test message") + + assert ( + str(exc_info.value) + == "'ADMIN EMAIL' not provided, please provide 'ADMIN_EMAIL' in your settings" + ) diff --git a/django_logging/tests/utils/test_set_conf.py b/django_logging/tests/utils/test_set_conf.py new file mode 100644 index 0000000..d773add --- /dev/null +++ b/django_logging/tests/utils/test_set_conf.py @@ -0,0 +1,149 @@ +from unittest.mock import patch, MagicMock +import os +from django_logging.constants.ansi_colors import AnsiColors + +from django_logging.utils.set_conf import set_config + + +@patch("django_logging.utils.set_conf.LogConfig") +@patch("django_logging.utils.set_conf.LogManager") +@patch("django_logging.utils.set_conf.is_auto_initialization_enabled") +@patch("django_logging.utils.set_conf.is_initialization_message_enabled") +@patch("logging.getLogger") +@patch("django_logging.utils.get_conf.get_config") +def test_set_config_success( + mock_get_conf, + mock_get_logger, + mock_is_initialization_message_enabled, + mock_is_auto_initialization_enabled, + mock_LogManager, + mock_LogConfig, +): + # Mock the configuration + mock_is_auto_initialization_enabled.return_value = True + mock_get_conf.return_value = ( + ["DEBUG", "INFO"], + "/path/to/logs", + {"DEBUG": 1, "INFO": 2}, + "DEBUG", + 1, + True, + "", + False, + ["INFO"], + 1, + ) + + # Mock the logger + mock_logger = MagicMock() + mock_get_logger.return_value = mock_logger + + # Call the function + set_config( + ["DEBUG", "INFO"], + "/path/to/logs", + {"DEBUG": 1, "INFO": 2}, + "DEBUG", + 1, + True, + "", + False, + ["ERROR"], + 1, + ) + + # Check that LogConfig and LogManager are instantiated + mock_LogConfig.assert_called_once_with( + ["DEBUG", "INFO"], + "/path/to/logs", + {"DEBUG": 1, "INFO": 2}, + "DEBUG", + 1, + True, + "", + False, + ["ERROR"], + 1, + ) + mock_LogManager.assert_called_once_with(mock_LogConfig.return_value) + + # Check if create_log_files and set_conf were called + mock_LogManager.return_value.create_log_files.assert_called_once() + mock_LogManager.return_value.set_conf.assert_called_once() + + # Ensure initialization message is logged if RUN_MAIN is "true" and initialization message is enabled + os.environ["RUN_MAIN"] = "true" + mock_is_initialization_message_enabled.return_value = True + set_config( + ["DEBUG", "INFO"], + "/path/to/logs", + {"DEBUG": 1, "INFO": 2}, + "DEBUG", + 1, + True, + "", + False, + ["ERROR"], + 1, + ) + mock_logger.info.assert_called_once() + + +@patch("django_logging.utils.set_conf.LogConfig") +@patch("django_logging.utils.set_conf.LogManager") +@patch("django_logging.utils.set_conf.is_auto_initialization_enabled") +def test_set_config_auto_initialization_disabled( + mock_is_auto_initialization_enabled, mock_LogManager, mock_LogConfig +): + mock_is_auto_initialization_enabled.return_value = False + + # Call the function + set_config( + ["DEBUG", "INFO"], + "/path/to/logs", + {"DEBUG": 1, "INFO": 2}, + "DEBUG", + 1, + True, + "", + False, + ["ERROR"], + 1, + ) + + # Verify that LogConfig and LogManager are not instantiated + mock_LogConfig.assert_not_called() + mock_LogManager.assert_not_called() + + +@patch("django_logging.utils.set_conf.LogConfig") +@patch("django_logging.utils.set_conf.LogManager") +@patch("django_logging.utils.set_conf.is_auto_initialization_enabled") +def test_set_config_exception_handling( + mock_is_auto_initialization_enabled, mock_LogManager, mock_LogConfig +): + mock_is_auto_initialization_enabled.return_value = True + mock_LogManager.side_effect = ValueError("Invalid configuration") + + with patch("logging.warning") as mock_warning: + set_config( + ["DEBUG", "INFO"], + "/path/to/logs", + {"DEBUG": 1, "INFO": 2}, + "DEBUG", + 1, + True, + "", + False, + ["ERROR"], + 1, + ) + mock_warning.assert_called_once_with( + "\n" + f"========================{AnsiColors.RED_BACKGROUND}DJANGO LOGGING{AnsiColors.RESET}" + f"========================\n" + f"{AnsiColors.RED}[CONFIGURATION ERROR]{AnsiColors.RESET}" + f" A configuration issue has been detected.\n" + "System checks will be run to provide more detailed information.\n" + "==============================================================\n" + ) From 2bb746d7efb854b82a3b10ea54c84052afd1e823 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 16:36:33 +0330 Subject: [PATCH 20/30] :rotating_light::white_check_mark::heavy_check_mark: test(validators): config_validators and email_settings_validator email_settings_validator: -Added tests to validate the check_email_settings function, ensuring all required email settings are present in Django settings. -Verified that appropriate error messages are generated when required email settings are missing, improving the robustness of email notification setup. config_validators: -Implemented tests for configuration validators to ensure correct validation of logging configuration options. -Tested various scenarios, including valid and invalid configurations, to confirm that the system raises errors when necessary and handles valid configurations correctly. --- django_logging/tests/__init__.py | 0 django_logging/tests/validators/__init__.py | 0 .../validators/test_config_validators.py | 160 ++++++++++++++++++ .../test_email_settings_validator.py | 93 ++++++++++ 4 files changed, 253 insertions(+) create mode 100644 django_logging/tests/__init__.py create mode 100644 django_logging/tests/validators/__init__.py create mode 100644 django_logging/tests/validators/test_config_validators.py create mode 100644 django_logging/tests/validators/test_email_settings_validator.py diff --git a/django_logging/tests/__init__.py b/django_logging/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_logging/tests/validators/__init__.py b/django_logging/tests/validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_logging/tests/validators/test_config_validators.py b/django_logging/tests/validators/test_config_validators.py new file mode 100644 index 0000000..82ad4c4 --- /dev/null +++ b/django_logging/tests/validators/test_config_validators.py @@ -0,0 +1,160 @@ +from unittest.mock import patch +from django_logging.validators.config_validators import ( + validate_directory, + validate_log_levels, + validate_format_string, + validate_format_option, + validate_boolean_setting, + validate_date_format, + validate_email_notifier, +) + + +def test_validate_directory_success(): + with patch("os.path.exists") as mock_exists, patch("os.mkdir") as mock_mkdir: + mock_exists.return_value = False + errors = validate_directory("new_dir", "test_directory") + assert not errors + mock_mkdir.assert_called_once() + + +def test_validate_directory_invalid_path(): + with patch("os.path.exists") as mock_exists: + mock_exists.return_value = False + errors = validate_directory(None, "test_directory") + assert len(errors) == 1 + assert errors[0].id == "django_logging.E001_test_directory" + + errors = validate_directory("/path\to/file", "test_directory") + assert len(errors) == 1 + assert errors[0].id == "django_logging.E002_test_directory" + + +def test_validate_directory_is_file(): + file_path = "path/to/file.txt" + + with patch("os.path.isdir") as mock_isdir, patch( + "os.path.isfile" + ) as mock_isfile, patch("os.path.exists") as mock_exists: + # Simulate that the path is a file, not a directory + mock_isdir.return_value = False + mock_isfile.return_value = True + mock_exists.return_value = True + + errors = validate_directory(file_path, "test_directory") + + assert len(errors) == 1 + assert errors[0].id == "django_logging.E003_test_directory" + assert ( + "The path specified in test_directory is not a directory." in errors[0].msg + ) + assert "Ensure test_directory points to a valid directory." in errors[0].hint + + +def test_validate_log_levels_success(): + valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + errors = validate_log_levels(["DEBUG", "INFO"], "log_levels", valid_levels) + assert not errors + + +def test_validate_log_levels_invalid_type(): + valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + errors = validate_log_levels("DEBUG, INFO", "log_levels", valid_levels) + assert len(errors) == 1 + assert errors[0].id == "django_logging.E004_log_levels" + + errors = validate_log_levels([], "log_levels", valid_levels) + assert len(errors) == 1 + assert errors[0].id == "django_logging.E005_log_levels" + + +def test_validate_format_string_success(): + format_str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + errors = validate_format_string(format_str, "log_format") + assert not errors + + +def test_validate_format_string_invalid(): + format_str = "%(invalid)s" + errors = validate_format_string(format_str, "log_format") + assert len(errors) == 1 + assert errors[0].id == "django_logging.E011_log_format" + + format_str = tuple() # invalid type + errors = validate_format_string(format_str, "log_format") + assert len(errors) == 1 + assert errors[0].id == "django_logging.E008_log_format" + + format_str = "" + errors = validate_format_string(format_str, "log_format") + assert len(errors) == 1 + assert errors[0].id == "django_logging.E009_log_format" + + +def test_validate_format_option_integer_success(): + format_option = 1 + errors = validate_format_option(format_option, "log_format_option") + assert not errors + + +def test_validate_format_option_failure(): + format_option = 15 + errors = validate_format_option(format_option, "log_format_option") + assert len(errors) == 1 + assert errors[0].id == "django_logging.E012_log_format_option" + + format_option = 1.5 + errors = validate_format_option(format_option, "log_format_option") + assert len(errors) == 1 + assert errors[0].id == "django_logging.E013_log_format_option" + + +def test_validate_format_option_string_success(): + format_option = "%(asctime)s - %(message)s" + errors = validate_format_option(format_option, "log_format_option") + assert not errors + + +def test_validate_boolean_setting_success(): + errors = validate_boolean_setting(True, "boolean_setting") + assert not errors + + +def test_validate_date_format_success(): + date_format = "%Y-%m-%d" + errors = validate_date_format(date_format, "date_format") + assert not errors + + +def test_validate_date_format_invalid_format(): + date_format = 1 # invalid type + errors = validate_date_format(date_format, "date_format") + assert len(errors) == 1 + assert errors[0].id == "django_logging.E015_date_format" + + date_format = "%invalid" + errors = validate_date_format(date_format, "date_format") + assert len(errors) == 1 + assert errors[0].id == "django_logging.E016_date_format" + + +def test_validate_email_notifier_success(): + notifier_config = { + "ENABLE": True, + "LOG_FORMAT": "%(asctime)s - %(message)s", + "NOTIFY_ERROR": True, + } + errors = validate_email_notifier(notifier_config) + assert not errors + + +def test_validate_email_notifier_invalid_type(): + notifier_config = ["ENABLE", "LOG_FORMAT"] + errors = validate_email_notifier(notifier_config) + assert len(errors) == 1 + assert errors[0].id == "django_logging.E017_LOG_EMAIL_NOTIFIER" + + notifier_config = {"ENABLE": "true", "LOG_FORMAT": "%(asctime)s - %(message)s"} + errors = validate_email_notifier(notifier_config) + assert len(errors) == 1 + assert errors[0].id == "django_logging.E018_LOG_EMAIL_NOTIFIER['ENABLE']" diff --git a/django_logging/tests/validators/test_email_settings_validator.py b/django_logging/tests/validators/test_email_settings_validator.py new file mode 100644 index 0000000..bf856ef --- /dev/null +++ b/django_logging/tests/validators/test_email_settings_validator.py @@ -0,0 +1,93 @@ +import pytest +from unittest.mock import patch +from django.conf import settings + +from django_logging.validators.email_settings_validator import check_email_settings + + +# Mock required email settings for testing +@pytest.fixture +def mock_email_settings(): + """Fixture to mock Django email settings.""" + return { + "EMAIL_HOST": "smtp.example.com", + "EMAIL_PORT": 587, + "EMAIL_HOST_USER": "user@example.com", + "EMAIL_HOST_PASSWORD": "password", + "DEFAULT_FROM_EMAIL": "from@example.com", + "ADMIN_EMAIL": "to@example.com", + } + + +def test_check_email_settings_all_present(mock_email_settings): + """Test when all required email settings are present.""" + with patch.object(settings, "EMAIL_HOST", mock_email_settings["EMAIL_HOST"]): + with patch.object(settings, "EMAIL_PORT", mock_email_settings["EMAIL_PORT"]): + with patch.object( + settings, "EMAIL_HOST_USER", mock_email_settings["EMAIL_HOST_USER"] + ): + with patch.object( + settings, + "EMAIL_HOST_PASSWORD", + mock_email_settings["EMAIL_HOST_PASSWORD"], + ): + with patch.object( + settings, + "DEFAULT_FROM_EMAIL", + mock_email_settings["DEFAULT_FROM_EMAIL"], + ): + with patch.object( + settings, + "ADMIN_EMAIL", + mock_email_settings["ADMIN_EMAIL"], + ): + errors = check_email_settings() + assert not errors # No errors should be present + + +def test_check_email_settings_missing_some(mock_email_settings): + """Test when some required email settings are missing.""" + with patch.object(settings, "EMAIL_HOST", mock_email_settings["EMAIL_HOST"]): + with patch.object(settings, "EMAIL_PORT", mock_email_settings["EMAIL_PORT"]): + with patch.object( + settings, "EMAIL_HOST_USER", None + ): # Simulate missing setting + with patch.object( + settings, + "EMAIL_HOST_PASSWORD", + mock_email_settings["EMAIL_HOST_PASSWORD"], + ): + with patch.object( + settings, + "DEFAULT_FROM_EMAIL", + mock_email_settings["DEFAULT_FROM_EMAIL"], + ): + with patch.object( + settings, + "ADMIN_EMAIL", + mock_email_settings["ADMIN_EMAIL"], + ): + errors = check_email_settings() + assert len(errors) == 1 + assert ( + errors[0].msg + == "Missing required email settings: EMAIL_HOST_USER" + ) + assert errors[0].id == "django_logging.E021" + + +def test_check_email_settings_all_missing(): + """Test when all required email settings are missing.""" + with patch.object(settings, "EMAIL_HOST", None): + with patch.object(settings, "EMAIL_PORT", None): + with patch.object(settings, "EMAIL_HOST_USER", None): + with patch.object(settings, "EMAIL_HOST_PASSWORD", None): + with patch.object(settings, "DEFAULT_FROM_EMAIL", None): + with patch.object(settings, "ADMIN_EMAIL", None): + errors = check_email_settings() + assert len(errors) == 1 + assert errors[0].msg.startswith( + 'Missing required email settings: EMAIL_HOST, EMAIL_PORT, EMAIL_HOST_USER,' + ' EMAIL_HOST_PASSWORD, DEFAULT_FROM_EMAIL, ADMIN_EMAIL' + ) + assert errors[0].id == "django_logging.E021" From ab5e95490bf17ff738fe795e15558102b6c01998 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 19:24:03 +0330 Subject: [PATCH 21/30] :books::heavy_check_mark: docs(tests): add detailed well styled docstring for all tests all tests passed with coverage 100% Closes #23 --- .../tests/commands/test_send_logs.py | 110 +++++++-- .../tests/filters/test_log_level_filter.py | 40 +++- .../formatters/test_colored_formatter.py | 82 ++++++- .../tests/handlers/test_email_handler.py | 111 ++++++++- .../middleware/test_request_middleware.py | 106 +++++++++ django_logging/tests/settings/test_checks.py | 136 ++++++++++- django_logging/tests/settings/test_conf.py | 111 +++++++-- .../tests/utils/test_context_manager.py | 82 ++++++- .../tests/utils/test_email_notifier.py | 88 +++++-- django_logging/tests/utils/test_get_conf.py | 73 +++++- .../tests/utils/test_log_and_notify.py | 107 ++++++++- django_logging/tests/utils/test_set_conf.py | 55 +++++ .../validators/test_config_validators.py | 218 ++++++++++++++++++ .../test_email_settings_validator.py | 94 ++++++-- 14 files changed, 1310 insertions(+), 103 deletions(-) diff --git a/django_logging/tests/commands/test_send_logs.py b/django_logging/tests/commands/test_send_logs.py index 51336b9..b6d0ac3 100644 --- a/django_logging/tests/commands/test_send_logs.py +++ b/django_logging/tests/commands/test_send_logs.py @@ -10,6 +10,9 @@ class SendLogsCommandTests(TestCase): + """ + Test suite for the `send_logs` management command in the django_logging package. + """ @patch( "django_logging.management.commands.send_logs.Command.validate_email_settings" @@ -19,25 +22,35 @@ class SendLogsCommandTests(TestCase): def test_handle_success( self, mock_email_message, mock_make_archive, mock_validate_email_settings ): - # Setup + """ + Test that the `send_logs` command successfully creates an archive of the logs + and sends an email when executed with valid settings. + + Args: + ---- + mock_email_message: Mock for the `EmailMessage` class used to send the email. + mock_make_archive: Mock for the `shutil.make_archive` function that creates the log archive. + mock_validate_email_settings: Mock for the `validate_email_settings` method in the command. + + Asserts: + ------- + - The `validate_email_settings` method is called exactly once. + - The `shutil.make_archive` function is called with the correct arguments. + - The `EmailMessage` is instantiated and sent. + """ temp_log_dir = tempfile.mkdtemp() temp_file = tempfile.NamedTemporaryFile(delete=False) temp_file.close() mock_make_archive.return_value = temp_file.name - # Mock settings with self.settings(DJANGO_LOGGING={"LOG_DIR": temp_log_dir}): - # Execute command call_command("send_logs", "test@example.com") - # Assert mock_validate_email_settings.assert_called_once() mock_make_archive.assert_called_once_with(ANY, "zip", temp_log_dir) mock_email_message.assert_called_once() - # Cleanup shutil.rmtree(temp_log_dir) - ( os.remove(temp_file.name + ".zip") if os.path.exists(temp_file.name + ".zip") @@ -51,46 +64,63 @@ def test_handle_success( def test_handle_email_send_failure( self, mock_email_send, mock_validate_email_settings ): - # Setup + """ + Test that the `send_logs` command handles email sending failures correctly + and logs an appropriate error message. + + Args: + ---- + mock_email_send: Mock for the `EmailMessage.send` method, simulating a failure. + mock_validate_email_settings: Mock for the `validate_email_settings` method in the command. + + Asserts: + ------- + - An error message is logged when the email sending fails. + """ temp_log_dir = tempfile.mkdtemp() mock_email_send.side_effect = Exception("Email send failed") - # Mock settings with self.settings(DJANGO_LOGGING={"LOG_DIR": temp_log_dir}): with self.assertLogs( "django_logging.management.commands.send_logs", level="ERROR" ) as cm: call_command("send_logs", "test@example.com") - # Assert self.assertIn( "ERROR:django_logging.management.commands.send_logs:Failed to send logs: Email send failed", cm.output, ) - # Cleanup shutil.rmtree(temp_log_dir) @patch( "django_logging.management.commands.send_logs.Command.validate_email_settings" ) def test_handle_missing_log_dir(self, mock_validate_email_settings): - # Mock settings with a non-existent log directory + """ + Test that the `send_logs` command logs an error when the specified log directory does not exist + and skips the email validation step. + + Args: + ---- + mock_validate_email_settings: Mock for the `validate_email_settings` method in the command. + + Asserts: + ------- + - An error message is logged if the log directory does not exist. + - The `validate_email_settings` method is not called. + """ non_existent_dir = "/non/existent/directory" with self.settings(DJANGO_LOGGING={"LOG_DIR": non_existent_dir}): with self.assertLogs( "django_logging.management.commands.send_logs", level="ERROR" ) as cm: - # Call the command and check that no exception is raised but logs are captured call_command("send_logs", "test@example.com") - # Check if the correct error message is logged - self.assertIn( - f'ERROR:django_logging.management.commands.send_logs:Log directory "{non_existent_dir}" does not exist.', - cm.output, - ) - - # Check that validate_email_settings was not called + self.assertIn( + f'ERROR:django_logging.management.commands.send_logs:Log directory "{non_existent_dir}" does not exist.', + cm.output, + ) mock_validate_email_settings.assert_not_called() @patch( @@ -98,6 +128,18 @@ def test_handle_missing_log_dir(self, mock_validate_email_settings): return_value=None, ) def test_validate_email_settings_success(self, mock_check_email_settings): + """ + Test that the `validate_email_settings` method successfully validates the email settings + without raising any exceptions. + + Args: + ---- + mock_check_email_settings: Mock for the `check_email_settings` function, simulating a successful check. + + Asserts: + ------- + - The `check_email_settings` function is called exactly once. + """ call_command("send_logs", "test@example.com") mock_check_email_settings.assert_called_once() @@ -106,6 +148,18 @@ def test_validate_email_settings_success(self, mock_check_email_settings): return_value="Missing config", ) def test_validate_email_settings_failure(self, mock_check_email_settings): + """ + Test that the `validate_email_settings` method raises an `ImproperlyConfigured` exception + when the email settings are invalid. + + Args: + ---- + mock_check_email_settings: Mock for the `check_email_settings` function, simulating a failure. + + Asserts: + ------- + - An `ImproperlyConfigured` exception is raised when email settings are invalid. + """ with self.assertRaises(ImproperlyConfigured): call_command("send_logs", "test@example.com") @@ -114,19 +168,27 @@ def test_validate_email_settings_failure(self, mock_check_email_settings): ) @patch("django_logging.management.commands.send_logs.shutil.make_archive") def test_cleanup_on_failure(self, mock_make_archive, mock_validate_email_settings): - # Setup + """ + Test that the `send_logs` command cleans up any partially created files when an error occurs + during the log archiving process. + + Args: + ---- + mock_make_archive: Mock for the `shutil.make_archive` function, simulating a failure. + mock_validate_email_settings: Mock for the `validate_email_settings` method in the command. + + Asserts: + ------- + - The zip file is not left behind if an error occurs during the archiving process. + """ temp_log_dir = tempfile.mkdtemp() temp_file = tempfile.NamedTemporaryFile(delete=False) temp_file.close() mock_make_archive.side_effect = Exception("Archive failed") - # Mock settings with self.settings(DJANGO_LOGGING={"LOG_DIR": temp_log_dir}): with self.assertRaises(Exception): call_command("send_logs", "test@example.com") - # Assert self.assertFalse(os.path.exists(temp_file.name + ".zip")) - - # Cleanup shutil.rmtree(temp_log_dir) diff --git a/django_logging/tests/filters/test_log_level_filter.py b/django_logging/tests/filters/test_log_level_filter.py index aa1839e..9a0dcfa 100644 --- a/django_logging/tests/filters/test_log_level_filter.py +++ b/django_logging/tests/filters/test_log_level_filter.py @@ -5,7 +5,13 @@ @pytest.fixture def log_record(): - """Fixture to create a dummy log record.""" + """ + Fixture to create a dummy log record for testing. + + Returns: + ------- + logging.LogRecord: A dummy log record with predefined attributes. + """ return logging.LogRecord( name="test", level=logging.DEBUG, @@ -18,7 +24,13 @@ def log_record(): def test_logging_level_filter_initialization(): - """Test that LoggingLevelFilter initializes with the correct logging level.""" + """ + Test that the `LoggingLevelFilter` initializes with the correct logging level. + + Asserts: + ------- + - The `logging_level` attribute of the filter instance is set to the provided logging level. + """ filter_instance = LoggingLevelFilter(logging.INFO) assert filter_instance.logging_level == logging.INFO, ( f"Expected logging_level to be {logging.INFO}, " @@ -27,7 +39,17 @@ def test_logging_level_filter_initialization(): def test_logging_level_filter_passes_matching_level(log_record): - """Test that the filter passes log records with the matching level.""" + """ + Test that the `LoggingLevelFilter` passes log records with the matching logging level. + + Args: + ---- + log_record (logging.LogRecord): A dummy log record created by the fixture. + + Asserts: + ------- + - The filter method returns True when the log record's level matches the filter's level. + """ log_record.levelno = logging.DEBUG filter_instance = LoggingLevelFilter(logging.DEBUG) @@ -37,7 +59,17 @@ def test_logging_level_filter_passes_matching_level(log_record): def test_logging_level_filter_blocks_non_matching_level(log_record): - """Test that the filter blocks log records with a non-matching level.""" + """ + Test that the `LoggingLevelFilter` blocks log records with a non-matching logging level. + + Args: + ---- + log_record (logging.LogRecord): A dummy log record created by the fixture. + + Asserts: + ------- + - The filter method returns False when the log record's level does not match the filter's level. + """ log_record.levelno = logging.WARNING filter_instance = LoggingLevelFilter(logging.ERROR) diff --git a/django_logging/tests/formatters/test_colored_formatter.py b/django_logging/tests/formatters/test_colored_formatter.py index 5e8d47a..a5df5bc 100644 --- a/django_logging/tests/formatters/test_colored_formatter.py +++ b/django_logging/tests/formatters/test_colored_formatter.py @@ -6,7 +6,14 @@ @pytest.fixture def log_record(): - """Fixture to create a dummy log record.""" + """ + Fixture to create a dummy log record for testing. + + Returns: + ------- + logging.LogRecord + A dummy log record with predefined attributes. + """ return logging.LogRecord( name="test", level=logging.DEBUG, @@ -20,13 +27,18 @@ def log_record(): @pytest.fixture def formatter(): - """Fixture to create a ColoredFormatter instance with a specific format.""" + """ + Fixture to create a `ColorizedFormatter` instance with a specific format. + + Returns: + ------- + ColorizedFormatter + An instance of `ColorizedFormatter` with a predefined format. + """ return ColorizedFormatter(fmt="%(levelname)s: %(message)s") -@patch( - "django_logging.formatters.colored_formatter.colorize_log_format", autospec=True -) +@patch("django_logging.formatters.colored_formatter.colorize_log_format", autospec=True) @patch( "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", side_effect=lambda fmt: fmt, @@ -34,7 +46,27 @@ def formatter(): def test_format_applies_colorization( mock_remove_ansi, mock_colorize, formatter, log_record ): - """Test that the format method applies colorization.""" + """ + Test that the `format` method of `ColorizedFormatter` applies colorization. + + This test verifies that the `format` method calls the `colorize_log_format` + function to apply colorization based on the log level. + + Parameters: + ---------- + mock_remove_ansi : MagicMock + Mock for `remove_ansi_escape_sequences`. + mock_colorize : MagicMock + Mock for `colorize_log_format`. + formatter : ColorizedFormatter + The formatter instance being tested. + log_record : logging.LogRecord + The dummy log record created by the fixture. + + Asserts: + ------- + - The `colorize_log_format` function is called once with the correct arguments. + """ # Mock the colorize_log_format to return a predictable format mock_colorize.return_value = "%(levelname)s: %(message)s" @@ -51,7 +83,25 @@ def test_format_applies_colorization( side_effect=lambda fmt: fmt, ) def test_format_resets_to_original_format(mock_remove_ansi, formatter, log_record): - """Test that the format method resets the format string after formatting.""" + """ + Test that the `format` method resets the format string to its original state after formatting. + + This test ensures that the formatter's internal format string is not permanently modified + by the colorization process and is reset to its original value after each log record is formatted. + + Parameters: + ---------- + mock_remove_ansi : MagicMock + Mock for `remove_ansi_escape_sequences`. + formatter : ColorizedFormatter + The formatter instance being tested. + log_record : logging.LogRecord + The dummy log record created by the fixture. + + Asserts: + ------- + - The formatter's internal format string (`_style._fmt`) matches the original format after formatting. + """ original_format = formatter._style._fmt formatter.format(log_record) assert ( @@ -64,7 +114,23 @@ def test_format_resets_to_original_format(mock_remove_ansi, formatter, log_recor side_effect=lambda fmt: fmt, ) def test_format_returns_formatted_output(formatter, log_record): - """Test that the format method returns the correctly formatted output.""" + """ + Test that the `format` method returns the correctly formatted log output. + + This test verifies that the formatted output matches the expected structure, + including the log level name and the log message. + + Parameters: + ---------- + formatter : ColorizedFormatter + The formatter instance being tested. + log_record : logging.LogRecord + The dummy log record created by the fixture. + + Asserts: + ------- + - The formatted output starts with the expected log level and message. + """ expected_output = f"{logging.getLevelName(log_record.levelno)}: {log_record.msg}" formatted_output = formatter.format(log_record) diff --git a/django_logging/tests/handlers/test_email_handler.py b/django_logging/tests/handlers/test_email_handler.py index ec32f38..719aa76 100644 --- a/django_logging/tests/handlers/test_email_handler.py +++ b/django_logging/tests/handlers/test_email_handler.py @@ -9,7 +9,14 @@ @pytest.fixture def log_record(): - """Fixture to create a dummy log record.""" + """ + Fixture to create a dummy log record. + + Returns: + ------- + logging.LogRecord + A dummy log record with predefined attributes for testing. + """ return logging.LogRecord( name="test", level=logging.ERROR, @@ -23,7 +30,14 @@ def log_record(): @pytest.fixture def email_handler(): - """Fixture to create an EmailHandler instance.""" + """ + Fixture to create an EmailHandler instance. + + Returns: + ------- + EmailHandler + An instance of the EmailHandler class. + """ return EmailHandler() @@ -36,7 +50,30 @@ def email_handler(): def test_emit_with_html_template( mock_use_template, mock_render_template, mock_send_email, email_handler, log_record ): - """Test the emit method with HTML template.""" + """ + Test the emit method when HTML templates are used. + + This test verifies that the EmailHandler's `emit` method correctly renders an HTML + template and sends an email when `use_email_notifier_template` is enabled. + + Args: + ---- + mock_use_template : MagicMock + Mock for the `use_email_notifier_template` function. + mock_render_template : MagicMock + Mock for the `render_template` method. + mock_send_email : MagicMock + Mock for the `send_email_async` function. + email_handler : EmailHandler + The EmailHandler instance being tested. + log_record : logging.LogRecord + The log record fixture used for testing. + + Asserts: + ------- + - `render_template` is called once with the correct arguments. + - `send_email_async` is called once with the expected email subject, HTML content, and recipients. + """ mock_render_template.return_value = "Formatted Log" email_handler.emit(log_record) @@ -53,7 +90,25 @@ def test_emit_with_html_template( return_value=False, ) def test_emit_without_html_template(mock_use_template, mock_send_email, log_record): - """Test the emit method without HTML template.""" + """ + Test the emit method when HTML templates are not used. + + This test checks that the EmailHandler's `emit` method correctly sends a plain text + email when `use_email_notifier_template` is disabled. + + Args: + ---- + mock_use_template : MagicMock + Mock for the `use_email_notifier_template` function. + mock_send_email : MagicMock + Mock for the `send_email_async` function. + log_record : logging.LogRecord + The log record fixture used for testing. + + Asserts: + ------- + - `send_email_async` is called once with the expected email subject, plain text content, and recipients. + """ email_handler = EmailHandler() email_handler.emit(log_record) @@ -70,7 +125,27 @@ def test_emit_without_html_template(mock_use_template, mock_send_email, log_reco def test_emit_handles_exception( mock_send_email, mock_handle_error, email_handler, log_record ): - """Test that emit handles exceptions properly.""" + """ + Test that the emit method handles exceptions during email sending. + + This test ensures that when an exception occurs during the email sending process, + the `handleError` method is called to manage the error. + + Args: + ---- + mock_send_email : MagicMock + Mock for the `send_email_async` function. + mock_handle_error : MagicMock + Mock for the `handleError` method. + email_handler : EmailHandler + The EmailHandler instance being tested. + log_record : logging.LogRecord + The log record fixture used for testing. + + Asserts: + ------- + - `handleError` is called once with the log record when an exception occurs. + """ email_handler.emit(log_record) mock_handle_error.assert_called_once_with(log_record) @@ -86,7 +161,31 @@ def test_emit_handles_exception( ) @patch("django_logging.handlers.email_handler.engines") def test_render_template(mock_engines, mock_get_user_agent, mock_get_ip_address): - """Test the render_template method.""" + """ + Test the render_template method of EmailHandler. + + This test verifies that the `render_template` method correctly renders the HTML + template with the provided log message and request details. + + Args: + ---- + mock_engines : MagicMock + Mock for the Django template engines. + mock_get_user_agent : MagicMock + Mock for the `get_user_agent` method of the middleware. + mock_get_ip_address : MagicMock + Mock for the `get_ip_address` method of the middleware. + + Asserts: + ------- + - The correct template is retrieved and rendered with the expected context data. + - The rendered HTML output matches the expected formatted log. + + Returns: + ------- + str + The rendered HTML output for the log message. + """ mock_template = MagicMock() mock_template.render.return_value = "Formatted Log" mock_django_engine = MagicMock() diff --git a/django_logging/tests/middleware/test_request_middleware.py b/django_logging/tests/middleware/test_request_middleware.py index f3ea5e1..4e5d26c 100644 --- a/django_logging/tests/middleware/test_request_middleware.py +++ b/django_logging/tests/middleware/test_request_middleware.py @@ -11,11 +11,28 @@ @pytest.fixture def request_factory(): + """ + Fixture to create a RequestFactory instance for generating request objects. + + Returns: + ------- + RequestFactory + An instance of RequestFactory for creating mock requests. + """ return RequestFactory() @pytest.fixture def get_response(): + """ + Fixture to create a mock get_response function. + + Returns: + ------- + function + A function that returns an HttpResponse with a dummy response. + """ + def _get_response(request): return HttpResponse("Test Response") @@ -24,10 +41,45 @@ def _get_response(request): @pytest.fixture def middleware(get_response): + """ + Fixture to create an instance of RequestLogMiddleware. + + Args: + ---- + get_response : function + A function that returns an HttpResponse for a given request. + + Returns: + ------- + RequestLogMiddleware + An instance of RequestLogMiddleware with the provided get_response function. + """ return RequestLogMiddleware(get_response) def test_authenticated_user_logging(middleware, request_factory, caplog): + """ + Test logging of requests for authenticated users. + + This test verifies that when an authenticated user makes a request, + the relevant request information, including the username, is logged. + + Args: + ---- + middleware : RequestLogMiddleware + The middleware instance used to process the request. + request_factory : RequestFactory + A factory for creating mock HTTP requests. + caplog : pytest.LogCaptureFixture + A fixture for capturing log messages. + + Asserts: + ------- + - "Request Info" is present in the logs. + - The requested path is logged. + - The username of the authenticated user is logged. + - The request object has `ip_address` and `browser_type` attributes. + """ request = request_factory.get("/test-path") UserModel = get_user_model() @@ -48,6 +100,26 @@ def test_authenticated_user_logging(middleware, request_factory, caplog): def test_anonymous_user_logging(middleware, request_factory, caplog): + """ + Test logging of requests for anonymous users. + + This test ensures that when an anonymous user makes a request, + the relevant request information, including the identification as "Anonymous", is logged. + + Args: + ---- + middleware : RequestLogMiddleware + The middleware instance used to process the request. + request_factory : RequestFactory + A factory for creating mock HTTP requests. + caplog : pytest.LogCaptureFixture + A fixture for capturing log messages. + + Asserts: + ------- + - "Request Info" is present in the logs. + - The request is identified as coming from an "Anonymous" user. + """ request = request_factory.get("/test-path") request.user = AnonymousUser() @@ -59,6 +131,23 @@ def test_anonymous_user_logging(middleware, request_factory, caplog): def test_ip_address_extraction(middleware, request_factory): + """ + Test extraction of the client's IP address from the request. + + This test verifies that the middleware correctly extracts the IP address + from the `HTTP_X_FORWARDED_FOR` header in the request. + + Args: + ---- + middleware : RequestLogMiddleware + The middleware instance used to process the request. + request_factory : RequestFactory + A factory for creating mock HTTP requests. + + Asserts: + ------- + - The `ip_address` attribute of the request is correctly set to the value in the `HTTP_X_FORWARDED_FOR` header. + """ request = request_factory.get("/test-path", HTTP_X_FORWARDED_FOR="192.168.1.1") middleware(request) @@ -67,6 +156,23 @@ def test_ip_address_extraction(middleware, request_factory): def test_user_agent_extraction(middleware, request_factory): + """ + Test extraction of the client's user agent from the request. + + This test verifies that the middleware correctly extracts the user agent + from the `HTTP_USER_AGENT` header in the request. + + Args: + ---- + middleware : RequestLogMiddleware + The middleware instance used to process the request. + request_factory : RequestFactory + A factory for creating mock HTTP requests. + + Asserts: + ------- + - The `browser_type` attribute of the request is correctly set to the value in the `HTTP_USER_AGENT` header. + """ request = request_factory.get("/test-path", HTTP_USER_AGENT="Mozilla/5.0") middleware(request) diff --git a/django_logging/tests/settings/test_checks.py b/django_logging/tests/settings/test_checks.py index 86a36a7..f278ada 100644 --- a/django_logging/tests/settings/test_checks.py +++ b/django_logging/tests/settings/test_checks.py @@ -8,13 +8,31 @@ @pytest.fixture def reset_settings(): - """Fixture to reset Django settings after each test.""" + """ + Fixture to reset Django settings after each test. + + This ensures that any modifications to the settings during a test are reverted after the test completes. + + Yields: + ------- + None + """ original_settings = settings.DJANGO_LOGGING yield settings.DJANGO_LOGGING = original_settings def test_valid_logging_settings(reset_settings): + """ + Test that valid logging settings do not produce any errors. + + This test verifies that when all logging settings are properly configured, + the `check_logging_settings` function does not return any errors. + + Asserts: + ------- + - No errors are returned by `check_logging_settings`. + """ settings.DJANGO_LOGGING = { "LOG_DIR": "logs", "LOG_FILE_LEVELS": ["DEBUG", "INFO", "ERROR"], @@ -38,6 +56,16 @@ def test_valid_logging_settings(reset_settings): def test_invalid_log_dir(reset_settings): + """ + Test invalid LOG_DIR setting. + + This test checks that when `LOG_DIR` is set to an invalid type (e.g., an integer), + the appropriate error is returned. + + Asserts: + ------- + - An error with the ID `django_logging.E001_LOG_DIR` is returned. + """ settings.DJANGO_LOGGING = { "LOG_DIR": 1, } @@ -46,6 +74,16 @@ def test_invalid_log_dir(reset_settings): def test_invalid_log_file_levels(reset_settings): + """ + Test invalid LOG_FILE_LEVELS setting. + + This test checks that when `LOG_FILE_LEVELS` contains invalid log levels, + the appropriate error is returned. + + Asserts: + ------- + - An error with the ID `django_logging.E007_LOG_FILE_LEVELS` is returned. + """ settings.DJANGO_LOGGING = { "LOG_FILE_LEVELS": ["invalid"], } @@ -54,6 +92,17 @@ def test_invalid_log_file_levels(reset_settings): def test_invalid_log_file_formats(reset_settings): + """ + Test invalid LOG_FILE_FORMATS setting. + + This test checks for various invalid configurations in `LOG_FILE_FORMATS` + and ensures that the appropriate errors are returned. + + Asserts: + ------- + - Errors with the IDs `django_logging.E011_LOG_FILE_FORMATS['DEBUG']` and `django_logging.E019_LOG_FILE_FORMATS` are returned for invalid formats. + - An error with the ID `django_logging.E020_LOG_FILE_FORMATS` is returned for invalid type. + """ settings.DJANGO_LOGGING = { "LOG_FILE_FORMATS": { "DEBUG": "%(levelname)s: %(invalid)s", @@ -72,6 +121,16 @@ def test_invalid_log_file_formats(reset_settings): def test_invalid_log_console_format(reset_settings): + """ + Test invalid LOG_CONSOLE_FORMAT setting. + + This test checks that when `LOG_CONSOLE_FORMAT` is set to an invalid value, + the appropriate error is returned. + + Asserts: + ------- + - An error with the ID `django_logging.E010_LOG_CONSOLE_FORMAT` is returned. + """ settings.DJANGO_LOGGING = { "LOG_CONSOLE_FORMAT": "invalid", } @@ -80,6 +139,16 @@ def test_invalid_log_console_format(reset_settings): def test_invalid_log_console_level(reset_settings): + """ + Test invalid LOG_CONSOLE_LEVEL setting. + + This test checks that when `LOG_CONSOLE_LEVEL` is set to an invalid value, + the appropriate error is returned. + + Asserts: + ------- + - An error with the ID `django_logging.E006_LOG_CONSOLE_LEVEL` is returned. + """ settings.DJANGO_LOGGING = { "LOG_CONSOLE_LEVEL": 10, } @@ -88,6 +157,16 @@ def test_invalid_log_console_level(reset_settings): def test_invalid_log_console_colorize(reset_settings): + """ + Test invalid LOG_CONSOLE_COLORIZE setting. + + This test checks that when `LOG_CONSOLE_COLORIZE` is set to an invalid value, + the appropriate error is returned. + + Asserts: + ------- + - An error with the ID `django_logging.E014_LOG_CONSOLE_COLORIZE` is returned. + """ settings.DJANGO_LOGGING = { "LOG_CONSOLE_COLORIZE": "not_a_boolean", } @@ -98,6 +177,16 @@ def test_invalid_log_console_colorize(reset_settings): def test_invalid_log_date_format(reset_settings): + """ + Test invalid LOG_DATE_FORMAT setting. + + This test checks that when `LOG_DATE_FORMAT` is set to an invalid value, + the appropriate error is returned. + + Asserts: + ------- + - An error with the ID `django_logging.E016_LOG_DATE_FORMAT` is returned. + """ settings.DJANGO_LOGGING = { "LOG_DATE_FORMAT": "%invalid_format", } @@ -106,6 +195,16 @@ def test_invalid_log_date_format(reset_settings): def test_invalid_auto_initialization_enable(reset_settings): + """ + Test invalid AUTO_INITIALIZATION_ENABLE setting. + + This test checks that when `AUTO_INITIALIZATION_ENABLE` is set to an invalid value, + the appropriate error is returned. + + Asserts: + ------- + - An error with the ID `django_logging.E014_AUTO_INITIALIZATION_ENABLE` is returned. + """ settings.DJANGO_LOGGING = { "AUTO_INITIALIZATION_ENABLE": "not_a_boolean", } @@ -116,6 +215,16 @@ def test_invalid_auto_initialization_enable(reset_settings): def test_invalid_initialization_message_enable(reset_settings): + """ + Test invalid INITIALIZATION_MESSAGE_ENABLE setting. + + This test checks that when `INITIALIZATION_MESSAGE_ENABLE` is set to an invalid value, + the appropriate error is returned. + + Asserts: + ------- + - An error with the ID `django_logging.E014_INITIALIZATION_MESSAGE_ENABLE` is returned. + """ settings.DJANGO_LOGGING = { "INITIALIZATION_MESSAGE_ENABLE": "not_a_boolean", } @@ -127,6 +236,16 @@ def test_invalid_initialization_message_enable(reset_settings): def test_invalid_log_email_notifier(reset_settings): + """ + Test invalid LOG_EMAIL_NOTIFIER setting. + + This test checks that when `LOG_EMAIL_NOTIFIER['ENABLE']` is set to an invalid value, + the appropriate error is returned. + + Asserts: + ------- + - An error with the ID `django_logging.E018_LOG_EMAIL_NOTIFIER['ENABLE']` is returned. + """ settings.DJANGO_LOGGING = { "LOG_EMAIL_NOTIFIER": { "ENABLE": "not_a_boolean", @@ -140,12 +259,25 @@ def test_invalid_log_email_notifier(reset_settings): def test_missing_email_settings(reset_settings): + """ + Test missing email settings when LOG_EMAIL_NOTIFIER is enabled. + + This test checks that when `LOG_EMAIL_NOTIFIER['ENABLE']` is set to True, + but required email settings are missing, the appropriate error is returned. + + Mocks: + ------ + - Mock the `check_email_settings` function to simulate missing email settings. + + Asserts: + ------- + - An error with the ID `django_logging.E010_EMAIL_SETTINGS` is returned. + """ settings.DJANGO_LOGGING = { "LOG_EMAIL_NOTIFIER": { "ENABLE": True, }, } - # Mocking check_email_settings to return errors with patch("django_logging.settings.checks.check_email_settings") as mock_check: mock_check.return_value = [ Error("EMAIL_BACKEND not set.", id="django_logging.E010_EMAIL_SETTINGS") diff --git a/django_logging/tests/settings/test_conf.py b/django_logging/tests/settings/test_conf.py index 0d59ad4..75b6046 100644 --- a/django_logging/tests/settings/test_conf.py +++ b/django_logging/tests/settings/test_conf.py @@ -8,6 +8,17 @@ @pytest.fixture def log_config(): + """ + Fixture to provide a default LogConfig instance. + + This fixture sets up a LogConfig object with sample values for various logging + configuration options, including log levels, log directory, formats, and email notifier settings. + + Returns: + ------- + LogConfig + An instance of the LogConfig class initialized with sample values. + """ return LogConfig( log_levels=["INFO", "WARNING", "ERROR"], log_dir="/tmp/logs", @@ -24,50 +35,97 @@ def log_config(): @pytest.fixture def log_manager(log_config): + """ + Fixture to provide a LogManager instance initialized with a LogConfig. + + This fixture sets up a LogManager object using the provided LogConfig instance + for managing logging configurations and operations. + + Returns: + ------- + LogManager + An instance of the LogManager class initialized with the provided LogConfig. + """ return LogManager(log_config) def test_resolve_format(): - # Test with int format option + """ + Test resolution of format options in LogConfig. + + This test verifies that the `resolve_format` method of LogConfig correctly + resolves format options based on different input types, including integer + format options and string formats. It also checks the behavior with colorization + enabled and disabled. + + Asserts: + ------- + - The integer format option resolves to the expected value from FORMAT_OPTIONS. + - The None format resolves to an appropriate default. + - String formats resolve to themselves. + """ resolved_format_option = LogConfig.resolve_format(1, use_colors=False) resolved_none_format = LogConfig.resolve_format(None, use_colors=False) assert resolved_format_option == FORMAT_OPTIONS[1] assert resolved_none_format - # Test with str format option resolved_format = LogConfig.resolve_format("%(message)s", use_colors=False) assert resolved_format == "%(message)s" - # Test with color enabled resolved_format = LogConfig.resolve_format("%(message)s", use_colors=True) assert resolved_format == "%(message)s" def test_remove_ansi_escape_sequences(): + """ + Test removal of ANSI escape sequences. + + This test verifies that the `remove_ansi_escape_sequences` method correctly + removes ANSI escape sequences from a string, resulting in a clean output. + + Asserts: + ------- + - The ANSI escape sequences are removed, leaving only the clean string. + """ ansi_string = "\x1b[31mERROR\x1b[0m" clean_string = LogConfig.remove_ansi_escape_sequences(ansi_string) assert clean_string == "ERROR" def test_create_log_files(log_manager): + """ + Test creation of log files. + + This test checks that the `create_log_files` method of LogManager correctly + creates log files for each log level and ensures that the necessary directories + are created. It also verifies that the open function is called the expected number + of times with the correct file paths. + + Mocks: + ------ + - `os.makedirs` to ensure directory creation. + - `os.path.exists` to simulate file existence checks. + - `builtins.open` to simulate file creation. + + Asserts: + ------- + - The `os.makedirs` function is called to create the log directory. + - The `open` function is called for each log level with the expected file path. + """ with mock.patch("os.makedirs") as makedirs_mock, mock.patch( "os.path.exists" ) as path_exists_mock, mock.patch("builtins.open", mock.mock_open()) as open_mock: - # Mock path_exists to return False so that it always attempts to create the file path_exists_mock.return_value = False log_manager.log_config.log_levels = ["INFO", "ERROR"] log_manager.log_files = {} log_manager.create_log_files() - # Check that the directories were created makedirs_mock.assert_called_with("/tmp/logs", exist_ok=True) - # Verify the log files are created assert open_mock.call_count == len(log_manager.log_config.log_levels) - # Verify file creation paths for log_level in log_manager.log_config.log_levels: expected_file_path = os.path.join("/tmp/logs", f"{log_level.lower()}.log") open_mock.assert_any_call(expected_file_path, "w") @@ -76,51 +134,74 @@ def test_create_log_files(log_manager): def test_set_conf(log_manager): + """ + Test setting up logging configuration. + + This test verifies that the `set_conf` method of LogManager correctly configures + logging settings using `logging.config.dictConfig`. It checks the structure of the + configuration dictionary to ensure all necessary components are present. + + Mocks: + ------ + - `logging.config.dictConfig` to simulate setting the logging configuration. + + Asserts: + ------- + - The `dictConfig` function is called. + - The configuration dictionary contains expected keys and structures. + """ with mock.patch("logging.config.dictConfig") as dictConfig_mock: log_manager.create_log_files() log_manager.set_conf() - # Check that the logging config was set assert dictConfig_mock.called config = dictConfig_mock.call_args[0][0] - # Verify the structure of the config assert "version" in config assert "formatters" in config assert "handlers" in config assert "loggers" in config assert "root" in config - # Check formatters assert "info" in config["formatters"] assert "error" in config["formatters"] assert "console" in config["formatters"] assert "email" in config["formatters"] - # Check handlers assert "info" in config["handlers"] assert "error" in config["handlers"] assert "console" in config["handlers"] assert "email_error" in config["handlers"] - # Check loggers assert "info" in config["loggers"] assert "error" in config["loggers"] - # Check the root logger assert "handlers" in config["root"] assert "disable_existing_loggers" in config def test_log_manager_get_log_file(log_manager): + """ + Test retrieval of log file paths. + + This test verifies that the `get_log_file` method of LogManager correctly returns + the file path for existing log levels and returns None for non-existent log levels. + + Mocks: + ------ + - `os.makedirs` and `builtins.open` to simulate log file creation. + + Asserts: + ------- + - The correct file path is returned for existing log levels. + - None is returned for a non-existent log level. + """ with mock.patch("os.makedirs"), mock.patch("builtins.open", mock.mock_open()): log_manager.create_log_files() - # Check retrieving the log files assert log_manager.get_log_file("INFO") == "/tmp/logs\\info.log" assert log_manager.get_log_file("ERROR") == "/tmp/logs\\error.log" - # Check for a non-existent log level assert log_manager.get_log_file("DEBUG") is None diff --git a/django_logging/tests/utils/test_context_manager.py b/django_logging/tests/utils/test_context_manager.py index 2e89219..9b06c84 100644 --- a/django_logging/tests/utils/test_context_manager.py +++ b/django_logging/tests/utils/test_context_manager.py @@ -10,20 +10,41 @@ @pytest.fixture def mock_logger(): - """Fixture to create a mock logger for testing.""" + """ + Fixture to create a mock logger for testing. + + This fixture creates a mock logger object, which is used to test logging-related + functionality without affecting the actual logging configuration. + + Yields: + ------- + logging.Logger + A mock logger instance with its manager mocked. + """ logger = logging.getLogger() with mock.patch.object(logger, "manager", new_callable=mock.Mock): yield logger def test_config_setup_auto_initialization_enabled(): - """Test that ValueError is raised when auto-initialization is enabled.""" + """ + Test that ValueError is raised when auto-initialization is enabled. + + This test verifies that if auto-initialization is enabled in the configuration, + the `config_setup` context manager raises a ValueError with the appropriate message. + + Asserts: + ------- + - A ValueError is raised with the message indicating that `AUTO_INITIALIZATION_ENABLE` + must be set to False. + """ with mock.patch( "django_logging.utils.context_manager.is_auto_initialization_enabled", return_value=True, ): with pytest.raises(ValueError) as excinfo: - with config_setup(): "" + with config_setup(): + """""" assert ( str(excinfo.value) @@ -32,7 +53,25 @@ def test_config_setup_auto_initialization_enabled(): def test_config_setup_applies_custom_config(mock_logger): - """Test that the custom logging configuration is applied.""" + """ + Test that the custom logging configuration is applied. + + This test checks that when auto-initialization is disabled, the `config_setup` context manager + correctly applies custom logging configurations obtained from `get_config`. It verifies that + the `LogManager` is used, and its methods for creating log files and setting the configuration + are called. + + Mocks: + ------ + - `django_logging.utils.context_manager.is_auto_initialization_enabled` to simulate disabled auto-initialization. + - `django_logging.utils.context_manager.get_config` to provide custom configuration values. + - `django_logging.utils.context_manager.LogManager` to check interaction with the log manager. + + Asserts: + ------- + - The `LogManager` instance returned by `config_setup` matches the mocked instance. + - The `create_log_files` and `set_conf` methods of the `LogManager` are called. + """ with mock.patch( "django_logging.utils.context_manager.is_auto_initialization_enabled", return_value=False, @@ -58,16 +97,29 @@ def test_config_setup_applies_custom_config(mock_logger): mock_log_manager = MockLogManager.return_value with config_setup() as log_manager: - # Assert that the custom log manager is used assert log_manager is mock_log_manager - - # Assert the log files are created and config is set mock_log_manager.create_log_files.assert_called_once() mock_log_manager.set_conf.assert_called_once() def test_config_context_restores_original_config(mock_logger): - """Test that the original logging configuration is restored after context exit.""" + """ + Test that the original logging configuration is restored after context exit. + + This test verifies that the `config_setup` context manager correctly restores the original + logging configuration after exiting the context. It checks that the logger's original settings + (config, level, handlers) are restored as they were before the context was entered. + + Mocks: + ------ + - `django_logging.utils.context_manager.is_auto_initialization_enabled` to simulate disabled auto-initialization. + - `django_logging.utils.context_manager.get_config` to provide custom configuration values. + - `django_logging.utils.context_manager.LogManager` to avoid actual log manager interaction. + + Asserts: + ------- + - The logger's configuration, level, and handlers are restored to their original state. + """ original_config = mock_logger.manager.loggerDict original_level = mock_logger.level original_handlers = mock_logger.handlers @@ -93,18 +145,26 @@ def test_config_context_restores_original_config(mock_logger): ): with mock.patch("django_logging.utils.context_manager.LogManager"): with config_setup(): - # Change the logger's configuration mock_logger.level = logging.ERROR mock_logger.handlers.append(logging.NullHandler()) - # After exiting the context, original config should be restored assert mock_logger.manager.loggerDict == original_config assert mock_logger.level == original_level assert mock_logger.handlers == original_handlers def test_restore_logging_config(mock_logger): - """Test the _restore_logging_config helper function.""" + """ + Test the _restore_logging_config helper function. + + This test checks that the `_restore_logging_config` function correctly restores the logger's + original configuration, level, and handlers. It verifies that after calling `_restore_logging_config`, + the logger is returned to its initial state. + + Asserts: + ------- + - The logger's configuration, level, and handlers match the original values provided to the function. + """ original_config = mock_logger.manager.loggerDict original_level = mock_logger.level original_handlers = mock_logger.handlers diff --git a/django_logging/tests/utils/test_email_notifier.py b/django_logging/tests/utils/test_email_notifier.py index 464d4f3..5220440 100644 --- a/django_logging/tests/utils/test_email_notifier.py +++ b/django_logging/tests/utils/test_email_notifier.py @@ -1,18 +1,40 @@ import logging import pytest import threading -from unittest.mock import patch, MagicMock +from unittest.mock import patch from django_logging.utils.log_email_notifier.notifier import send_email_async @pytest.fixture def mock_smtp(): + """ + Fixture to mock the SMTP object used for sending emails. + + This fixture patches the `SMTP` class from `smtplib` to prevent actual email sending and + allows testing the interactions with the mock SMTP object. + + Yields: + ------- + unittest.mock.MagicMock + A mock object representing the SMTP class. + """ with patch("django_logging.utils.log_email_notifier.notifier.SMTP") as mock_smtp: yield mock_smtp @pytest.fixture def mock_settings(): + """ + Fixture to mock the Django settings used for email configuration. + + This fixture patches the `settings` object to provide fake email configuration values + without needing to access the actual Django settings. + + Yields: + ------- + unittest.mock.MagicMock + A mock object representing the Django settings with predefined email configurations. + """ with patch("django_logging.utils.log_email_notifier.notifier.settings") as mock_settings: mock_settings.DEFAULT_FROM_EMAIL = "from@example.com" mock_settings.EMAIL_HOST = "smtp.example.com" @@ -24,60 +46,83 @@ def mock_settings(): @pytest.fixture def mock_logger(): + """ + Fixture to mock the logger used for logging messages. + + This fixture patches the `logger.info` and `logger.warning` methods to intercept and test + logging calls without affecting the actual logging configuration. + + Yields: + ------- + tuple + A tuple containing mock objects for `logger.info` and `logger.warning`. + """ with patch("django_logging.utils.log_email_notifier.notifier.logger.info") as mock_info, patch( "django_logging.utils.log_email_notifier.notifier.logger.warning" ) as mock_warning: yield mock_info, mock_warning -# Test for successful email sending def test_send_email_async_success(mock_smtp, mock_settings, mock_logger): + """ + Test that the send_email_async function successfully sends an email. + + This test verifies that when `send_email_async` is called with valid parameters: + - The SMTP server is correctly initialized with the specified host and port. + - The login method is called with the correct credentials. + - The email is sent with the expected 'From', 'To', 'Subject', and 'Body' fields. + - The SMTP connection is properly terminated with a call to `quit`. + - The success log message is correctly written. + + Mocks: + ------ + - `django_logging.utils.log_email_notifier.notifier.SMTP` to simulate SMTP interactions. + - `django_logging.utils.log_email_notifier.notifier.settings` to provide mock email settings. + - `django_logging.utils.log_email_notifier.notifier.logger.info` and `logger.warning` to test logging behavior. + + Asserts: + ------- + - The `SMTP` object was called with the correct host and port. + - The `login` method was called with the correct credentials. + - The `sendmail` method was called with the correct email fields. + - The `quit` method was called on the SMTP instance. + - The success message was logged and no warning message was logged. + """ mock_info, mock_warning = mock_logger - # Create an event to signal when the email has been sent email_sent_event = threading.Event() - # Pass the event to the send_email_async function send_email_async( "Test Subject", "Test Body", ["to@example.com"], event=email_sent_event ) - # Wait for the event to be set, indicating the email was sent email_sent_event.wait() - # Ensure the SMTP server was called with the correct parameters mock_smtp.assert_called_once_with( mock_settings.EMAIL_HOST, mock_settings.EMAIL_PORT ) mock_smtp_instance = mock_smtp.return_value - # Check if login was called with the correct credentials mock_smtp_instance.login.assert_called_once_with( mock_settings.EMAIL_HOST_USER, mock_settings.EMAIL_HOST_PASSWORD ) - # Capture the arguments passed to sendmail sendmail_args = mock_smtp_instance.sendmail.call_args[0] - # Verify the email content expected_from = mock_settings.DEFAULT_FROM_EMAIL expected_to = ["to@example.com"] expected_subject = "Test Subject" expected_body = "Test Body" - # Check that the 'From' and 'To' fields are correct assert sendmail_args[0] == expected_from assert sendmail_args[1] == expected_to - # Parse the actual email content and compare it to the expected content actual_email_content = sendmail_args[2] assert f"Subject: {expected_subject}" in actual_email_content assert expected_body in actual_email_content - # Check if the SMTP server was properly quit mock_smtp_instance.quit.assert_called_once() - # Ensure the success log message was written mock_info.assert_called_once_with( "Log Record has been sent to ADMIN EMAIL successfully." ) @@ -85,6 +130,22 @@ def test_send_email_async_success(mock_smtp, mock_settings, mock_logger): def test_send_email_async_failure(mock_smtp, mock_settings, mock_logger): + """ + Test that the send_email_async function handles SMTP failures. + + This test verifies that when `send_email_async` encounters an SMTP exception: + - The failure is logged with an appropriate error message. + - The success message is not logged. + + Mocks: + ------ + - `django_logging.utils.log_email_notifier.notifier.SMTP` to simulate an SMTP failure. + + Asserts: + ------- + - The warning message was logged indicating the failure. + - The success message was not logged. + """ mock_info, mock_warning = mock_logger mock_smtp.side_effect = Exception("SMTP failure") @@ -96,7 +157,6 @@ def test_send_email_async_failure(mock_smtp, mock_settings, mock_logger): email_sent_event.wait() - # Ensure the failure log message was written mock_warning.assert_called_once_with( "Email Notifier failed to send Log Record: SMTP failure" ) diff --git a/django_logging/tests/utils/test_get_conf.py b/django_logging/tests/utils/test_get_conf.py index ba57f50..5a68ce0 100644 --- a/django_logging/tests/utils/test_get_conf.py +++ b/django_logging/tests/utils/test_get_conf.py @@ -11,7 +11,18 @@ @pytest.fixture def mock_settings(): - """Fixture to mock Django settings.""" + """ + Fixture to mock Django settings. + + This fixture sets up mock settings for `DJANGO_LOGGING` to provide controlled values + for testing the configuration functions. The settings are patched into the Django settings + object during the test. + + Yields: + ------- + dict + A dictionary containing the mock settings used in the tests. + """ mock_settings = { "DJANGO_LOGGING": { "LOG_FILE_LEVELS": ["DEBUG", "INFO"], @@ -36,6 +47,21 @@ def mock_settings(): def test_get_conf(mock_settings): + """ + Test that the `get_config` function returns the correct configuration values. + + This test verifies that the `get_config` function extracts and returns the correct + configuration values from the Django settings. + + Mocks: + ------ + - `django.conf.settings` to provide mock configuration values. + + Asserts: + ------- + - The returned configuration matches the expected values for logging levels, directory, + file formats, console settings, email notifier settings, etc. + """ expected = [ ["DEBUG", "INFO"], # log_levels "/custom/log/dir", # log_dir @@ -55,6 +81,21 @@ def test_get_conf(mock_settings): def test_use_email_notifier_template(mock_settings): + """ + Test that the `use_email_notifier_template` function correctly reads the `USE_TEMPLATE` setting. + + This test verifies that the `use_email_notifier_template` function returns `True` by default, + and correctly reflects changes to the `USE_TEMPLATE` setting. + + Mocks: + ------ + - `django.conf.settings` to provide mock configuration values. + + Asserts: + ------- + - The default return value of `use_email_notifier_template` is `True`. + - Changing the `USE_TEMPLATE` setting to `False` updates the return value accordingly. + """ # By default, USE_TEMPLATE is True assert use_email_notifier_template() is True @@ -65,6 +106,21 @@ def test_use_email_notifier_template(mock_settings): def test_is_auto_initialization_enabled(mock_settings): + """ + Test that the `is_auto_initialization_enabled` function correctly reads the `AUTO_INITIALIZATION_ENABLE` setting. + + This test verifies that the `is_auto_initialization_enabled` function returns `True` by default, + and correctly reflects changes to the `AUTO_INITIALIZATION_ENABLE` setting. + + Mocks: + ------ + - `django.conf.settings` to provide mock configuration values. + + Asserts: + ------- + - The default return value of `is_auto_initialization_enabled` is `True`. + - Changing the `AUTO_INITIALIZATION_ENABLE` setting to `False` updates the return value accordingly. + """ # By default, AUTO_INITIALIZATION_ENABLE is True assert is_auto_initialization_enabled() is True @@ -75,6 +131,21 @@ def test_is_auto_initialization_enabled(mock_settings): def test_is_initialization_message_enabled(mock_settings): + """ + Test that the `is_initialization_message_enabled` function correctly reads the `INITIALIZATION_MESSAGE_ENABLE` setting. + + This test verifies that the `is_initialization_message_enabled` function returns `True` by default, + and correctly reflects changes to the `INITIALIZATION_MESSAGE_ENABLE` setting. + + Mocks: + ------ + - `django.conf.settings` to provide mock configuration values. + + Asserts: + ------- + - The default return value of `is_initialization_message_enabled` is `True`. + - Changing the `INITIALIZATION_MESSAGE_ENABLE` setting to `False` updates the return value accordingly. + """ # By default, INITIALIZATION_MESSAGE_ENABLE is True assert is_initialization_message_enabled() is True diff --git a/django_logging/tests/utils/test_log_and_notify.py b/django_logging/tests/utils/test_log_and_notify.py index 38b2e14..32e75bc 100644 --- a/django_logging/tests/utils/test_log_and_notify.py +++ b/django_logging/tests/utils/test_log_and_notify.py @@ -7,6 +7,19 @@ # Helper function to mock LogConfig def mock_log_config(email_notifier_enable=True): + """ + Helper function to create a mock LogConfig object. + + Parameters: + ----------- + email_notifier_enable : bool + Indicates whether the email notifier is enabled. + + Returns: + -------- + MagicMock + A mock object with specified settings. + """ return MagicMock( log_email_notifier_enable=email_notifier_enable, log_email_notifier_log_format=1, @@ -15,18 +28,50 @@ def mock_log_config(email_notifier_enable=True): @pytest.fixture def mock_logger(): + """ + Fixture to create a mock logger object for testing. + + Returns: + -------- + MagicMock + A mock logger object used in the tests. + """ return MagicMock() @pytest.fixture def mock_settings(): + """ + Fixture to mock Django settings related to email notifications. + + This fixture sets up a mock ADMIN_EMAIL setting for testing and cleans up + by deleting the setting after the test. + + Yields: + ------- + None + """ settings.ADMIN_EMAIL = "admin@example.com" yield del settings.ADMIN_EMAIL -# Test: Email notifier is disabled def test_log_and_notify_email_notifier_disabled(mock_logger): + """ + Test that a ValueError is raised when email notifier is disabled. + + This test checks that the `log_and_notify_admin` function raises a `ValueError` + if the email notifier is disabled in the configuration. + + Mocks: + ------ + - `django_logging.utils.log_email_notifier.log_and_notify.get_config` to return a + configuration where the email notifier is disabled. + + Asserts: + ------- + - A `ValueError` with the message "Email notifier is disabled" is raised. + """ with patch( "django_logging.utils.log_email_notifier.log_and_notify.get_config", return_value=mock_log_config(False), @@ -35,8 +80,28 @@ def test_log_and_notify_email_notifier_disabled(mock_logger): log_and_notify_admin(mock_logger, logging.ERROR, "Test message") -# Test: Successful log and email notification to admin def test_log_and_notify_admin_success(mock_logger, mock_settings): + """ + Test successful logging and email notification to admin. + + This test verifies that the `log_and_notify_admin` function correctly handles a log + record and sends an email notification when the email notifier is enabled. + + Mocks: + ------ + - `django_logging.utils.log_email_notifier.log_and_notify.get_config` to return a + configuration where the email notifier is enabled. + - `django_logging.utils.log_email_notifier.log_and_notify.inspect.currentframe` to + simulate the current frame information. + - `mock_logger.makeRecord` to simulate creating a log record. + - `EmailHandler.render_template` to provide a mock email body. + - `send_email_async` to check the email sending functionality. + + Asserts: + ------- + - The log record is handled by the logger. + - An email is sent with the expected subject and body. + """ log_record = logging.LogRecord( name="test_logger", level=logging.ERROR, @@ -80,8 +145,25 @@ def test_log_and_notify_admin_success(mock_logger, mock_settings): ) -# Test: Logging failure due to invalid parameters def test_log_and_notify_admin_logging_failure(mock_logger, mock_settings): + """ + Test logging failure due to invalid parameters. + + This test verifies that the `log_and_notify_admin` function raises a `ValueError` + if there is an error during the creation of the log record. + + Mocks: + ------ + - `django_logging.utils.log_email_notifier.log_and_notify.get_config` to return a + configuration where the email notifier is enabled. + - `django_logging.utils.log_email_notifier.log_and_notify.inspect.currentframe` to + simulate the current frame information. + - Simulates a `TypeError` during `mock_logger.makeRecord`. + + Asserts: + ------- + - A `ValueError` with the message "Failed to log message due to invalid param" is raised. + """ with patch( "django_logging.utils.log_email_notifier.log_and_notify.get_config", return_value=mock_log_config(True), @@ -103,8 +185,25 @@ def test_log_and_notify_admin_logging_failure(mock_logger, mock_settings): log_and_notify_admin(mock_logger, logging.ERROR, "Test message") -# Test: Missing ADMIN_EMAIL setting def test_log_and_notify_admin_missing_admin_email(mock_logger): + """ + Test logging and email notification when ADMIN_EMAIL is missing. + + This test verifies that the `log_and_notify_admin` function raises a `ValueError` + if the `ADMIN_EMAIL` setting is not provided. + + Mocks: + ------ + - `django_logging.utils.log_email_notifier.log_and_notify.get_config` to return a + configuration where the email notifier is enabled. + - `django_logging.utils.log_email_notifier.log_and_notify.inspect.currentframe` to + simulate the current frame information. + - `mock_logger.makeRecord` to simulate creating a log record. + + Asserts: + ------- + - A `ValueError` with the message "'ADMIN EMAIL' not provided, please provide 'ADMIN_EMAIL' in your settings" is raised. + """ # Simulate the absence of ADMIN_EMAIL in settings settings.ADMIN_EMAIL = None diff --git a/django_logging/tests/utils/test_set_conf.py b/django_logging/tests/utils/test_set_conf.py index d773add..f8cf08a 100644 --- a/django_logging/tests/utils/test_set_conf.py +++ b/django_logging/tests/utils/test_set_conf.py @@ -19,6 +19,29 @@ def test_set_config_success( mock_LogManager, mock_LogConfig, ): + """ + Test the successful execution of set_config. + + This test verifies that the set_config function correctly initializes + LogConfig and LogManager when auto initialization is enabled. It also + checks that the initialization message is logged if RUN_MAIN is set to "true" + and initialization messages are enabled. + + Mocks: + ------ + - `django_logging.utils.get_conf.get_config` to return mock configuration values. + - `logging.getLogger` to provide a mock logger. + - `django_logging.utils.set_conf.LogConfig` to simulate LogConfig instantiation. + - `django_logging.utils.set_conf.LogManager` to simulate LogManager instantiation. + - `django_logging.utils.set_conf.is_auto_initialization_enabled` to control auto initialization. + - `django_logging.utils.set_conf.is_initialization_message_enabled` to check if initialization messages are enabled. + + Asserts: + ------- + - `LogConfig` and `LogManager` are instantiated with the expected arguments. + - `create_log_files` and `set_conf` methods of `LogManager` are called once. + - If RUN_MAIN is "true" and initialization messages are enabled, an info log is generated. + """ # Mock the configuration mock_is_auto_initialization_enabled.return_value = True mock_get_conf.return_value = ( @@ -95,6 +118,22 @@ def test_set_config_success( def test_set_config_auto_initialization_disabled( mock_is_auto_initialization_enabled, mock_LogManager, mock_LogConfig ): + """ + Test that LogConfig and LogManager are not instantiated when auto initialization is disabled. + + This test verifies that when auto initialization is disabled, the set_config function + does not instantiate `LogConfig` or `LogManager`. + + Mocks: + ------ + - `django_logging.utils.set_conf.is_auto_initialization_enabled` to return False. + - `django_logging.utils.set_conf.LogConfig` to simulate LogConfig instantiation. + - `django_logging.utils.set_conf.LogManager` to simulate LogManager instantiation. + + Asserts: + ------- + - `LogConfig` and `LogManager` are not instantiated. + """ mock_is_auto_initialization_enabled.return_value = False # Call the function @@ -122,6 +161,22 @@ def test_set_config_auto_initialization_disabled( def test_set_config_exception_handling( mock_is_auto_initialization_enabled, mock_LogManager, mock_LogConfig ): + """ + Test that set_config handles exceptions and logs a warning message. + + This test verifies that if an exception occurs during the instantiation of + `LogManager`, a warning message is logged indicating a configuration error. + + Mocks: + ------ + - `django_logging.utils.set_conf.is_auto_initialization_enabled` to return True. + - `django_logging.utils.set_conf.LogManager` to raise a ValueError. + - `logging.warning` to capture the warning message. + + Asserts: + ------- + - A warning message indicating a configuration error is logged. + """ mock_is_auto_initialization_enabled.return_value = True mock_LogManager.side_effect = ValueError("Invalid configuration") diff --git a/django_logging/tests/validators/test_config_validators.py b/django_logging/tests/validators/test_config_validators.py index 82ad4c4..38afd0e 100644 --- a/django_logging/tests/validators/test_config_validators.py +++ b/django_logging/tests/validators/test_config_validators.py @@ -11,6 +11,23 @@ def test_validate_directory_success(): + """ + Test the successful validation of a directory path. + + This test verifies that the `validate_directory` function correctly + creates a new directory if it does not exist and does not return any + errors when a valid directory path is provided. + + Mocks: + ------ + - `os.path.exists` to simulate directory existence check. + - `os.mkdir` to simulate directory creation. + + Asserts: + ------- + - No errors are returned. + - The directory creation function is called once. + """ with patch("os.path.exists") as mock_exists, patch("os.mkdir") as mock_mkdir: mock_exists.return_value = False errors = validate_directory("new_dir", "test_directory") @@ -19,6 +36,20 @@ def test_validate_directory_success(): def test_validate_directory_invalid_path(): + """ + Test validation of invalid directory paths. + + This test verifies that `validate_directory` returns appropriate errors + when provided with invalid paths, such as `None` or invalid file paths. + + Mocks: + ------ + - `os.path.exists` to simulate directory existence check. + + Asserts: + ------- + - Appropriate errors are returned for invalid paths. + """ with patch("os.path.exists") as mock_exists: mock_exists.return_value = False errors = validate_directory(None, "test_directory") @@ -31,6 +62,22 @@ def test_validate_directory_invalid_path(): def test_validate_directory_is_file(): + """ + Test validation when the path is a file, not a directory. + + This test verifies that `validate_directory` correctly identifies and + returns an error when the specified path is a file instead of a directory. + + Mocks: + ------ + - `os.path.isdir` to simulate directory check. + - `os.path.isfile` to simulate file check. + - `os.path.exists` to simulate path existence check. + + Asserts: + ------- + - Appropriate error is returned indicating the path is not a directory. + """ file_path = "path/to/file.txt" with patch("os.path.isdir") as mock_isdir, patch( @@ -52,12 +99,40 @@ def test_validate_directory_is_file(): def test_validate_log_levels_success(): + """ + Test successful validation of log levels. + + This test verifies that `validate_log_levels` does not return any errors + when provided with valid log levels. + + Mocks: + ------ + - None. + + Asserts: + ------- + - No errors are returned for valid log levels. + """ valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] errors = validate_log_levels(["DEBUG", "INFO"], "log_levels", valid_levels) assert not errors def test_validate_log_levels_invalid_type(): + """ + Test validation of log levels with invalid types. + + This test verifies that `validate_log_levels` returns errors when provided + with invalid log level types, such as a string or an empty list. + + Mocks: + ------ + - None. + + Asserts: + ------- + - Appropriate errors are returned for invalid log level types. + """ valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] errors = validate_log_levels("DEBUG, INFO", "log_levels", valid_levels) assert len(errors) == 1 @@ -69,12 +144,41 @@ def test_validate_log_levels_invalid_type(): def test_validate_format_string_success(): + """ + Test successful validation of a log format string. + + This test verifies that `validate_format_string` does not return any errors + when provided with a valid format string. + + Mocks: + ------ + - None. + + Asserts: + ------- + - No errors are returned for a valid format string. + """ format_str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" errors = validate_format_string(format_str, "log_format") assert not errors def test_validate_format_string_invalid(): + """ + Test validation of an invalid log format string. + + This test verifies that `validate_format_string` returns appropriate errors + when provided with invalid format strings, such as incorrect placeholders + or invalid types. + + Mocks: + ------ + - None. + + Asserts: + ------- + - Appropriate errors are returned for invalid format strings. + """ format_str = "%(invalid)s" errors = validate_format_string(format_str, "log_format") assert len(errors) == 1 @@ -92,12 +196,41 @@ def test_validate_format_string_invalid(): def test_validate_format_option_integer_success(): + """ + Test successful validation of an integer format option. + + This test verifies that `validate_format_option` does not return any errors + when provided with a valid integer format option. + + Mocks: + ------ + - None. + + Asserts: + ------- + - No errors are returned for a valid integer format option. + """ format_option = 1 errors = validate_format_option(format_option, "log_format_option") assert not errors def test_validate_format_option_failure(): + """ + Test validation of invalid format options. + + This test verifies that `validate_format_option` returns appropriate errors + when provided with invalid format options, such as integers outside of a valid + range or non-integer values. + + Mocks: + ------ + - None. + + Asserts: + ------- + - Appropriate errors are returned for invalid format options. + """ format_option = 15 errors = validate_format_option(format_option, "log_format_option") assert len(errors) == 1 @@ -110,23 +243,80 @@ def test_validate_format_option_failure(): def test_validate_format_option_string_success(): + """ + Test successful validation of a string format option. + + This test verifies that `validate_format_option` does not return any errors + when provided with a valid string format option. + + Mocks: + ------ + - None. + + Asserts: + ------- + - No errors are returned for a valid string format option. + """ format_option = "%(asctime)s - %(message)s" errors = validate_format_option(format_option, "log_format_option") assert not errors def test_validate_boolean_setting_success(): + """ + Test successful validation of a boolean setting. + + This test verifies that `validate_boolean_setting` does not return any errors + when provided with a valid boolean setting. + + Mocks: + ------ + - None. + + Asserts: + ------- + - No errors are returned for a valid boolean setting. + """ errors = validate_boolean_setting(True, "boolean_setting") assert not errors def test_validate_date_format_success(): + """ + Test successful validation of a date format string. + + This test verifies that `validate_date_format` does not return any errors + when provided with a valid date format string. + + Mocks: + ------ + - None. + + Asserts: + ------- + - No errors are returned for a valid date format string. + """ date_format = "%Y-%m-%d" errors = validate_date_format(date_format, "date_format") assert not errors def test_validate_date_format_invalid_format(): + """ + Test validation of invalid date format strings. + + This test verifies that `validate_date_format` returns appropriate errors + when provided with invalid date format strings, such as incorrect types + or invalid format patterns. + + Mocks: + ------ + - None. + + Asserts: + ------- + - Appropriate errors are returned for invalid date format strings. + """ date_format = 1 # invalid type errors = validate_date_format(date_format, "date_format") assert len(errors) == 1 @@ -139,6 +329,20 @@ def test_validate_date_format_invalid_format(): def test_validate_email_notifier_success(): + """ + Test successful validation of email notifier configuration. + + This test verifies that `validate_email_notifier` does not return any errors + when provided with a valid email notifier configuration. + + Mocks: + ------ + - None. + + Asserts: + ------- + - No errors are returned for a valid email notifier configuration. + """ notifier_config = { "ENABLE": True, "LOG_FORMAT": "%(asctime)s - %(message)s", @@ -149,6 +353,20 @@ def test_validate_email_notifier_success(): def test_validate_email_notifier_invalid_type(): + """ + Test validation of invalid email notifier configuration types. + + This test verifies that `validate_email_notifier` returns appropriate errors + when provided with configurations that have incorrect types. + + Mocks: + ------ + - None. + + Asserts: + ------- + - Appropriate errors are returned for invalid configuration types. + """ notifier_config = ["ENABLE", "LOG_FORMAT"] errors = validate_email_notifier(notifier_config) assert len(errors) == 1 diff --git a/django_logging/tests/validators/test_email_settings_validator.py b/django_logging/tests/validators/test_email_settings_validator.py index bf856ef..8d7031c 100644 --- a/django_logging/tests/validators/test_email_settings_validator.py +++ b/django_logging/tests/validators/test_email_settings_validator.py @@ -8,7 +8,19 @@ # Mock required email settings for testing @pytest.fixture def mock_email_settings(): - """Fixture to mock Django email settings.""" + """ + Fixture to mock Django email settings. + + Returns: + -------- + dict: A dictionary containing mock email settings: + - EMAIL_HOST: "smtp.example.com" + - EMAIL_PORT: 587 + - EMAIL_HOST_USER: "user@example.com" + - EMAIL_HOST_PASSWORD: "password" + - DEFAULT_FROM_EMAIL: "from@example.com" + - ADMIN_EMAIL: "to@example.com" + """ return { "EMAIL_HOST": "smtp.example.com", "EMAIL_PORT": 587, @@ -20,7 +32,25 @@ def mock_email_settings(): def test_check_email_settings_all_present(mock_email_settings): - """Test when all required email settings are present.""" + """ + Test validation when all required email settings are present. + + This test verifies that `check_email_settings` does not return any errors + when all required email settings are correctly configured. + + Mocks: + ------ + - `settings.EMAIL_HOST` set to "smtp.example.com" + - `settings.EMAIL_PORT` set to 587 + - `settings.EMAIL_HOST_USER` set to "user@example.com" + - `settings.EMAIL_HOST_PASSWORD` set to "password" + - `settings.DEFAULT_FROM_EMAIL` set to "from@example.com" + - `settings.ADMIN_EMAIL` set to "to@example.com" + + Asserts: + ------- + - No errors are returned when all required settings are present. + """ with patch.object(settings, "EMAIL_HOST", mock_email_settings["EMAIL_HOST"]): with patch.object(settings, "EMAIL_PORT", mock_email_settings["EMAIL_PORT"]): with patch.object( @@ -37,21 +67,39 @@ def test_check_email_settings_all_present(mock_email_settings): mock_email_settings["DEFAULT_FROM_EMAIL"], ): with patch.object( - settings, - "ADMIN_EMAIL", - mock_email_settings["ADMIN_EMAIL"], + settings, + "ADMIN_EMAIL", + mock_email_settings["ADMIN_EMAIL"], ): errors = check_email_settings() assert not errors # No errors should be present def test_check_email_settings_missing_some(mock_email_settings): - """Test when some required email settings are missing.""" + """ + Test validation when some required email settings are missing. + + This test verifies that `check_email_settings` returns an error when some + required email settings are missing, specifically the `EMAIL_HOST_USER` setting. + + Mocks: + ------ + - `settings.EMAIL_HOST` set to "smtp.example.com" + - `settings.EMAIL_PORT` set to 587 + - `settings.EMAIL_HOST_USER` set to None (simulating missing setting) + - `settings.EMAIL_HOST_PASSWORD` set to "password" + - `settings.DEFAULT_FROM_EMAIL` set to "from@example.com" + - `settings.ADMIN_EMAIL` set to "to@example.com" + + Asserts: + ------- + - Exactly one error is returned indicating the missing `EMAIL_HOST_USER` setting. + """ with patch.object(settings, "EMAIL_HOST", mock_email_settings["EMAIL_HOST"]): with patch.object(settings, "EMAIL_PORT", mock_email_settings["EMAIL_PORT"]): with patch.object( - settings, "EMAIL_HOST_USER", None - ): # Simulate missing setting + settings, "EMAIL_HOST_USER", None # Simulate missing setting + ): with patch.object( settings, "EMAIL_HOST_PASSWORD", @@ -63,9 +111,9 @@ def test_check_email_settings_missing_some(mock_email_settings): mock_email_settings["DEFAULT_FROM_EMAIL"], ): with patch.object( - settings, - "ADMIN_EMAIL", - mock_email_settings["ADMIN_EMAIL"], + settings, + "ADMIN_EMAIL", + mock_email_settings["ADMIN_EMAIL"], ): errors = check_email_settings() assert len(errors) == 1 @@ -77,7 +125,25 @@ def test_check_email_settings_missing_some(mock_email_settings): def test_check_email_settings_all_missing(): - """Test when all required email settings are missing.""" + """ + Test validation when all required email settings are missing. + + This test verifies that `check_email_settings` returns an error when all + required email settings are missing. + + Mocks: + ------ + - `settings.EMAIL_HOST` set to None + - `settings.EMAIL_PORT` set to None + - `settings.EMAIL_HOST_USER` set to None + - `settings.EMAIL_HOST_PASSWORD` set to None + - `settings.DEFAULT_FROM_EMAIL` set to None + - `settings.ADMIN_EMAIL` set to None + + Asserts: + ------- + - Exactly one error is returned indicating all required email settings are missing. + """ with patch.object(settings, "EMAIL_HOST", None): with patch.object(settings, "EMAIL_PORT", None): with patch.object(settings, "EMAIL_HOST_USER", None): @@ -87,7 +153,7 @@ def test_check_email_settings_all_missing(): errors = check_email_settings() assert len(errors) == 1 assert errors[0].msg.startswith( - 'Missing required email settings: EMAIL_HOST, EMAIL_PORT, EMAIL_HOST_USER,' - ' EMAIL_HOST_PASSWORD, DEFAULT_FROM_EMAIL, ADMIN_EMAIL' + "Missing required email settings: EMAIL_HOST, EMAIL_PORT, EMAIL_HOST_USER," + " EMAIL_HOST_PASSWORD, DEFAULT_FROM_EMAIL, ADMIN_EMAIL" ) assert errors[0].id == "django_logging.E021" From c91dfd2b421b6b28c52c51b55b8ee29aa5907fc1 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 21:06:50 +0330 Subject: [PATCH 22/30] :zap: Update(test) TypeAnnotations in all tests --- .../tests/commands/test_send_logs.py | 27 +++++++---- .../tests/filters/test_log_level_filter.py | 12 +++-- .../formatters/test_colored_formatter.py | 15 +++--- .../tests/handlers/test_email_handler.py | 16 +++---- .../middleware/test_request_middleware.py | 28 +++++++---- django_logging/tests/settings/test_checks.py | 47 ++++++++++--------- django_logging/tests/settings/test_conf.py | 14 +++--- .../tests/utils/test_context_manager.py | 10 ++-- .../tests/utils/test_email_notifier.py | 28 +++++++---- django_logging/tests/utils/test_get_conf.py | 12 +++-- .../tests/utils/test_log_and_notify.py | 18 ++++--- django_logging/tests/utils/test_set_conf.py | 26 +++++----- .../validators/test_config_validators.py | 30 ++++++------ .../test_email_settings_validator.py | 10 ++-- 14 files changed, 172 insertions(+), 121 deletions(-) diff --git a/django_logging/tests/commands/test_send_logs.py b/django_logging/tests/commands/test_send_logs.py index b6d0ac3..88bcff9 100644 --- a/django_logging/tests/commands/test_send_logs.py +++ b/django_logging/tests/commands/test_send_logs.py @@ -2,7 +2,7 @@ import tempfile import shutil -from unittest.mock import patch, ANY +from unittest.mock import patch, ANY, Mock from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command @@ -20,8 +20,11 @@ class SendLogsCommandTests(TestCase): @patch("django_logging.management.commands.send_logs.shutil.make_archive") @patch("django_logging.management.commands.send_logs.EmailMessage") def test_handle_success( - self, mock_email_message, mock_make_archive, mock_validate_email_settings - ): + self, + mock_email_message: Mock, + mock_make_archive: Mock, + mock_validate_email_settings: Mock, + ) -> None: """ Test that the `send_logs` command successfully creates an archive of the logs and sends an email when executed with valid settings. @@ -62,8 +65,8 @@ def test_handle_success( ) @patch("django_logging.management.commands.send_logs.EmailMessage.send") def test_handle_email_send_failure( - self, mock_email_send, mock_validate_email_settings - ): + self, mock_email_send: Mock, mock_validate_email_settings: Mock + ) -> None: """ Test that the `send_logs` command handles email sending failures correctly and logs an appropriate error message. @@ -96,7 +99,7 @@ def test_handle_email_send_failure( @patch( "django_logging.management.commands.send_logs.Command.validate_email_settings" ) - def test_handle_missing_log_dir(self, mock_validate_email_settings): + def test_handle_missing_log_dir(self, mock_validate_email_settings: Mock) -> None: """ Test that the `send_logs` command logs an error when the specified log directory does not exist and skips the email validation step. @@ -127,7 +130,9 @@ def test_handle_missing_log_dir(self, mock_validate_email_settings): "django_logging.management.commands.send_logs.check_email_settings", return_value=None, ) - def test_validate_email_settings_success(self, mock_check_email_settings): + def test_validate_email_settings_success( + self, mock_check_email_settings: Mock + ) -> None: """ Test that the `validate_email_settings` method successfully validates the email settings without raising any exceptions. @@ -147,7 +152,9 @@ def test_validate_email_settings_success(self, mock_check_email_settings): "django_logging.management.commands.send_logs.check_email_settings", return_value="Missing config", ) - def test_validate_email_settings_failure(self, mock_check_email_settings): + def test_validate_email_settings_failure( + self, mock_check_email_settings: Mock + ) -> None: """ Test that the `validate_email_settings` method raises an `ImproperlyConfigured` exception when the email settings are invalid. @@ -167,7 +174,9 @@ def test_validate_email_settings_failure(self, mock_check_email_settings): "django_logging.management.commands.send_logs.Command.validate_email_settings" ) @patch("django_logging.management.commands.send_logs.shutil.make_archive") - def test_cleanup_on_failure(self, mock_make_archive, mock_validate_email_settings): + def test_cleanup_on_failure( + self, mock_make_archive, mock_validate_email_settings: Mock + ) -> None: """ Test that the `send_logs` command cleans up any partially created files when an error occurs during the log archiving process. diff --git a/django_logging/tests/filters/test_log_level_filter.py b/django_logging/tests/filters/test_log_level_filter.py index 9a0dcfa..8a9c8bf 100644 --- a/django_logging/tests/filters/test_log_level_filter.py +++ b/django_logging/tests/filters/test_log_level_filter.py @@ -4,7 +4,7 @@ @pytest.fixture -def log_record(): +def log_record() -> logging.LogRecord: """ Fixture to create a dummy log record for testing. @@ -23,7 +23,7 @@ def log_record(): ) -def test_logging_level_filter_initialization(): +def test_logging_level_filter_initialization() -> None: """ Test that the `LoggingLevelFilter` initializes with the correct logging level. @@ -38,7 +38,9 @@ def test_logging_level_filter_initialization(): ) -def test_logging_level_filter_passes_matching_level(log_record): +def test_logging_level_filter_passes_matching_level( + log_record: logging.LogRecord, +) -> None: """ Test that the `LoggingLevelFilter` passes log records with the matching logging level. @@ -58,7 +60,9 @@ def test_logging_level_filter_passes_matching_level(log_record): ) -def test_logging_level_filter_blocks_non_matching_level(log_record): +def test_logging_level_filter_blocks_non_matching_level( + log_record: logging.LogRecord, +) -> None: """ Test that the `LoggingLevelFilter` blocks log records with a non-matching logging level. diff --git a/django_logging/tests/formatters/test_colored_formatter.py b/django_logging/tests/formatters/test_colored_formatter.py index a5df5bc..a65a18e 100644 --- a/django_logging/tests/formatters/test_colored_formatter.py +++ b/django_logging/tests/formatters/test_colored_formatter.py @@ -1,11 +1,11 @@ import logging import pytest -from unittest.mock import patch +from unittest.mock import patch, MagicMock from django_logging.formatters import ColorizedFormatter @pytest.fixture -def log_record(): +def log_record() -> logging.LogRecord: """ Fixture to create a dummy log record for testing. @@ -26,7 +26,7 @@ def log_record(): @pytest.fixture -def formatter(): +def formatter() -> ColorizedFormatter: """ Fixture to create a `ColorizedFormatter` instance with a specific format. @@ -44,8 +44,8 @@ def formatter(): side_effect=lambda fmt: fmt, ) def test_format_applies_colorization( - mock_remove_ansi, mock_colorize, formatter, log_record -): + mock_remove_ansi: MagicMock, mock_colorize: MagicMock, formatter: ColorizedFormatter, log_record: logging.LogRecord +) -> None: """ Test that the `format` method of `ColorizedFormatter` applies colorization. @@ -82,7 +82,8 @@ def test_format_applies_colorization( "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", side_effect=lambda fmt: fmt, ) -def test_format_resets_to_original_format(mock_remove_ansi, formatter, log_record): +def test_format_resets_to_original_format( + mock_remove_ansi: MagicMock, formatter: ColorizedFormatter, log_record: logging.LogRecord) -> None: """ Test that the `format` method resets the format string to its original state after formatting. @@ -113,7 +114,7 @@ def test_format_resets_to_original_format(mock_remove_ansi, formatter, log_recor "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", side_effect=lambda fmt: fmt, ) -def test_format_returns_formatted_output(formatter, log_record): +def test_format_returns_formatted_output(formatter: ColorizedFormatter, log_record: logging.LogRecord) -> None: """ Test that the `format` method returns the correctly formatted log output. diff --git a/django_logging/tests/handlers/test_email_handler.py b/django_logging/tests/handlers/test_email_handler.py index 719aa76..c5179af 100644 --- a/django_logging/tests/handlers/test_email_handler.py +++ b/django_logging/tests/handlers/test_email_handler.py @@ -8,7 +8,7 @@ @pytest.fixture -def log_record(): +def log_record() -> logging.LogRecord: """ Fixture to create a dummy log record. @@ -29,7 +29,7 @@ def log_record(): @pytest.fixture -def email_handler(): +def email_handler() -> EmailHandler: """ Fixture to create an EmailHandler instance. @@ -48,8 +48,8 @@ def email_handler(): return_value=True, ) def test_emit_with_html_template( - mock_use_template, mock_render_template, mock_send_email, email_handler, log_record -): + mock_use_template: MagicMock, mock_render_template: MagicMock, mock_send_email: MagicMock, email_handler: EmailHandler, log_record: logging.LogRecord +) -> None: """ Test the emit method when HTML templates are used. @@ -89,7 +89,7 @@ def test_emit_with_html_template( "django_logging.handlers.email_handler.use_email_notifier_template", return_value=False, ) -def test_emit_without_html_template(mock_use_template, mock_send_email, log_record): +def test_emit_without_html_template(mock_use_template: MagicMock, mock_send_email: MagicMock, log_record: logging.LogRecord) -> None: """ Test the emit method when HTML templates are not used. @@ -123,8 +123,8 @@ def test_emit_without_html_template(mock_use_template, mock_send_email, log_reco side_effect=Exception("Email send failed"), ) def test_emit_handles_exception( - mock_send_email, mock_handle_error, email_handler, log_record -): + mock_send_email: MagicMock, mock_handle_error: MagicMock, email_handler: EmailHandler, log_record: logging.LogRecord +) -> None: """ Test that the emit method handles exceptions during email sending. @@ -160,7 +160,7 @@ def test_emit_handles_exception( return_value="Mozilla/5.0", ) @patch("django_logging.handlers.email_handler.engines") -def test_render_template(mock_engines, mock_get_user_agent, mock_get_ip_address): +def test_render_template(mock_engines: MagicMock, mock_get_user_agent: MagicMock, mock_get_ip_address: MagicMock): """ Test the render_template method of EmailHandler. diff --git a/django_logging/tests/middleware/test_request_middleware.py b/django_logging/tests/middleware/test_request_middleware.py index 4e5d26c..2f1ceb4 100644 --- a/django_logging/tests/middleware/test_request_middleware.py +++ b/django_logging/tests/middleware/test_request_middleware.py @@ -10,7 +10,7 @@ @pytest.fixture -def request_factory(): +def request_factory() -> RequestFactory: """ Fixture to create a RequestFactory instance for generating request objects. @@ -23,7 +23,7 @@ def request_factory(): @pytest.fixture -def get_response(): +def get_response() -> callable: """ Fixture to create a mock get_response function. @@ -33,14 +33,14 @@ def get_response(): A function that returns an HttpResponse with a dummy response. """ - def _get_response(request): + def _get_response(request) -> HttpResponse: return HttpResponse("Test Response") return _get_response @pytest.fixture -def middleware(get_response): +def middleware(get_response: callable) -> RequestLogMiddleware: """ Fixture to create an instance of RequestLogMiddleware. @@ -57,7 +57,11 @@ def middleware(get_response): return RequestLogMiddleware(get_response) -def test_authenticated_user_logging(middleware, request_factory, caplog): +def test_authenticated_user_logging( + middleware: RequestLogMiddleware, + request_factory: RequestFactory, + caplog: pytest.LogCaptureFixture, +) -> None: """ Test logging of requests for authenticated users. @@ -99,7 +103,11 @@ def test_authenticated_user_logging(middleware, request_factory, caplog): assert request.browser_type -def test_anonymous_user_logging(middleware, request_factory, caplog): +def test_anonymous_user_logging( + middleware: RequestLogMiddleware, + request_factory: RequestFactory, + caplog: pytest.LogCaptureFixture, +) -> None: """ Test logging of requests for anonymous users. @@ -130,7 +138,9 @@ def test_anonymous_user_logging(middleware, request_factory, caplog): assert "Anonymous" in caplog.text -def test_ip_address_extraction(middleware, request_factory): +def test_ip_address_extraction( + middleware: RequestLogMiddleware, request_factory: RequestFactory +) -> None: """ Test extraction of the client's IP address from the request. @@ -155,7 +165,9 @@ def test_ip_address_extraction(middleware, request_factory): assert request.ip_address == "192.168.1.1" -def test_user_agent_extraction(middleware, request_factory): +def test_user_agent_extraction( + middleware: RequestLogMiddleware, request_factory: RequestFactory +) -> None: """ Test extraction of the client's user agent from the request. diff --git a/django_logging/tests/settings/test_checks.py b/django_logging/tests/settings/test_checks.py index f278ada..ff113b0 100644 --- a/django_logging/tests/settings/test_checks.py +++ b/django_logging/tests/settings/test_checks.py @@ -1,3 +1,4 @@ +from typing import Generator, List from unittest.mock import patch import pytest @@ -7,7 +8,7 @@ @pytest.fixture -def reset_settings(): +def reset_settings() -> Generator[None, None, None]: """ Fixture to reset Django settings after each test. @@ -22,7 +23,7 @@ def reset_settings(): settings.DJANGO_LOGGING = original_settings -def test_valid_logging_settings(reset_settings): +def test_valid_logging_settings(reset_settings: None) -> None: """ Test that valid logging settings do not produce any errors. @@ -51,11 +52,11 @@ def test_valid_logging_settings(reset_settings): "LOG_FORMAT": 1, }, } - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert not errors -def test_invalid_log_dir(reset_settings): +def test_invalid_log_dir(reset_settings: None) -> None: """ Test invalid LOG_DIR setting. @@ -69,11 +70,11 @@ def test_invalid_log_dir(reset_settings): settings.DJANGO_LOGGING = { "LOG_DIR": 1, } - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert any(error.id == "django_logging.E001_LOG_DIR" for error in errors) -def test_invalid_log_file_levels(reset_settings): +def test_invalid_log_file_levels(reset_settings: None) -> None: """ Test invalid LOG_FILE_LEVELS setting. @@ -91,7 +92,7 @@ def test_invalid_log_file_levels(reset_settings): assert any(error.id == "django_logging.E007_LOG_FILE_LEVELS" for error in errors) -def test_invalid_log_file_formats(reset_settings): +def test_invalid_log_file_formats(reset_settings: None) -> None: """ Test invalid LOG_FILE_FORMATS setting. @@ -109,7 +110,7 @@ def test_invalid_log_file_formats(reset_settings): "invalid": "%(message)s", }, } - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert any( error.id == "django_logging.E011_LOG_FILE_FORMATS['DEBUG']" for error in errors ) @@ -120,7 +121,7 @@ def test_invalid_log_file_formats(reset_settings): assert any(error.id == "django_logging.E020_LOG_FILE_FORMATS" for error in errors) -def test_invalid_log_console_format(reset_settings): +def test_invalid_log_console_format(reset_settings: None) -> None: """ Test invalid LOG_CONSOLE_FORMAT setting. @@ -138,7 +139,7 @@ def test_invalid_log_console_format(reset_settings): assert any(error.id == "django_logging.E010_LOG_CONSOLE_FORMAT" for error in errors) -def test_invalid_log_console_level(reset_settings): +def test_invalid_log_console_level(reset_settings: None) -> None: """ Test invalid LOG_CONSOLE_LEVEL setting. @@ -152,11 +153,11 @@ def test_invalid_log_console_level(reset_settings): settings.DJANGO_LOGGING = { "LOG_CONSOLE_LEVEL": 10, } - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert any(error.id == "django_logging.E006_LOG_CONSOLE_LEVEL" for error in errors) -def test_invalid_log_console_colorize(reset_settings): +def test_invalid_log_console_colorize(reset_settings: None) -> None: """ Test invalid LOG_CONSOLE_COLORIZE setting. @@ -170,13 +171,13 @@ def test_invalid_log_console_colorize(reset_settings): settings.DJANGO_LOGGING = { "LOG_CONSOLE_COLORIZE": "not_a_boolean", } - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert any( error.id == "django_logging.E014_LOG_CONSOLE_COLORIZE" for error in errors ) -def test_invalid_log_date_format(reset_settings): +def test_invalid_log_date_format(reset_settings: None) -> None: """ Test invalid LOG_DATE_FORMAT setting. @@ -190,11 +191,11 @@ def test_invalid_log_date_format(reset_settings): settings.DJANGO_LOGGING = { "LOG_DATE_FORMAT": "%invalid_format", } - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert any(error.id == "django_logging.E016_LOG_DATE_FORMAT" for error in errors) -def test_invalid_auto_initialization_enable(reset_settings): +def test_invalid_auto_initialization_enable(reset_settings: None) -> None: """ Test invalid AUTO_INITIALIZATION_ENABLE setting. @@ -208,13 +209,13 @@ def test_invalid_auto_initialization_enable(reset_settings): settings.DJANGO_LOGGING = { "AUTO_INITIALIZATION_ENABLE": "not_a_boolean", } - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert any( error.id == "django_logging.E014_AUTO_INITIALIZATION_ENABLE" for error in errors ) -def test_invalid_initialization_message_enable(reset_settings): +def test_invalid_initialization_message_enable(reset_settings: None) -> None: """ Test invalid INITIALIZATION_MESSAGE_ENABLE setting. @@ -228,14 +229,14 @@ def test_invalid_initialization_message_enable(reset_settings): settings.DJANGO_LOGGING = { "INITIALIZATION_MESSAGE_ENABLE": "not_a_boolean", } - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert any( error.id == "django_logging.E014_INITIALIZATION_MESSAGE_ENABLE" for error in errors ) -def test_invalid_log_email_notifier(reset_settings): +def test_invalid_log_email_notifier(reset_settings: None) -> None: """ Test invalid LOG_EMAIL_NOTIFIER setting. @@ -251,14 +252,14 @@ def test_invalid_log_email_notifier(reset_settings): "ENABLE": "not_a_boolean", }, } - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert any( error.id == "django_logging.E018_LOG_EMAIL_NOTIFIER['ENABLE']" for error in errors ) -def test_missing_email_settings(reset_settings): +def test_missing_email_settings(reset_settings: None) -> None: """ Test missing email settings when LOG_EMAIL_NOTIFIER is enabled. @@ -282,5 +283,5 @@ def test_missing_email_settings(reset_settings): mock_check.return_value = [ Error("EMAIL_BACKEND not set.", id="django_logging.E010_EMAIL_SETTINGS") ] - errors = check_logging_settings(None) + errors: List[Error] = check_logging_settings(None) assert any(error.id == "django_logging.E010_EMAIL_SETTINGS" for error in errors) diff --git a/django_logging/tests/settings/test_conf.py b/django_logging/tests/settings/test_conf.py index 75b6046..ebc4708 100644 --- a/django_logging/tests/settings/test_conf.py +++ b/django_logging/tests/settings/test_conf.py @@ -7,7 +7,7 @@ @pytest.fixture -def log_config(): +def log_config() -> LogConfig: """ Fixture to provide a default LogConfig instance. @@ -34,7 +34,7 @@ def log_config(): @pytest.fixture -def log_manager(log_config): +def log_manager(log_config) -> LogManager: """ Fixture to provide a LogManager instance initialized with a LogConfig. @@ -49,7 +49,7 @@ def log_manager(log_config): return LogManager(log_config) -def test_resolve_format(): +def test_resolve_format() -> None: """ Test resolution of format options in LogConfig. @@ -77,7 +77,7 @@ def test_resolve_format(): assert resolved_format == "%(message)s" -def test_remove_ansi_escape_sequences(): +def test_remove_ansi_escape_sequences() -> None: """ Test removal of ANSI escape sequences. @@ -93,7 +93,7 @@ def test_remove_ansi_escape_sequences(): assert clean_string == "ERROR" -def test_create_log_files(log_manager): +def test_create_log_files(log_manager: LogManager) -> None: """ Test creation of log files. @@ -133,7 +133,7 @@ def test_create_log_files(log_manager): log_manager.log_files[log_level] = expected_file_path -def test_set_conf(log_manager): +def test_set_conf(log_manager: LogManager) -> None: """ Test setting up logging configuration. @@ -181,7 +181,7 @@ def test_set_conf(log_manager): assert "disable_existing_loggers" in config -def test_log_manager_get_log_file(log_manager): +def test_log_manager_get_log_file(log_manager: LogManager) -> None: """ Test retrieval of log file paths. diff --git a/django_logging/tests/utils/test_context_manager.py b/django_logging/tests/utils/test_context_manager.py index 9b06c84..92ae466 100644 --- a/django_logging/tests/utils/test_context_manager.py +++ b/django_logging/tests/utils/test_context_manager.py @@ -9,7 +9,7 @@ @pytest.fixture -def mock_logger(): +def mock_logger() -> logging.Logger: """ Fixture to create a mock logger for testing. @@ -26,7 +26,7 @@ def mock_logger(): yield logger -def test_config_setup_auto_initialization_enabled(): +def test_config_setup_auto_initialization_enabled() -> None: """ Test that ValueError is raised when auto-initialization is enabled. @@ -52,7 +52,7 @@ def test_config_setup_auto_initialization_enabled(): ) -def test_config_setup_applies_custom_config(mock_logger): +def test_config_setup_applies_custom_config(mock_logger: logging.Logger) -> None: """ Test that the custom logging configuration is applied. @@ -102,7 +102,7 @@ def test_config_setup_applies_custom_config(mock_logger): mock_log_manager.set_conf.assert_called_once() -def test_config_context_restores_original_config(mock_logger): +def test_config_context_restores_original_config(mock_logger: logging.Logger) -> None: """ Test that the original logging configuration is restored after context exit. @@ -153,7 +153,7 @@ def test_config_context_restores_original_config(mock_logger): assert mock_logger.handlers == original_handlers -def test_restore_logging_config(mock_logger): +def test_restore_logging_config(mock_logger: logging.Logger) -> None: """ Test the _restore_logging_config helper function. diff --git a/django_logging/tests/utils/test_email_notifier.py b/django_logging/tests/utils/test_email_notifier.py index 5220440..3d375a9 100644 --- a/django_logging/tests/utils/test_email_notifier.py +++ b/django_logging/tests/utils/test_email_notifier.py @@ -1,12 +1,12 @@ import logging import pytest import threading -from unittest.mock import patch +from unittest.mock import patch, MagicMock from django_logging.utils.log_email_notifier.notifier import send_email_async @pytest.fixture -def mock_smtp(): +def mock_smtp() -> MagicMock: """ Fixture to mock the SMTP object used for sending emails. @@ -23,7 +23,7 @@ def mock_smtp(): @pytest.fixture -def mock_settings(): +def mock_settings() -> MagicMock: """ Fixture to mock the Django settings used for email configuration. @@ -35,7 +35,9 @@ def mock_settings(): unittest.mock.MagicMock A mock object representing the Django settings with predefined email configurations. """ - with patch("django_logging.utils.log_email_notifier.notifier.settings") as mock_settings: + with patch( + "django_logging.utils.log_email_notifier.notifier.settings" + ) as mock_settings: mock_settings.DEFAULT_FROM_EMAIL = "from@example.com" mock_settings.EMAIL_HOST = "smtp.example.com" mock_settings.EMAIL_PORT = 587 @@ -45,7 +47,7 @@ def mock_settings(): @pytest.fixture -def mock_logger(): +def mock_logger() -> tuple[MagicMock, MagicMock]: """ Fixture to mock the logger used for logging messages. @@ -57,13 +59,19 @@ def mock_logger(): tuple A tuple containing mock objects for `logger.info` and `logger.warning`. """ - with patch("django_logging.utils.log_email_notifier.notifier.logger.info") as mock_info, patch( + with patch( + "django_logging.utils.log_email_notifier.notifier.logger.info" + ) as mock_info, patch( "django_logging.utils.log_email_notifier.notifier.logger.warning" ) as mock_warning: yield mock_info, mock_warning -def test_send_email_async_success(mock_smtp, mock_settings, mock_logger): +def test_send_email_async_success( + mock_smtp: MagicMock, + mock_settings: MagicMock, + mock_logger: tuple[MagicMock, MagicMock], +) -> None: """ Test that the send_email_async function successfully sends an email. @@ -129,7 +137,11 @@ def test_send_email_async_success(mock_smtp, mock_settings, mock_logger): mock_warning.assert_not_called() -def test_send_email_async_failure(mock_smtp, mock_settings, mock_logger): +def test_send_email_async_failure( + mock_smtp: MagicMock, + mock_settings: MagicMock, + mock_logger: tuple[MagicMock, MagicMock], +) -> None: """ Test that the send_email_async function handles SMTP failures. diff --git a/django_logging/tests/utils/test_get_conf.py b/django_logging/tests/utils/test_get_conf.py index 5a68ce0..5b251c9 100644 --- a/django_logging/tests/utils/test_get_conf.py +++ b/django_logging/tests/utils/test_get_conf.py @@ -1,3 +1,5 @@ +from typing import Dict + import pytest from unittest.mock import patch from django.conf import settings @@ -10,7 +12,7 @@ @pytest.fixture -def mock_settings(): +def mock_settings() -> Dict: """ Fixture to mock Django settings. @@ -46,7 +48,7 @@ def mock_settings(): yield mock_settings -def test_get_conf(mock_settings): +def test_get_conf(mock_settings: Dict) -> None: """ Test that the `get_config` function returns the correct configuration values. @@ -80,7 +82,7 @@ def test_get_conf(mock_settings): assert result == expected -def test_use_email_notifier_template(mock_settings): +def test_use_email_notifier_template(mock_settings: Dict) -> None: """ Test that the `use_email_notifier_template` function correctly reads the `USE_TEMPLATE` setting. @@ -105,7 +107,7 @@ def test_use_email_notifier_template(mock_settings): assert use_email_notifier_template() is False -def test_is_auto_initialization_enabled(mock_settings): +def test_is_auto_initialization_enabled(mock_settings: Dict) -> None: """ Test that the `is_auto_initialization_enabled` function correctly reads the `AUTO_INITIALIZATION_ENABLE` setting. @@ -130,7 +132,7 @@ def test_is_auto_initialization_enabled(mock_settings): assert is_auto_initialization_enabled() is False -def test_is_initialization_message_enabled(mock_settings): +def test_is_initialization_message_enabled(mock_settings: Dict) -> None: """ Test that the `is_initialization_message_enabled` function correctly reads the `INITIALIZATION_MESSAGE_ENABLE` setting. diff --git a/django_logging/tests/utils/test_log_and_notify.py b/django_logging/tests/utils/test_log_and_notify.py index 32e75bc..22f5495 100644 --- a/django_logging/tests/utils/test_log_and_notify.py +++ b/django_logging/tests/utils/test_log_and_notify.py @@ -6,7 +6,7 @@ # Helper function to mock LogConfig -def mock_log_config(email_notifier_enable=True): +def mock_log_config(email_notifier_enable: bool = True) -> MagicMock: """ Helper function to create a mock LogConfig object. @@ -27,7 +27,7 @@ def mock_log_config(email_notifier_enable=True): @pytest.fixture -def mock_logger(): +def mock_logger() -> MagicMock: """ Fixture to create a mock logger object for testing. @@ -40,7 +40,7 @@ def mock_logger(): @pytest.fixture -def mock_settings(): +def mock_settings() -> None: """ Fixture to mock Django settings related to email notifications. @@ -56,7 +56,7 @@ def mock_settings(): del settings.ADMIN_EMAIL -def test_log_and_notify_email_notifier_disabled(mock_logger): +def test_log_and_notify_email_notifier_disabled(mock_logger: MagicMock) -> None: """ Test that a ValueError is raised when email notifier is disabled. @@ -80,7 +80,9 @@ def test_log_and_notify_email_notifier_disabled(mock_logger): log_and_notify_admin(mock_logger, logging.ERROR, "Test message") -def test_log_and_notify_admin_success(mock_logger, mock_settings): +def test_log_and_notify_admin_success( + mock_logger: MagicMock, mock_settings: None +) -> None: """ Test successful logging and email notification to admin. @@ -145,7 +147,9 @@ def test_log_and_notify_admin_success(mock_logger, mock_settings): ) -def test_log_and_notify_admin_logging_failure(mock_logger, mock_settings): +def test_log_and_notify_admin_logging_failure( + mock_logger: MagicMock, mock_settings: None +) -> None: """ Test logging failure due to invalid parameters. @@ -185,7 +189,7 @@ def test_log_and_notify_admin_logging_failure(mock_logger, mock_settings): log_and_notify_admin(mock_logger, logging.ERROR, "Test message") -def test_log_and_notify_admin_missing_admin_email(mock_logger): +def test_log_and_notify_admin_missing_admin_email(mock_logger: MagicMock) -> None: """ Test logging and email notification when ADMIN_EMAIL is missing. diff --git a/django_logging/tests/utils/test_set_conf.py b/django_logging/tests/utils/test_set_conf.py index f8cf08a..b18555b 100644 --- a/django_logging/tests/utils/test_set_conf.py +++ b/django_logging/tests/utils/test_set_conf.py @@ -12,13 +12,13 @@ @patch("logging.getLogger") @patch("django_logging.utils.get_conf.get_config") def test_set_config_success( - mock_get_conf, - mock_get_logger, - mock_is_initialization_message_enabled, - mock_is_auto_initialization_enabled, - mock_LogManager, - mock_LogConfig, -): + mock_get_conf: MagicMock, + mock_get_logger: MagicMock, + mock_is_initialization_message_enabled: MagicMock, + mock_is_auto_initialization_enabled: MagicMock, + mock_LogManager: MagicMock, + mock_LogConfig: MagicMock, +) -> None: """ Test the successful execution of set_config. @@ -116,8 +116,10 @@ def test_set_config_success( @patch("django_logging.utils.set_conf.LogManager") @patch("django_logging.utils.set_conf.is_auto_initialization_enabled") def test_set_config_auto_initialization_disabled( - mock_is_auto_initialization_enabled, mock_LogManager, mock_LogConfig -): + mock_is_auto_initialization_enabled: MagicMock, + mock_LogManager: MagicMock, + mock_LogConfig: MagicMock, +) -> None: """ Test that LogConfig and LogManager are not instantiated when auto initialization is disabled. @@ -159,8 +161,10 @@ def test_set_config_auto_initialization_disabled( @patch("django_logging.utils.set_conf.LogManager") @patch("django_logging.utils.set_conf.is_auto_initialization_enabled") def test_set_config_exception_handling( - mock_is_auto_initialization_enabled, mock_LogManager, mock_LogConfig -): + mock_is_auto_initialization_enabled: MagicMock, + mock_LogManager: MagicMock, + mock_LogConfig: MagicMock, +) -> None: """ Test that set_config handles exceptions and logs a warning message. diff --git a/django_logging/tests/validators/test_config_validators.py b/django_logging/tests/validators/test_config_validators.py index 38afd0e..672da60 100644 --- a/django_logging/tests/validators/test_config_validators.py +++ b/django_logging/tests/validators/test_config_validators.py @@ -10,7 +10,7 @@ ) -def test_validate_directory_success(): +def test_validate_directory_success() -> None: """ Test the successful validation of a directory path. @@ -35,7 +35,7 @@ def test_validate_directory_success(): mock_mkdir.assert_called_once() -def test_validate_directory_invalid_path(): +def test_validate_directory_invalid_path() -> None: """ Test validation of invalid directory paths. @@ -61,7 +61,7 @@ def test_validate_directory_invalid_path(): assert errors[0].id == "django_logging.E002_test_directory" -def test_validate_directory_is_file(): +def test_validate_directory_is_file() -> None: """ Test validation when the path is a file, not a directory. @@ -98,7 +98,7 @@ def test_validate_directory_is_file(): assert "Ensure test_directory points to a valid directory." in errors[0].hint -def test_validate_log_levels_success(): +def test_validate_log_levels_success() -> None: """ Test successful validation of log levels. @@ -118,7 +118,7 @@ def test_validate_log_levels_success(): assert not errors -def test_validate_log_levels_invalid_type(): +def test_validate_log_levels_invalid_type() -> None: """ Test validation of log levels with invalid types. @@ -143,7 +143,7 @@ def test_validate_log_levels_invalid_type(): assert errors[0].id == "django_logging.E005_log_levels" -def test_validate_format_string_success(): +def test_validate_format_string_success() -> None: """ Test successful validation of a log format string. @@ -163,7 +163,7 @@ def test_validate_format_string_success(): assert not errors -def test_validate_format_string_invalid(): +def test_validate_format_string_invalid() -> None: """ Test validation of an invalid log format string. @@ -195,7 +195,7 @@ def test_validate_format_string_invalid(): assert errors[0].id == "django_logging.E009_log_format" -def test_validate_format_option_integer_success(): +def test_validate_format_option_integer_success() -> None: """ Test successful validation of an integer format option. @@ -215,7 +215,7 @@ def test_validate_format_option_integer_success(): assert not errors -def test_validate_format_option_failure(): +def test_validate_format_option_failure() -> None: """ Test validation of invalid format options. @@ -242,7 +242,7 @@ def test_validate_format_option_failure(): assert errors[0].id == "django_logging.E013_log_format_option" -def test_validate_format_option_string_success(): +def test_validate_format_option_string_success() -> None: """ Test successful validation of a string format option. @@ -262,7 +262,7 @@ def test_validate_format_option_string_success(): assert not errors -def test_validate_boolean_setting_success(): +def test_validate_boolean_setting_success() -> None: """ Test successful validation of a boolean setting. @@ -281,7 +281,7 @@ def test_validate_boolean_setting_success(): assert not errors -def test_validate_date_format_success(): +def test_validate_date_format_success() -> None: """ Test successful validation of a date format string. @@ -301,7 +301,7 @@ def test_validate_date_format_success(): assert not errors -def test_validate_date_format_invalid_format(): +def test_validate_date_format_invalid_format() -> None: """ Test validation of invalid date format strings. @@ -328,7 +328,7 @@ def test_validate_date_format_invalid_format(): assert errors[0].id == "django_logging.E016_date_format" -def test_validate_email_notifier_success(): +def test_validate_email_notifier_success() -> None: """ Test successful validation of email notifier configuration. @@ -352,7 +352,7 @@ def test_validate_email_notifier_success(): assert not errors -def test_validate_email_notifier_invalid_type(): +def test_validate_email_notifier_invalid_type() -> None: """ Test validation of invalid email notifier configuration types. diff --git a/django_logging/tests/validators/test_email_settings_validator.py b/django_logging/tests/validators/test_email_settings_validator.py index 8d7031c..835dcb7 100644 --- a/django_logging/tests/validators/test_email_settings_validator.py +++ b/django_logging/tests/validators/test_email_settings_validator.py @@ -1,3 +1,5 @@ +from typing import Dict + import pytest from unittest.mock import patch from django.conf import settings @@ -7,7 +9,7 @@ # Mock required email settings for testing @pytest.fixture -def mock_email_settings(): +def mock_email_settings() -> Dict: """ Fixture to mock Django email settings. @@ -31,7 +33,7 @@ def mock_email_settings(): } -def test_check_email_settings_all_present(mock_email_settings): +def test_check_email_settings_all_present(mock_email_settings: Dict) -> None: """ Test validation when all required email settings are present. @@ -75,7 +77,7 @@ def test_check_email_settings_all_present(mock_email_settings): assert not errors # No errors should be present -def test_check_email_settings_missing_some(mock_email_settings): +def test_check_email_settings_missing_some(mock_email_settings: Dict) -> None: """ Test validation when some required email settings are missing. @@ -124,7 +126,7 @@ def test_check_email_settings_missing_some(mock_email_settings): assert errors[0].id == "django_logging.E021" -def test_check_email_settings_all_missing(): +def test_check_email_settings_all_missing() -> None: """ Test validation when all required email settings are missing. From e5ed86e90f4acddd5cddd6b612929576ea60a78c Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Fri, 23 Aug 2024 21:23:38 +0330 Subject: [PATCH 23/30] Update(tests) test_context_manager typo --- django_logging/tests/utils/test_context_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/django_logging/tests/utils/test_context_manager.py b/django_logging/tests/utils/test_context_manager.py index 92ae466..2467b5a 100644 --- a/django_logging/tests/utils/test_context_manager.py +++ b/django_logging/tests/utils/test_context_manager.py @@ -43,8 +43,7 @@ def test_config_setup_auto_initialization_enabled() -> None: return_value=True, ): with pytest.raises(ValueError) as excinfo: - with config_setup(): - """""" + with config_setup(): "" assert ( str(excinfo.value) From 0dedcce5df5463fd57594feac4b2e3dbcc6561a7 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Sat, 24 Aug 2024 14:39:48 +0330 Subject: [PATCH 24/30] :zap: Update(formatter) rename ColoredFormatter and usages --- django_logging/formatters/__init__.py | 2 +- .../formatters/colored_formatter.py | 2 +- django_logging/settings/conf.py | 2 +- .../formatters/test_colored_formatter.py | 26 +++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/django_logging/formatters/__init__.py b/django_logging/formatters/__init__.py index 17a440c..cd8b46e 100644 --- a/django_logging/formatters/__init__.py +++ b/django_logging/formatters/__init__.py @@ -1 +1 @@ -from .colored_formatter import ColorizedFormatter +from .colored_formatter import ColoredFormatter diff --git a/django_logging/formatters/colored_formatter.py b/django_logging/formatters/colored_formatter.py index 37ef585..d82413e 100644 --- a/django_logging/formatters/colored_formatter.py +++ b/django_logging/formatters/colored_formatter.py @@ -3,7 +3,7 @@ from django_logging.utils.console_colorizer import colorize_log_format -class ColorizedFormatter(logging.Formatter): +class ColoredFormatter(logging.Formatter): def format(self, record): original_format = self._style._fmt diff --git a/django_logging/settings/conf.py b/django_logging/settings/conf.py index bdfb24d..449720a 100644 --- a/django_logging/settings/conf.py +++ b/django_logging/settings/conf.py @@ -189,7 +189,7 @@ def set_conf(self) -> None: } if self.log_config.colorize_console: formatters["console"].update( - {"()": "django_logging.formatters.ColorizedFormatter"} + {"()": "django_logging.formatters.ColoredFormatter"} ) formatters["email"] = { diff --git a/django_logging/tests/formatters/test_colored_formatter.py b/django_logging/tests/formatters/test_colored_formatter.py index a65a18e..b268937 100644 --- a/django_logging/tests/formatters/test_colored_formatter.py +++ b/django_logging/tests/formatters/test_colored_formatter.py @@ -1,7 +1,7 @@ import logging import pytest from unittest.mock import patch, MagicMock -from django_logging.formatters import ColorizedFormatter +from django_logging.formatters import ColoredFormatter @pytest.fixture @@ -26,16 +26,16 @@ def log_record() -> logging.LogRecord: @pytest.fixture -def formatter() -> ColorizedFormatter: +def formatter() -> ColoredFormatter: """ - Fixture to create a `ColorizedFormatter` instance with a specific format. + Fixture to create a `ColoredFormatter` instance with a specific format. Returns: ------- - ColorizedFormatter - An instance of `ColorizedFormatter` with a predefined format. + ColoredFormatter + An instance of `ColoredFormatter` with a predefined format. """ - return ColorizedFormatter(fmt="%(levelname)s: %(message)s") + return ColoredFormatter(fmt="%(levelname)s: %(message)s") @patch("django_logging.formatters.colored_formatter.colorize_log_format", autospec=True) @@ -44,10 +44,10 @@ def formatter() -> ColorizedFormatter: side_effect=lambda fmt: fmt, ) def test_format_applies_colorization( - mock_remove_ansi: MagicMock, mock_colorize: MagicMock, formatter: ColorizedFormatter, log_record: logging.LogRecord + mock_remove_ansi: MagicMock, mock_colorize: MagicMock, formatter: ColoredFormatter, log_record: logging.LogRecord ) -> None: """ - Test that the `format` method of `ColorizedFormatter` applies colorization. + Test that the `format` method of `ColoredFormatter` applies colorization. This test verifies that the `format` method calls the `colorize_log_format` function to apply colorization based on the log level. @@ -58,7 +58,7 @@ def test_format_applies_colorization( Mock for `remove_ansi_escape_sequences`. mock_colorize : MagicMock Mock for `colorize_log_format`. - formatter : ColorizedFormatter + formatter : ColoredFormatter The formatter instance being tested. log_record : logging.LogRecord The dummy log record created by the fixture. @@ -83,7 +83,7 @@ def test_format_applies_colorization( side_effect=lambda fmt: fmt, ) def test_format_resets_to_original_format( - mock_remove_ansi: MagicMock, formatter: ColorizedFormatter, log_record: logging.LogRecord) -> None: + mock_remove_ansi: MagicMock, formatter: ColoredFormatter, log_record: logging.LogRecord) -> None: """ Test that the `format` method resets the format string to its original state after formatting. @@ -94,7 +94,7 @@ def test_format_resets_to_original_format( ---------- mock_remove_ansi : MagicMock Mock for `remove_ansi_escape_sequences`. - formatter : ColorizedFormatter + formatter : ColoredFormatter The formatter instance being tested. log_record : logging.LogRecord The dummy log record created by the fixture. @@ -114,7 +114,7 @@ def test_format_resets_to_original_format( "django_logging.settings.conf.LogConfig.remove_ansi_escape_sequences", side_effect=lambda fmt: fmt, ) -def test_format_returns_formatted_output(formatter: ColorizedFormatter, log_record: logging.LogRecord) -> None: +def test_format_returns_formatted_output(formatter: ColoredFormatter, log_record: logging.LogRecord) -> None: """ Test that the `format` method returns the correctly formatted log output. @@ -123,7 +123,7 @@ def test_format_returns_formatted_output(formatter: ColorizedFormatter, log_reco Parameters: ---------- - formatter : ColorizedFormatter + formatter : ColoredFormatter The formatter instance being tested. log_record : logging.LogRecord The dummy log record created by the fixture. From 07b88ebf231ec2f086cc0068a8c3239237166bda Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Sat, 24 Aug 2024 19:27:55 +0330 Subject: [PATCH 25/30] :zap: Update(constants) default_settings type annotations --- django_logging/constants/default_settings.py | 35 ++++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/django_logging/constants/default_settings.py b/django_logging/constants/default_settings.py index 927b9a7..725e4a1 100644 --- a/django_logging/constants/default_settings.py +++ b/django_logging/constants/default_settings.py @@ -1,4 +1,5 @@ import os +from typing import cast from dataclasses import dataclass, field from django_logging.constants.config_types import ( @@ -22,23 +23,29 @@ class DefaultLoggingSettings: auto_initialization_enable: bool = True initialization_message_enable: bool = True log_file_formats: LogFileFormatsType = field( - default_factory=lambda: { - "DEBUG": 1, - "INFO": 1, - "WARNING": 1, - "ERROR": 1, - "CRITICAL": 1, - } + default_factory=lambda: cast( + LogFileFormatsType, + { + "DEBUG": 1, + "INFO": 1, + "WARNING": 1, + "ERROR": 1, + "CRITICAL": 1, + }, + ) ) log_console_level: LogLevel = "DEBUG" log_console_format: FormatOption = 1 log_console_colorize: bool = True log_email_notifier: LogEmailNotifierType = field( - default_factory=lambda: { - "ENABLE": False, - "NOTIFY_ERROR": False, - "NOTIFY_CRITICAL": False, - "LOG_FORMAT": 1, - "USE_TEMPLATE": True, - } + default_factory=lambda: cast( + LogEmailNotifierType, + { + "ENABLE": False, + "NOTIFY_ERROR": False, + "NOTIFY_CRITICAL": False, + "LOG_FORMAT": 1, + "USE_TEMPLATE": True, + }, + ) ) From 151e73e82e6c63814c2bb94d794b1b09b1a1b0f8 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Sat, 24 Aug 2024 19:33:22 +0330 Subject: [PATCH 26/30] :zap: Update formatters and handlers type annotations --- django_logging/formatters/colored_formatter.py | 6 +++--- django_logging/handlers/email_handler.py | 18 ++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/django_logging/formatters/colored_formatter.py b/django_logging/formatters/colored_formatter.py index d82413e..f14d228 100644 --- a/django_logging/formatters/colored_formatter.py +++ b/django_logging/formatters/colored_formatter.py @@ -1,10 +1,10 @@ -import logging +from logging import LogRecord, Formatter from django_logging.settings.conf import LogConfig from django_logging.utils.console_colorizer import colorize_log_format -class ColoredFormatter(logging.Formatter): - def format(self, record): +class ColoredFormatter(Formatter): + def format(self, record: LogRecord) -> str: original_format = self._style._fmt # checks that the format does not have any color it's self diff --git a/django_logging/handlers/email_handler.py b/django_logging/handlers/email_handler.py index 379b66c..8011f5b 100644 --- a/django_logging/handlers/email_handler.py +++ b/django_logging/handlers/email_handler.py @@ -1,6 +1,8 @@ -import logging +from logging import Handler, LogRecord +from typing import Optional from django.conf import settings +from django.http import HttpRequest from django.template import engines from django.utils.timezone import now from django_logging.utils.log_email_notifier.notifier import send_email_async @@ -8,17 +10,13 @@ from django_logging.middleware import RequestLogMiddleware -class EmailHandler(logging.Handler): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.include_html = use_email_notifier_template() - - def emit(self, record): +class EmailHandler(Handler): + def emit(self, record: LogRecord) -> None: try: request = getattr(record, "request", None) log_entry = self.format(record) - if self.include_html: + if use_email_notifier_template(): email_body = self.render_template(log_entry, request) else: email_body = log_entry @@ -31,8 +29,8 @@ def emit(self, record): @staticmethod def render_template( - log_entry, request=None, template_path="email_notifier_template.html" - ): + log_entry: str, request: Optional[HttpRequest] = None, template_path: str = "email_notifier_template.html" + ) -> str: django_engine = engines["django"] template = django_engine.get_template(template_path) From af542d11d8c47d41aa6dda3bcad9006c3f67d852 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Sat, 24 Aug 2024 19:35:01 +0330 Subject: [PATCH 27/30] :zap: Update(commands) send_logs command Type Annotations --- django_logging/management/commands/send_logs.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/django_logging/management/commands/send_logs.py b/django_logging/management/commands/send_logs.py index b390a08..3f92e9a 100644 --- a/django_logging/management/commands/send_logs.py +++ b/django_logging/management/commands/send_logs.py @@ -2,6 +2,8 @@ import shutil import tempfile import logging +from argparse import ArgumentParser +from typing import Dict, Tuple from django.core.exceptions import ImproperlyConfigured from django.core.mail import EmailMessage @@ -27,7 +29,7 @@ class Command(BaseCommand): help = "Send log folder to the specified email address" - def add_arguments(self, parser): + def add_arguments(self, parser: ArgumentParser) -> None: """ Add custom command arguments. @@ -38,7 +40,7 @@ def add_arguments(self, parser): "email", type=str, help="The email address to send the logs to" ) - def handle(self, *args, **kwargs): + def handle(self, *args: Tuple, **kwargs: Dict) -> None: """ The main entry point for the command. @@ -95,7 +97,7 @@ def handle(self, *args, **kwargs): os.remove(zip_path) logger.info("Temporary zip file cleaned up successfully.") - def validate_email_settings(self): + def validate_email_settings(self) -> None: """ Check if all required email settings are present in the settings file. From 50a5169897ed2fd9e5fafee7f69a97e4d711115a Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Sat, 24 Aug 2024 19:36:53 +0330 Subject: [PATCH 28/30] :zap: Update(utils) Improve Type Annotations and compatibility --- django_logging/utils/console_colorizer.py | 3 ++- django_logging/utils/context_manager.py | 10 +++++----- .../utils/log_email_notifier/log_and_notify.py | 6 +++--- django_logging/utils/log_email_notifier/notifier.py | 10 ++++++++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/django_logging/utils/console_colorizer.py b/django_logging/utils/console_colorizer.py index 2acc27f..a540cf7 100644 --- a/django_logging/utils/console_colorizer.py +++ b/django_logging/utils/console_colorizer.py @@ -1,7 +1,8 @@ from django_logging.constants.ansi_colors import LOG_LEVEL_COLORS, AnsiColors +from django_logging.constants.config_types import LogLevel -def colorize_log_format(log_format, levelname): +def colorize_log_format(log_format: str, levelname: str) -> str: color_mapping = { "%(asctime)s": f"{AnsiColors.CYAN}%(asctime)s{AnsiColors.RESET}", "%(created)f": f"{AnsiColors.BRIGHT_BLUE}%(created)f{AnsiColors.RESET}", diff --git a/django_logging/utils/context_manager.py b/django_logging/utils/context_manager.py index 1d73f73..9a7a055 100644 --- a/django_logging/utils/context_manager.py +++ b/django_logging/utils/context_manager.py @@ -1,6 +1,6 @@ from contextlib import contextmanager -from logging import getLogger, Logger -from typing import Dict +from logging import getLogger, Logger, PlaceHolder +from typing import Dict, Iterator, Union from django.conf import settings from django_logging.settings.conf import LogConfig, LogManager @@ -8,7 +8,7 @@ @contextmanager -def config_setup() -> LogManager: +def config_setup() -> Iterator[LogManager]: """ Context manager to temporarily apply a custom logging configuration. @@ -44,7 +44,7 @@ def config_setup() -> LogManager: def _restore_logging_config( logger: Logger, - original_config: Dict[str, Logger], + original_config: Dict[str, Union[Logger, PlaceHolder]], original_level: int, original_handlers: list, ) -> None: @@ -53,7 +53,7 @@ def _restore_logging_config( Args: logger (Logger): The root logger instance. - original_config (Dict[str, Logger]): The original logger dictionary. + original_config (Dict[str, Logger | PlaceHolder]): The original logger dictionary. original_level (int): The original root logger level. original_handlers (list): The original root logger handlers. """ diff --git a/django_logging/utils/log_email_notifier/log_and_notify.py b/django_logging/utils/log_email_notifier/log_and_notify.py index d42f819..fb26557 100644 --- a/django_logging/utils/log_email_notifier/log_and_notify.py +++ b/django_logging/utils/log_email_notifier/log_and_notify.py @@ -1,6 +1,6 @@ import logging import inspect -from typing import Optional, Dict +from typing import Optional, Dict, Any from django.conf import settings @@ -12,7 +12,7 @@ def log_and_notify_admin( - logger, level: int, message: str, extra: Optional[Dict] = None + logger: logging.Logger, level: int, message: str, extra: Optional[Dict[str, Any]] = None ) -> None: # Get the caller's frame to capture the correct module, file, and line number frame = inspect.currentframe().f_back @@ -39,7 +39,7 @@ def log_and_notify_admin( fn=frame.f_code.co_filename, lno=frame.f_lineno, msg=message, - args=None, + args=(), exc_info=None, func=frame.f_code.co_name, extra=extra, diff --git a/django_logging/utils/log_email_notifier/notifier.py b/django_logging/utils/log_email_notifier/notifier.py index 5bca62f..c721814 100644 --- a/django_logging/utils/log_email_notifier/notifier.py +++ b/django_logging/utils/log_email_notifier/notifier.py @@ -1,5 +1,6 @@ import logging import threading +from typing import List, Optional from smtplib import SMTP from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart @@ -9,8 +10,13 @@ logger = logging.getLogger(__name__) -def send_email_async(subject, body, recipient_list, event=None): - def send_email(): +def send_email_async( + subject: str, + body: str, + recipient_list: List[str], + event: Optional[threading.Event] = None, +) -> None: + def send_email() -> None: msg = MIMEMultipart() msg["From"] = settings.DEFAULT_FROM_EMAIL msg["To"] = ", ".join(recipient_list) From 6bd42762efc4b198e6a5d35035a39edb3d3fb960 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Sat, 24 Aug 2024 19:38:56 +0330 Subject: [PATCH 29/30] :zap: Update(validators) Improve Type Annotations for args and return types --- django_logging/validators/config_validators.py | 4 ++-- django_logging/validators/email_settings_validator.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/django_logging/validators/config_validators.py b/django_logging/validators/config_validators.py index fe5d294..64f0657 100644 --- a/django_logging/validators/config_validators.py +++ b/django_logging/validators/config_validators.py @@ -160,7 +160,7 @@ def validate_format_option( def validate_boolean_setting(value: bool, config_name: str) -> List[Error]: - errors = [] + errors: List[Error] = [] if not isinstance(value, bool): errors.append( Error( @@ -214,7 +214,7 @@ def validate_email_notifier(notifier_config: LogEmailNotifierType) -> List[Error bool_attrs = ["ENABLE", "NOTIFY_ERROR", "NOTIFY_CRITICAL", "USE_TEMPLATE"] if expected_type is bool and key in bool_attrs: - errors.extend(validate_boolean_setting(value, config_name)) + errors.extend(validate_boolean_setting(bool(value), config_name)) elif isinstance(value, (int, str)) and key == "LOG_FORMAT": errors.extend(validate_format_option(value, config_name)) diff --git a/django_logging/validators/email_settings_validator.py b/django_logging/validators/email_settings_validator.py index a835240..d988368 100644 --- a/django_logging/validators/email_settings_validator.py +++ b/django_logging/validators/email_settings_validator.py @@ -7,7 +7,7 @@ ) -def check_email_settings(require_admin_email=True) -> List[Error]: +def check_email_settings(require_admin_email: bool = True) -> List[Error]: """ Check if all required email settings are present in the settings file. From 43a80a7b1061606b8a24ebcad5593830dcaa4040 Mon Sep 17 00:00:00 2001 From: MEHRSHAD MIRSHEKARY Date: Sat, 24 Aug 2024 19:41:37 +0330 Subject: [PATCH 30/30] :zap: Update(tests) Improve Type Annotations in methods args and returns --- .../tests/commands/test_send_logs.py | 2 +- .../tests/handlers/test_email_handler.py | 23 +++++++++++++++---- .../middleware/test_request_middleware.py | 7 +++--- django_logging/tests/settings/test_conf.py | 6 ++--- .../tests/utils/test_context_manager.py | 3 ++- .../tests/utils/test_email_notifier.py | 8 ++++--- django_logging/tests/utils/test_get_conf.py | 4 ++-- .../tests/utils/test_log_and_notify.py | 4 +++- .../validators/test_config_validators.py | 22 ++++++++++-------- 9 files changed, 51 insertions(+), 28 deletions(-) diff --git a/django_logging/tests/commands/test_send_logs.py b/django_logging/tests/commands/test_send_logs.py index 88bcff9..3c3e67f 100644 --- a/django_logging/tests/commands/test_send_logs.py +++ b/django_logging/tests/commands/test_send_logs.py @@ -175,7 +175,7 @@ def test_validate_email_settings_failure( ) @patch("django_logging.management.commands.send_logs.shutil.make_archive") def test_cleanup_on_failure( - self, mock_make_archive, mock_validate_email_settings: Mock + self, mock_make_archive: Mock, mock_validate_email_settings: Mock ) -> None: """ Test that the `send_logs` command cleans up any partially created files when an error occurs diff --git a/django_logging/tests/handlers/test_email_handler.py b/django_logging/tests/handlers/test_email_handler.py index c5179af..a97bc52 100644 --- a/django_logging/tests/handlers/test_email_handler.py +++ b/django_logging/tests/handlers/test_email_handler.py @@ -48,7 +48,11 @@ def email_handler() -> EmailHandler: return_value=True, ) def test_emit_with_html_template( - mock_use_template: MagicMock, mock_render_template: MagicMock, mock_send_email: MagicMock, email_handler: EmailHandler, log_record: logging.LogRecord + mock_use_template: MagicMock, + mock_render_template: MagicMock, + mock_send_email: MagicMock, + email_handler: EmailHandler, + log_record: logging.LogRecord, ) -> None: """ Test the emit method when HTML templates are used. @@ -89,7 +93,11 @@ def test_emit_with_html_template( "django_logging.handlers.email_handler.use_email_notifier_template", return_value=False, ) -def test_emit_without_html_template(mock_use_template: MagicMock, mock_send_email: MagicMock, log_record: logging.LogRecord) -> None: +def test_emit_without_html_template( + mock_use_template: MagicMock, + mock_send_email: MagicMock, + log_record: logging.LogRecord, +) -> None: """ Test the emit method when HTML templates are not used. @@ -123,7 +131,10 @@ def test_emit_without_html_template(mock_use_template: MagicMock, mock_send_emai side_effect=Exception("Email send failed"), ) def test_emit_handles_exception( - mock_send_email: MagicMock, mock_handle_error: MagicMock, email_handler: EmailHandler, log_record: logging.LogRecord + mock_send_email: MagicMock, + mock_handle_error: MagicMock, + email_handler: EmailHandler, + log_record: logging.LogRecord, ) -> None: """ Test that the emit method handles exceptions during email sending. @@ -160,7 +171,11 @@ def test_emit_handles_exception( return_value="Mozilla/5.0", ) @patch("django_logging.handlers.email_handler.engines") -def test_render_template(mock_engines: MagicMock, mock_get_user_agent: MagicMock, mock_get_ip_address: MagicMock): +def test_render_template( + mock_engines: MagicMock, + mock_get_user_agent: MagicMock, + mock_get_ip_address: MagicMock, +) -> None: """ Test the render_template method of EmailHandler. diff --git a/django_logging/tests/middleware/test_request_middleware.py b/django_logging/tests/middleware/test_request_middleware.py index 2f1ceb4..f375ddf 100644 --- a/django_logging/tests/middleware/test_request_middleware.py +++ b/django_logging/tests/middleware/test_request_middleware.py @@ -1,4 +1,5 @@ import logging +from typing import Callable from unittest.mock import Mock import pytest @@ -23,7 +24,7 @@ def request_factory() -> RequestFactory: @pytest.fixture -def get_response() -> callable: +def get_response() -> Callable: """ Fixture to create a mock get_response function. @@ -33,14 +34,14 @@ def get_response() -> callable: A function that returns an HttpResponse with a dummy response. """ - def _get_response(request) -> HttpResponse: + def _get_response(request: RequestFactory) -> HttpResponse: return HttpResponse("Test Response") return _get_response @pytest.fixture -def middleware(get_response: callable) -> RequestLogMiddleware: +def middleware(get_response: Callable) -> RequestLogMiddleware: """ Fixture to create an instance of RequestLogMiddleware. diff --git a/django_logging/tests/settings/test_conf.py b/django_logging/tests/settings/test_conf.py index ebc4708..12f0c2c 100644 --- a/django_logging/tests/settings/test_conf.py +++ b/django_logging/tests/settings/test_conf.py @@ -22,7 +22,7 @@ def log_config() -> LogConfig: return LogConfig( log_levels=["INFO", "WARNING", "ERROR"], log_dir="/tmp/logs", - log_file_formats={"INFO": 1, "WARNING": None, "ERROR": "%(message)s"}, + log_file_formats={"INFO": 1, "WARNING": None, "ERROR": "%(message)s"}, # type: ignore console_level="INFO", console_format=1, colorize_console=False, @@ -34,7 +34,7 @@ def log_config() -> LogConfig: @pytest.fixture -def log_manager(log_config) -> LogManager: +def log_manager(log_config: LogConfig) -> LogManager: """ Fixture to provide a LogManager instance initialized with a LogConfig. @@ -65,7 +65,7 @@ def test_resolve_format() -> None: - String formats resolve to themselves. """ resolved_format_option = LogConfig.resolve_format(1, use_colors=False) - resolved_none_format = LogConfig.resolve_format(None, use_colors=False) + resolved_none_format = LogConfig.resolve_format(None, use_colors=False) # type: ignore assert resolved_format_option == FORMAT_OPTIONS[1] assert resolved_none_format diff --git a/django_logging/tests/utils/test_context_manager.py b/django_logging/tests/utils/test_context_manager.py index 2467b5a..c4fc81d 100644 --- a/django_logging/tests/utils/test_context_manager.py +++ b/django_logging/tests/utils/test_context_manager.py @@ -1,4 +1,5 @@ import logging +from typing import Generator from unittest import mock import pytest @@ -9,7 +10,7 @@ @pytest.fixture -def mock_logger() -> logging.Logger: +def mock_logger() -> Generator[logging.Logger, None, None]: """ Fixture to create a mock logger for testing. diff --git a/django_logging/tests/utils/test_email_notifier.py b/django_logging/tests/utils/test_email_notifier.py index 3d375a9..4fedead 100644 --- a/django_logging/tests/utils/test_email_notifier.py +++ b/django_logging/tests/utils/test_email_notifier.py @@ -1,4 +1,6 @@ import logging +from typing import Generator + import pytest import threading from unittest.mock import patch, MagicMock @@ -6,7 +8,7 @@ @pytest.fixture -def mock_smtp() -> MagicMock: +def mock_smtp() -> Generator[MagicMock, None, None]: """ Fixture to mock the SMTP object used for sending emails. @@ -23,7 +25,7 @@ def mock_smtp() -> MagicMock: @pytest.fixture -def mock_settings() -> MagicMock: +def mock_settings() -> Generator[MagicMock, None, None]: """ Fixture to mock the Django settings used for email configuration. @@ -47,7 +49,7 @@ def mock_settings() -> MagicMock: @pytest.fixture -def mock_logger() -> tuple[MagicMock, MagicMock]: +def mock_logger() -> Generator[tuple[MagicMock, MagicMock], None, None]: """ Fixture to mock the logger used for logging messages. diff --git a/django_logging/tests/utils/test_get_conf.py b/django_logging/tests/utils/test_get_conf.py index 5b251c9..357a24e 100644 --- a/django_logging/tests/utils/test_get_conf.py +++ b/django_logging/tests/utils/test_get_conf.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, Generator import pytest from unittest.mock import patch @@ -12,7 +12,7 @@ @pytest.fixture -def mock_settings() -> Dict: +def mock_settings() -> Generator[Dict, None, None]: """ Fixture to mock Django settings. diff --git a/django_logging/tests/utils/test_log_and_notify.py b/django_logging/tests/utils/test_log_and_notify.py index 22f5495..1df982d 100644 --- a/django_logging/tests/utils/test_log_and_notify.py +++ b/django_logging/tests/utils/test_log_and_notify.py @@ -1,4 +1,6 @@ import logging +from typing import Generator + import pytest from unittest.mock import patch, MagicMock from django.conf import settings @@ -40,7 +42,7 @@ def mock_logger() -> MagicMock: @pytest.fixture -def mock_settings() -> None: +def mock_settings() -> Generator[None, None, None]: """ Fixture to mock Django settings related to email notifications. diff --git a/django_logging/tests/validators/test_config_validators.py b/django_logging/tests/validators/test_config_validators.py index 672da60..3c531c7 100644 --- a/django_logging/tests/validators/test_config_validators.py +++ b/django_logging/tests/validators/test_config_validators.py @@ -1,4 +1,6 @@ from unittest.mock import patch + +from django_logging.constants.config_types import LogLevels from django_logging.validators.config_validators import ( validate_directory, validate_log_levels, @@ -52,7 +54,7 @@ def test_validate_directory_invalid_path() -> None: """ with patch("os.path.exists") as mock_exists: mock_exists.return_value = False - errors = validate_directory(None, "test_directory") + errors = validate_directory(None, "test_directory") # type: ignore assert len(errors) == 1 assert errors[0].id == "django_logging.E001_test_directory" @@ -113,7 +115,7 @@ def test_validate_log_levels_success() -> None: ------- - No errors are returned for valid log levels. """ - valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + valid_levels: LogLevels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] errors = validate_log_levels(["DEBUG", "INFO"], "log_levels", valid_levels) assert not errors @@ -133,8 +135,8 @@ def test_validate_log_levels_invalid_type() -> None: ------- - Appropriate errors are returned for invalid log level types. """ - valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] - errors = validate_log_levels("DEBUG, INFO", "log_levels", valid_levels) + valid_levels: LogLevels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + errors = validate_log_levels("DEBUG, INFO", "log_levels", valid_levels) # type: ignore assert len(errors) == 1 assert errors[0].id == "django_logging.E004_log_levels" @@ -185,7 +187,7 @@ def test_validate_format_string_invalid() -> None: assert errors[0].id == "django_logging.E011_log_format" format_str = tuple() # invalid type - errors = validate_format_string(format_str, "log_format") + errors = validate_format_string(format_str, "log_format") # type: ignore assert len(errors) == 1 assert errors[0].id == "django_logging.E008_log_format" @@ -220,7 +222,7 @@ def test_validate_format_option_failure() -> None: Test validation of invalid format options. This test verifies that `validate_format_option` returns appropriate errors - when provided with invalid format options, such as integers outside of a valid + when provided with invalid format options, such as integers that not in a valid range or non-integer values. Mocks: @@ -237,7 +239,7 @@ def test_validate_format_option_failure() -> None: assert errors[0].id == "django_logging.E012_log_format_option" format_option = 1.5 - errors = validate_format_option(format_option, "log_format_option") + errors = validate_format_option(format_option, "log_format_option") # type: ignore assert len(errors) == 1 assert errors[0].id == "django_logging.E013_log_format_option" @@ -318,7 +320,7 @@ def test_validate_date_format_invalid_format() -> None: - Appropriate errors are returned for invalid date format strings. """ date_format = 1 # invalid type - errors = validate_date_format(date_format, "date_format") + errors = validate_date_format(date_format, "date_format") # type: ignore assert len(errors) == 1 assert errors[0].id == "django_logging.E015_date_format" @@ -368,11 +370,11 @@ def test_validate_email_notifier_invalid_type() -> None: - Appropriate errors are returned for invalid configuration types. """ notifier_config = ["ENABLE", "LOG_FORMAT"] - errors = validate_email_notifier(notifier_config) + errors = validate_email_notifier(notifier_config) # type: ignore assert len(errors) == 1 assert errors[0].id == "django_logging.E017_LOG_EMAIL_NOTIFIER" notifier_config = {"ENABLE": "true", "LOG_FORMAT": "%(asctime)s - %(message)s"} - errors = validate_email_notifier(notifier_config) + errors = validate_email_notifier(notifier_config) # type: ignore assert len(errors) == 1 assert errors[0].id == "django_logging.E018_LOG_EMAIL_NOTIFIER['ENABLE']"