Task 5: Add Infrastructure Logging tests (49 tests)

- test_infrastructure_logger.py: 21 tests for setup_logging (log levels,
  file creation, handlers, formatters, startup banner) and get_logger
- test_uvicorn_logging_config.py: 28 tests for LOGGING_CONFIG structure,
  formatters, handlers, logger definitions, paths, and get_uvicorn_log_config
This commit is contained in:
2026-02-07 18:24:32 +01:00
parent 5b3fbf36b9
commit 9275747b6d
2 changed files with 327 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
"""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)

View File

@@ -0,0 +1,170 @@
"""Unit tests for the uvicorn logging configuration module.
Tests LOGGING_CONFIG dict and get_uvicorn_log_config from
src/infrastructure/logging/uvicorn_config.py.
"""
import logging
import logging.config
import pytest
from src.infrastructure.logging.uvicorn_config import (
LOGGING_CONFIG,
LOG_FILE,
LOGS_DIR,
get_uvicorn_log_config,
)
class TestLoggingConfigStructure:
"""Tests for the LOGGING_CONFIG dict schema."""
def test_version_is_one(self):
"""Logging config version must be 1."""
assert LOGGING_CONFIG["version"] == 1
def test_disable_existing_loggers_false(self):
"""Existing loggers should not be disabled."""
assert LOGGING_CONFIG["disable_existing_loggers"] is False
def test_has_formatters_section(self):
"""Config contains formatters."""
assert "formatters" in LOGGING_CONFIG
assert len(LOGGING_CONFIG["formatters"]) >= 2
def test_has_handlers_section(self):
"""Config contains handlers."""
assert "handlers" in LOGGING_CONFIG
assert "console" in LOGGING_CONFIG["handlers"]
assert "file" in LOGGING_CONFIG["handlers"]
def test_has_loggers_section(self):
"""Config contains logger definitions."""
assert "loggers" in LOGGING_CONFIG
def test_has_root_section(self):
"""Config has a root logger entry."""
assert "root" in LOGGING_CONFIG
assert "handlers" in LOGGING_CONFIG["root"]
class TestFormatters:
"""Tests for formatter definitions."""
def test_default_formatter_exists(self):
"""Default formatter is defined."""
assert "default" in LOGGING_CONFIG["formatters"]
def test_access_formatter_exists(self):
"""Access formatter is defined."""
assert "access" in LOGGING_CONFIG["formatters"]
def test_detailed_formatter_exists(self):
"""Detailed formatter is defined for file output."""
assert "detailed" in LOGGING_CONFIG["formatters"]
def test_detailed_formatter_has_datefmt(self):
"""Detailed formatter includes date format."""
assert "datefmt" in LOGGING_CONFIG["formatters"]["detailed"]
class TestHandlers:
"""Tests for handler definitions."""
def test_console_handler_streams_to_stdout(self):
"""Console handler streams to stdout."""
assert LOGGING_CONFIG["handlers"]["console"]["stream"] == "ext://sys.stdout"
def test_file_handler_uses_detailed_formatter(self):
"""File handler uses the detailed formatter."""
assert LOGGING_CONFIG["handlers"]["file"]["formatter"] == "detailed"
def test_file_handler_encoding_is_utf8(self):
"""File handler writes UTF-8."""
assert LOGGING_CONFIG["handlers"]["file"]["encoding"] == "utf-8"
def test_file_handler_mode_is_append(self):
"""File handler appends to existing file."""
assert LOGGING_CONFIG["handlers"]["file"]["mode"] == "a"
class TestLoggerDefinitions:
"""Tests for individual logger entries."""
def test_uvicorn_logger_defined(self):
"""Main uvicorn logger is configured."""
assert "uvicorn" in LOGGING_CONFIG["loggers"]
def test_uvicorn_error_logger_defined(self):
"""uvicorn.error logger is configured."""
assert "uvicorn.error" in LOGGING_CONFIG["loggers"]
def test_uvicorn_access_logger_defined(self):
"""uvicorn.access logger is configured."""
assert "uvicorn.access" in LOGGING_CONFIG["loggers"]
def test_aniworld_logger_defined(self):
"""Application 'aniworld' logger is configured."""
assert "aniworld" in LOGGING_CONFIG["loggers"]
def test_watchfiles_logger_suppressed(self):
"""watchfiles.main logger level is WARNING to reduce noise."""
wf = LOGGING_CONFIG["loggers"]["watchfiles.main"]
assert wf["level"] == "WARNING"
def test_uvicorn_loggers_no_propagation(self):
"""Uvicorn loggers do not propagate to root."""
for name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
assert LOGGING_CONFIG["loggers"][name]["propagate"] is False
def test_all_loggers_have_handlers(self):
"""Every logger definition specifies at least one handler."""
for name, cfg in LOGGING_CONFIG["loggers"].items():
assert "handlers" in cfg, f"Logger '{name}' missing handlers"
assert len(cfg["handlers"]) >= 1
class TestPaths:
"""Tests for log path constants."""
def test_logs_dir_is_path(self):
"""LOGS_DIR is a Path object."""
from pathlib import Path
assert isinstance(LOGS_DIR, Path)
def test_log_file_is_path(self):
"""LOG_FILE is a Path object."""
from pathlib import Path
assert isinstance(LOG_FILE, Path)
def test_log_file_parent_is_logs_dir(self):
"""LOG_FILE resides in LOGS_DIR."""
assert LOG_FILE.parent == LOGS_DIR
def test_log_file_name(self):
"""Default log file is fastapi_app.log."""
assert LOG_FILE.name == "fastapi_app.log"
class TestGetUvicornLogConfig:
"""Tests for get_uvicorn_log_config function."""
def test_returns_dict(self):
"""Returns a dictionary."""
cfg = get_uvicorn_log_config()
assert isinstance(cfg, dict)
def test_returns_same_config(self):
"""Returns the module-level LOGGING_CONFIG constant."""
assert get_uvicorn_log_config() is LOGGING_CONFIG
def test_config_is_valid_for_dictconfig(self, tmp_path, monkeypatch):
"""Configuration is accepted by logging.config.dictConfig."""
cfg = get_uvicorn_log_config()
# Override the file handler filename to avoid side effects
import copy
test_cfg = copy.deepcopy(cfg)
test_cfg["handlers"]["file"]["filename"] = str(tmp_path / "test.log")
# Should not raise
logging.config.dictConfig(test_cfg)