Aniworld/tests/unit/test_logging.py
Lukas 8e885dd40b feat: implement comprehensive logging system
- Created src/server/utils/logging.py with structured JSON logging
- Multiple log handlers for app, error, download, security, performance
- Request logging middleware with unique request IDs and timing
- Log rotation and cleanup functionality
- Comprehensive test suite with 19 passing tests
- Context variables for request and user tracking
- Security event logging and download progress tracking

Features:
- JSON formatted logs with consistent structure
- Automatic log rotation (10MB files, 5 backups)
- Request/response logging middleware
- Performance monitoring
- Security auditing
- Download progress tracking
- Old log cleanup functionality

Tests: All 19 tests passing for logging system functionality
2025-10-12 23:33:56 +02:00

401 lines
13 KiB
Python

"""
Tests for the logging system.
"""
import json
import logging
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from src.server.utils.logging import (
JSONFormatter,
LoggingConfig,
RequestLoggingMiddleware,
cleanup_old_logs,
get_logger,
init_logging,
log_download_progress,
log_security_event,
request_id_var,
setup_logging,
user_id_var,
)
class TestJSONFormatter:
"""Test the JSON log formatter."""
def test_format_basic_log(self):
"""Test basic log formatting."""
formatter = JSONFormatter()
record = logging.LogRecord(
name="test",
level=logging.INFO,
pathname="/test.py",
lineno=42,
msg="Test message",
args=(),
exc_info=None
)
result = formatter.format(record)
log_data = json.loads(result)
assert log_data["level"] == "INFO"
assert log_data["logger"] == "test"
assert log_data["message"] == "Test message"
assert log_data["module"] == "test"
assert log_data["line"] == 42
assert "timestamp" in log_data
def test_format_with_context(self):
"""Test log formatting with request context."""
request_id_var.set("test-request-123")
user_id_var.set("user-456")
formatter = JSONFormatter()
record = logging.LogRecord(
name="test",
level=logging.INFO,
pathname="/test.py",
lineno=42,
msg="Test message",
args=(),
exc_info=None
)
result = formatter.format(record)
log_data = json.loads(result)
assert log_data["request_id"] == "test-request-123"
assert log_data["user_id"] == "user-456"
# Clean up context
request_id_var.set(None)
user_id_var.set(None)
def test_format_with_exception(self):
"""Test log formatting with exception."""
formatter = JSONFormatter()
try:
raise ValueError("Test error")
except ValueError:
import sys
exc_info = sys.exc_info()
record = logging.LogRecord(
name="test",
level=logging.ERROR,
pathname="/test.py",
lineno=42,
msg="Error occurred",
args=(),
exc_info=exc_info
)
result = formatter.format(record)
log_data = json.loads(result)
assert log_data["level"] == "ERROR"
assert "exception" in log_data
assert "ValueError" in log_data["exception"]
def test_format_with_extra_fields(self):
"""Test log formatting with extra fields."""
formatter = JSONFormatter()
record = logging.LogRecord(
name="test",
level=logging.INFO,
pathname="/test.py",
lineno=42,
msg="Test message",
args=(),
exc_info=None
)
# Add extra fields
record.episode_id = "episode-123"
record.download_speed = 1024.5
result = formatter.format(record)
log_data = json.loads(result)
assert "extra" in log_data
assert log_data["extra"]["episode_id"] == "episode-123"
assert log_data["extra"]["download_speed"] == 1024.5
class TestLoggingConfig:
"""Test the logging configuration."""
def test_init_with_defaults(self):
"""Test initialization with default values."""
with tempfile.TemporaryDirectory() as temp_dir:
config = LoggingConfig(log_dir=temp_dir)
assert config.log_dir == Path(temp_dir)
assert config.log_level == logging.INFO
assert config.enable_console is True
assert config.enable_json_format is True
# Check that log files would be created
# No logs yet, files created on first log
assert config.log_dir.exists()
def test_log_directory_creation(self):
"""Test that log directory is created."""
with tempfile.TemporaryDirectory() as temp_dir:
log_dir = Path(temp_dir) / "logs" / "subdir"
config = LoggingConfig(log_dir=log_dir)
assert log_dir.exists()
assert config.log_dir == log_dir
def test_logger_setup(self):
"""Test that loggers are properly configured."""
with tempfile.TemporaryDirectory() as temp_dir:
LoggingConfig(log_dir=temp_dir)
# Test main logger
logger = logging.getLogger()
assert logger.level == logging.INFO
# Test specific loggers
download_logger = logging.getLogger("download")
assert download_logger.level == logging.INFO
assert download_logger.propagate is False
security_logger = logging.getLogger("security")
assert security_logger.level == logging.INFO
assert security_logger.propagate is False
performance_logger = logging.getLogger("performance")
assert performance_logger.level == logging.INFO
assert performance_logger.propagate is False
def test_file_logging(self):
"""Test that log files are created and written to."""
with tempfile.TemporaryDirectory() as temp_dir:
LoggingConfig(log_dir=temp_dir, enable_console=False)
# Write some logs
logger = logging.getLogger("test")
logger.info("Test info message")
logger.error("Test error message")
# Force handler flush
for handler in logging.getLogger().handlers:
handler.flush()
# Check that log files exist and contain content
app_log = Path(temp_dir) / "app.log"
error_log = Path(temp_dir) / "error.log"
# Files should exist after logging
assert app_log.exists()
assert error_log.exists()
class TestRequestLoggingMiddleware:
"""Test the request logging middleware."""
@pytest.fixture
def mock_request(self):
"""Create a mock request."""
request = MagicMock()
request.method = "GET"
request.url = "http://test.com/api/test"
request.headers = {
"user-agent": "test-agent",
"content-length": "100"
}
request.client.host = "127.0.0.1"
return request
@pytest.fixture
def mock_response(self):
"""Create a mock response."""
response = MagicMock()
response.status_code = 200
response.headers = {"content-length": "200"}
return response
@pytest.mark.asyncio
async def test_successful_request_logging(
self, mock_request, mock_response):
"""Test logging of successful requests."""
app = MagicMock()
middleware = RequestLoggingMiddleware(app)
async def mock_call_next(request):
return mock_response
with patch.object(middleware.logger, 'info') as mock_log_info:
with patch.object(
middleware.performance_logger, 'info') as mock_perf_log:
response = await middleware.dispatch(
mock_request, mock_call_next)
assert response == mock_response
assert mock_log_info.call_count == 2 # Start and completion
assert mock_perf_log.call_count == 1
# Check log messages
start_call = mock_log_info.call_args_list[0]
assert "Request started" in start_call[0][0]
completion_call = mock_log_info.call_args_list[1]
assert "Request completed" in completion_call[0][0]
@pytest.mark.asyncio
async def test_failed_request_logging(self, mock_request):
"""Test logging of failed requests."""
app = MagicMock()
middleware = RequestLoggingMiddleware(app)
async def mock_call_next(request):
raise ValueError("Test error")
with patch.object(middleware.logger, 'info') as mock_log_info:
with patch.object(middleware.logger, 'error') as mock_log_error:
with pytest.raises(ValueError):
await middleware.dispatch(mock_request, mock_call_next)
assert mock_log_info.call_count == 1 # Only start
assert mock_log_error.call_count == 1 # Error
error_call = mock_log_error.call_args_list[0]
assert "Request failed" in error_call[0][0]
def test_get_client_ip_forwarded_for(self):
"""Test client IP extraction with X-Forwarded-For header."""
app = MagicMock()
middleware = RequestLoggingMiddleware(app)
request = MagicMock()
request.headers = {"x-forwarded-for": "192.168.1.1, 10.0.0.1"}
ip = middleware._get_client_ip(request)
assert ip == "192.168.1.1"
def test_get_client_ip_real_ip(self):
"""Test client IP extraction with X-Real-IP header."""
app = MagicMock()
middleware = RequestLoggingMiddleware(app)
request = MagicMock()
request.headers = {"x-real-ip": "192.168.1.2"}
ip = middleware._get_client_ip(request)
assert ip == "192.168.1.2"
def test_get_client_ip_direct(self):
"""Test client IP extraction from direct connection."""
app = MagicMock()
middleware = RequestLoggingMiddleware(app)
request = MagicMock()
request.headers = {}
request.client.host = "192.168.1.3"
ip = middleware._get_client_ip(request)
assert ip == "192.168.1.3"
class TestUtilityFunctions:
"""Test utility functions."""
def test_setup_logging(self):
"""Test setup_logging function."""
with tempfile.TemporaryDirectory() as temp_dir:
config = setup_logging(log_dir=temp_dir, log_level="DEBUG")
assert isinstance(config, LoggingConfig)
assert config.log_dir == Path(temp_dir)
assert config.log_level == logging.DEBUG
def test_get_logger(self):
"""Test get_logger function."""
logger = get_logger("test.module")
assert isinstance(logger, logging.Logger)
assert logger.name == "test.module"
def test_log_download_progress(self):
"""Test download progress logging."""
with patch('logging.getLogger') as mock_get_logger:
mock_logger = MagicMock()
mock_get_logger.return_value = mock_logger
log_download_progress(
episode_id="ep-123",
progress=0.75,
status="downloading",
speed=1024.5,
eta="5 minutes"
)
mock_get_logger.assert_called_with("download")
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args
assert "Download progress" in call_args[0][0]
assert call_args[1]["extra"]["episode_id"] == "ep-123"
assert call_args[1]["extra"]["progress"] == 0.75
def test_log_security_event(self):
"""Test security event logging."""
with patch('logging.getLogger') as mock_get_logger:
mock_logger = MagicMock()
mock_get_logger.return_value = mock_logger
log_security_event(
event_type="login_attempt",
details={"user_ip": "192.168.1.1", "success": True},
severity="INFO"
)
mock_get_logger.assert_called_with("security")
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args
assert "Security event: login_attempt" in call_args[0][0]
def test_cleanup_old_logs(self):
"""Test log cleanup function."""
with tempfile.TemporaryDirectory() as temp_dir:
log_dir = Path(temp_dir)
# Create some test log files
old_log = log_dir / "old.log"
new_log = log_dir / "new.log"
old_log.touch()
new_log.touch()
# Test that function runs without error
# (Real test would require complex mocking of file system)
try:
cleanup_old_logs(log_dir, days_to_keep=30)
# If no exception is raised, the function works
success = True
except Exception:
success = False
assert success
# Both files should still exist since they're new
assert old_log.exists()
assert new_log.exists()
def test_init_logging(self):
"""Test init_logging function."""
with tempfile.TemporaryDirectory() as temp_dir:
init_logging(log_dir=temp_dir, log_level="DEBUG")
# Should set up global logging
logger = logging.getLogger()
assert logger.level == logging.DEBUG