"""Unit tests for the infrastructure logger module. Tests setup_logging and get_logger from src/infrastructure/logging/logger.py. """ import logging from pathlib import Path from unittest.mock import patch import pytest from src.infrastructure.logging.logger import get_logger, setup_logging class TestSetupLogging: """Tests for setup_logging function.""" def test_returns_logger_instance(self, tmp_path): """setup_logging returns a logging.Logger.""" logger = setup_logging(log_dir=tmp_path) assert isinstance(logger, logging.Logger) def test_logger_name_is_aniworld(self, tmp_path): """Returned logger has name 'aniworld'.""" logger = setup_logging(log_dir=tmp_path) assert logger.name == "aniworld" def test_creates_log_directory(self, tmp_path): """Log directory is created if it does not exist.""" log_dir = tmp_path / "new_logs" setup_logging(log_dir=log_dir) assert log_dir.is_dir() def test_creates_log_file(self, tmp_path): """A log file is created in the log directory.""" setup_logging(log_dir=tmp_path) assert (tmp_path / "fastapi_app.log").exists() def test_custom_log_file_name(self, tmp_path): """Custom log file name is used.""" setup_logging(log_file="custom.log", log_dir=tmp_path) assert (tmp_path / "custom.log").exists() def test_default_log_level_is_info(self, tmp_path): """Default log level falls back to INFO.""" with patch("src.infrastructure.logging.logger.settings") as mock_settings: mock_settings.log_level = None logger = setup_logging(log_dir=tmp_path) assert logger.getEffectiveLevel() == logging.INFO def test_custom_log_level_debug(self, tmp_path): """Custom log level DEBUG is applied.""" logger = setup_logging(log_level="DEBUG", log_dir=tmp_path) assert logger.getEffectiveLevel() == logging.DEBUG def test_custom_log_level_warning(self, tmp_path): """Custom log level WARNING is applied.""" logger = setup_logging(log_level="WARNING", log_dir=tmp_path) assert logger.getEffectiveLevel() == logging.WARNING def test_log_level_case_insensitive(self, tmp_path): """Log level is case-insensitive.""" logger = setup_logging(log_level="debug", log_dir=tmp_path) assert logger.getEffectiveLevel() == logging.DEBUG def test_root_logger_has_two_handlers(self, tmp_path): """Root logger gets exactly console + file handlers.""" setup_logging(log_dir=tmp_path) root = logging.getLogger() # Should be at least 2 handlers (console + file) handler_types = {type(h).__name__ for h in root.handlers} assert "StreamHandler" in handler_types assert "FileHandler" in handler_types def test_clears_existing_root_handlers(self, tmp_path): """Existing root handlers are cleared to avoid duplicates.""" root = logging.getLogger() root.addHandler(logging.StreamHandler()) initial_count = len(root.handlers) setup_logging(log_dir=tmp_path) # After setup, should have exactly 2 (console + file) assert len(root.handlers) == 2 def test_writes_to_log_file(self, tmp_path): """Messages are written to the log file.""" logger = setup_logging(log_dir=tmp_path) logger.info("test message for file") # Flush handlers for handler in logging.getLogger().handlers: handler.flush() log_content = (tmp_path / "fastapi_app.log").read_text() assert "test message for file" in log_content def test_log_format_includes_timestamp(self, tmp_path): """File log entries include a timestamp.""" logger = setup_logging(log_dir=tmp_path) logger.info("timestamp check") for handler in logging.getLogger().handlers: handler.flush() log_content = (tmp_path / "fastapi_app.log").read_text() # Detailed formatter includes date assert "-" in log_content # At minimum YYYY-MM-DD format def test_log_format_includes_level_name(self, tmp_path): """File log entries include the log level name.""" logger = setup_logging(log_dir=tmp_path) logger.warning("level check") for handler in logging.getLogger().handlers: handler.flush() log_content = (tmp_path / "fastapi_app.log").read_text() assert "WARNING" in log_content def test_startup_banner_logged(self, tmp_path): """Startup banner with log config info is written.""" setup_logging(log_dir=tmp_path) for handler in logging.getLogger().handlers: handler.flush() log_content = (tmp_path / "fastapi_app.log").read_text() assert "Logging configured successfully" in log_content def test_level_from_settings(self, tmp_path): """Falls back to settings.log_level when no explicit level.""" with patch("src.infrastructure.logging.logger.settings") as mock_settings: mock_settings.log_level = "ERROR" logger = setup_logging(log_dir=tmp_path) assert logger.getEffectiveLevel() == logging.ERROR def test_invalid_log_level_defaults_to_info(self, tmp_path): """Invalid log level string falls back to INFO.""" logger = setup_logging(log_level="INVALID_LEVEL", log_dir=tmp_path) assert logger.getEffectiveLevel() == logging.INFO class TestGetLogger: """Tests for get_logger function.""" def test_returns_logger_with_given_name(self): """get_logger returns a logger with the requested name.""" logger = get_logger("my_module") assert logger.name == "my_module" def test_returns_same_logger_for_same_name(self): """Calling get_logger twice with same name returns same object.""" a = get_logger("shared_name") b = get_logger("shared_name") assert a is b def test_hierarchical_name(self): """Dotted names produce hierarchical loggers.""" parent = get_logger("app") child = get_logger("app.sub") assert child.parent is parent or child.parent.name == "app" def test_logger_is_standard_logging(self): """Returned object is a standard logging.Logger.""" logger = get_logger("test_std") assert isinstance(logger, logging.Logger)