"""Tests for structured logger functionality.""" import pytest import json import logging from unittest.mock import Mock, patch from src.application.error_handling.structured_logger import ( StructuredLogger, LogContext, JsonFormatter, ContextFormatter, get_structured_logger, set_correlation_id, get_correlation_id, generate_correlation_id ) class TestLogContext: """Test cases for LogContext.""" def test_log_context_creation(self): """Test creating log context.""" context = LogContext( correlation_id="test-123", operation="test_operation", component="test_component" ) assert context.correlation_id == "test-123" assert context.operation == "test_operation" assert context.component == "test_component" def test_log_context_to_dict(self): """Test converting log context to dictionary.""" context = LogContext( correlation_id="test-123", operation="test_operation", metadata={"key": "value"} ) context_dict = context.to_dict() assert context_dict["correlation_id"] == "test-123" assert context_dict["operation"] == "test_operation" assert context_dict["metadata"] == {"key": "value"} assert "user_id" not in context_dict # None values should be excluded class TestStructuredLogger: """Test cases for StructuredLogger.""" def setup_method(self): """Set up test fixtures.""" self.logger = StructuredLogger("test_logger", enable_json_logging=False) def test_logger_creation(self): """Test creating structured logger.""" assert self.logger.logger.name == "test_logger" assert not self.logger.enable_json_logging def test_debug_logging(self): """Test debug logging.""" context = LogContext(correlation_id="test-123", operation="test_op") with patch.object(self.logger.logger, 'debug') as mock_debug: self.logger.info("Test debug message", context=context) mock_debug.assert_called_once() args, kwargs = mock_debug.call_args assert "Test debug message" in args[0] assert "extra" in kwargs def test_info_logging(self): """Test info logging.""" context = LogContext(correlation_id="test-123") extra = {"key": "value"} with patch.object(self.logger.logger, 'info') as mock_info: self.logger.info("Test info message", context=context, extra=extra) mock_info.assert_called_once() args, kwargs = mock_info.call_args assert "Test info message" in args[0] assert kwargs["extra"]["extra"] == extra def test_error_logging_with_exception(self): """Test error logging with exception.""" context = LogContext(correlation_id="test-123") exception = ValueError("Test error") with patch.object(self.logger.logger, 'error') as mock_error: self.logger.error("Test error message", context=context, exception=exception) mock_error.assert_called_once() args, kwargs = mock_error.call_args assert "Test error message" in args[0] assert kwargs["extra"]["exception"]["type"] == "ValueError" assert kwargs["extra"]["exception"]["message"] == "Test error" def test_log_operation_start(self): """Test logging operation start.""" extra = {"param": "value"} with patch.object(self.logger.logger, 'info') as mock_info: correlation_id = self.logger.log_operation_start("test_operation", extra=extra) assert correlation_id is not None mock_info.assert_called_once() args, kwargs = mock_info.call_args assert "Operation started: test_operation" in args[0] def test_log_operation_end_success(self): """Test logging successful operation end.""" correlation_id = "test-123" with patch.object(self.logger.logger, 'info') as mock_info: self.logger.log_operation_end( "test_operation", correlation_id, success=True, duration=1.5 ) mock_info.assert_called_once() args, kwargs = mock_info.call_args assert "completed successfully" in args[0] assert kwargs["extra"]["extra"]["success"] is True assert kwargs["extra"]["extra"]["duration_seconds"] == 1.5 def test_log_operation_end_failure(self): """Test logging failed operation end.""" correlation_id = "test-123" with patch.object(self.logger.logger, 'error') as mock_error: self.logger.log_operation_end( "test_operation", correlation_id, success=False ) mock_error.assert_called_once() args, kwargs = mock_error.call_args assert "failed" in args[0] assert kwargs["extra"]["extra"]["success"] is False def test_log_performance_metric(self): """Test logging performance metric.""" context = LogContext(correlation_id="test-123") with patch.object(self.logger.logger, 'info') as mock_info: self.logger.log_performance_metric( "response_time", 150.5, "ms", context=context ) mock_info.assert_called_once() args, kwargs = mock_info.call_args assert "Performance metric: response_time=150.5 ms" in args[0] assert kwargs["extra"]["extra"]["metric"]["name"] == "response_time" assert kwargs["extra"]["extra"]["metric"]["value"] == 150.5 assert kwargs["extra"]["extra"]["metric"]["unit"] == "ms" class TestJsonFormatter: """Test cases for JsonFormatter.""" def setup_method(self): """Set up test fixtures.""" self.formatter = JsonFormatter() def test_format_log_record(self): """Test formatting log record as JSON.""" record = logging.LogRecord( name="test_logger", level=logging.INFO, pathname="test.py", lineno=10, msg="Test message", args=(), exc_info=None ) # Add extra data record.extra = { "correlation_id": "test-123", "operation": "test_op" } formatted = self.formatter.format(record) # Should be valid JSON log_data = json.loads(formatted) assert log_data["message"] == "Test message" assert log_data["level"] == "INFO" assert log_data["correlation_id"] == "test-123" assert log_data["operation"] == "test_op" def test_format_error_handling(self): """Test formatter error handling.""" record = logging.LogRecord( name="test_logger", level=logging.INFO, pathname="test.py", lineno=10, msg="Test message", args=(), exc_info=None ) # Add problematic extra data that can't be JSON serialized record.extra = { "correlation_id": "test-123", "problematic_data": object() # Can't be JSON serialized } formatted = self.formatter.format(record) # Should still work and include error message assert "Test message" in formatted class TestContextFormatter: """Test cases for ContextFormatter.""" def setup_method(self): """Set up test fixtures.""" self.formatter = ContextFormatter() def test_format_with_correlation_id(self): """Test formatting with correlation ID.""" record = logging.LogRecord( name="test_logger", level=logging.INFO, pathname="test.py", lineno=10, msg="Test message", args=(), exc_info=None ) record.extra = {"correlation_id": "test-123"} formatted = self.formatter.format(record) assert "[test-123]" in formatted assert "Test message" in formatted def test_format_with_operation(self): """Test formatting with operation context.""" record = logging.LogRecord( name="test_logger", level=logging.INFO, pathname="test.py", lineno=10, msg="Test message", args=(), exc_info=None ) record.extra = { "correlation_id": "test-123", "operation": "test_operation" } formatted = self.formatter.format(record) assert "[test_operation]" in formatted assert "Test message" in formatted class TestUtilityFunctions: """Test cases for utility functions.""" def test_get_structured_logger(self): """Test getting structured logger.""" logger = get_structured_logger("test_logger") assert isinstance(logger, StructuredLogger) assert logger.logger.name == "test_logger" def test_correlation_id_context(self): """Test correlation ID context management.""" # Initially should be None assert get_correlation_id() is None # Set correlation ID set_correlation_id("test-123") assert get_correlation_id() == "test-123" def test_generate_correlation_id(self): """Test generating correlation ID.""" correlation_id = generate_correlation_id() assert correlation_id is not None assert len(correlation_id) > 0 # Should generate different IDs another_id = generate_correlation_id() assert correlation_id != another_id