Skip to main content
Glama

MCP Server for Odoo

by ivnvxd
Mozilla Public License 2.0
88
  • Apple
  • Linux
test_error_handling.py•17.8 kB
"""Tests for error handling and logging system.""" import json import logging import os import tempfile import time from unittest.mock import MagicMock, patch import pytest from mcp_server_odoo.error_handling import ( AuthenticationError, ConfigurationError, ConnectionError, ErrorCategory, ErrorContext, ErrorHandler, ErrorSeverity, MCPError, NotFoundError, PermissionError, RateLimitError, SystemError, ValidationError, error_handler, format_user_error, handle_odoo_error, ) from mcp_server_odoo.logging_config import ( LoggingConfig, PerformanceLogger, RequestLoggingAdapter, StructuredFormatter, log_request, log_response, logging_config, perf_logger, setup_logging, ) class TestMCPError: """Test the MCPError base class.""" def test_error_creation(self): """Test creating an MCPError with all parameters.""" context = ErrorContext( model="res.partner", operation="search", record_id=42, user_id=1, request_id="test-123", ) error = MCPError( message="Test error", category=ErrorCategory.VALIDATION, severity=ErrorSeverity.MEDIUM, code="TEST_ERROR", details={"field": "email", "value": "invalid"}, context=context, ) assert error.message == "Test error" assert error.category == ErrorCategory.VALIDATION assert error.severity == ErrorSeverity.MEDIUM assert error.code == "TEST_ERROR" assert error.details == {"field": "email", "value": "invalid"} assert error.context.model == "res.partner" assert error.context.operation == "search" def test_error_code_generation(self): """Test automatic error code generation.""" error = AuthenticationError("Invalid credentials") assert error.code == "AUTH_ERROR" error = PermissionError("Access denied") assert error.code == "PERMISSION_DENIED" error = NotFoundError("Record not found") assert error.code == "NOT_FOUND" error = ValidationError("Invalid input") assert error.code == "VALIDATION_ERROR" error = ConnectionError("Connection failed") assert error.code == "CONNECTION_ERROR" error = SystemError("System failure") assert error.code == "SYSTEM_ERROR" error = ConfigurationError("Bad config") assert error.code == "CONFIG_ERROR" error = RateLimitError("Too many requests") assert error.code == "RATE_LIMIT_EXCEEDED" def test_error_to_dict(self): """Test converting error to dictionary.""" context = ErrorContext(model="res.partner", operation="create") error = ValidationError( "Invalid email format", details={"field": "email"}, context=context, ) error_dict = error.to_dict() assert "error" in error_dict assert error_dict["error"]["code"] == "VALIDATION_ERROR" assert error_dict["error"]["message"] == "Invalid email format" assert error_dict["error"]["category"] == "VALIDATION" assert error_dict["error"]["severity"] == "low" # Details are sanitized to only include safe fields assert error_dict["error"]["details"] == {"field": "email"} assert error_dict["error"]["context"]["model"] == "res.partner" assert error_dict["error"]["context"]["operation"] == "create" assert "timestamp" in error_dict["error"] def test_error_to_mcp_error(self): """Test converting to MCP-compliant error format.""" error = ValidationError( "Invalid input", details={"field": "name", "issue": "too_short"}, ) mcp_error = error.to_mcp_error() assert mcp_error.code == -32000 # Application error code assert mcp_error.message == "Invalid input" assert mcp_error.data["code"] == "VALIDATION_ERROR" # Details are now sanitized - only safe fields are included assert mcp_error.data["details"] == {"field": "name"} class TestErrorHandler: """Test the ErrorHandler class.""" def test_error_handler_initialization(self): """Test error handler initialization.""" handler = ErrorHandler() assert handler.metrics.total_errors == 0 assert len(handler.metrics.errors_by_category) == 0 assert len(handler.metrics.errors_by_severity) == 0 assert handler.metrics.last_error_time is None assert handler._max_history_size == 1000 def test_handle_mcp_error(self): """Test handling an MCPError.""" handler = ErrorHandler() handler.clear_metrics() error = ValidationError("Test validation error") with pytest.raises(ValidationError): handler.handle_error(error) # Check metrics assert handler.metrics.total_errors == 1 assert handler.metrics.errors_by_category[ErrorCategory.VALIDATION] == 1 assert handler.metrics.errors_by_severity[ErrorSeverity.LOW] == 1 assert handler.metrics.last_error_time is not None # Check history recent = handler.get_recent_errors(limit=1) assert len(recent) == 1 assert recent[0]["error"]["message"] == "Test validation error" def test_handle_standard_exception(self): """Test converting standard exceptions to MCPError.""" handler = ErrorHandler() handler.clear_metrics() # Test ValueError conversion with pytest.raises(ValidationError) as exc_info: handler.handle_error(ValueError("Invalid value")) assert "Invalid input: Invalid value" in str(exc_info.value) # Test ConnectionRefusedError conversion with pytest.raises(ConnectionError) as exc_info: handler.handle_error(ConnectionRefusedError("Connection refused")) assert "Connection failed:" in str(exc_info.value) # Test KeyError conversion with pytest.raises(NotFoundError) as exc_info: handler.handle_error(KeyError("missing_key")) assert "Resource not found:" in str(exc_info.value) # Test generic exception conversion with pytest.raises(SystemError) as exc_info: handler.handle_error(RuntimeError("Something went wrong")) assert "Unexpected error:" in str(exc_info.value) def test_handle_error_no_reraise(self): """Test handling error without re-raising.""" handler = ErrorHandler() error = ValidationError("Test error") result = handler.handle_error(error, reraise=False) assert isinstance(result, MCPError) assert result.message == "Test error" def test_error_context_manager(self): """Test error context manager.""" handler = ErrorHandler() handler.clear_metrics() with pytest.raises(ValidationError) as exc_info: with handler.error_context(model="res.partner", operation="create"): raise ValueError("Invalid field") error = exc_info.value assert error.context.model == "res.partner" assert error.context.operation == "create" def test_get_metrics(self): """Test getting error metrics.""" handler = ErrorHandler() handler.clear_metrics() # Generate some errors handler.handle_error(ValidationError("Error 1"), reraise=False) handler.handle_error(PermissionError("Error 2"), reraise=False) handler.handle_error(ValidationError("Error 3"), reraise=False) metrics = handler.get_metrics() assert metrics["total_errors"] == 3 assert metrics["errors_by_category"]["VALIDATION"] == 2 assert metrics["errors_by_category"]["PERMISSION"] == 1 assert metrics["errors_by_severity"]["low"] == 2 assert metrics["errors_by_severity"]["medium"] == 1 assert metrics["last_error_time"] is not None assert "error_rate_per_minute" in metrics assert "uptime_seconds" in metrics def test_error_history_limit(self): """Test that error history respects size limit.""" handler = ErrorHandler() handler._max_history_size = 5 handler.clear_metrics() # Add more errors than the limit for i in range(10): handler.handle_error( ValidationError(f"Error {i}"), reraise=False, ) # Check that only the last 5 are kept recent = handler.get_recent_errors(limit=10) assert len(recent) == 5 # Messages are sanitized, but we can verify the history is properly limited class TestOdooErrorHandling: """Test Odoo-specific error handling.""" def test_handle_odoo_access_denied(self): """Test handling Odoo access denied errors.""" error = Exception("Access Denied for model res.partner") result = handle_odoo_error(error, model="res.partner", operation="read") assert isinstance(result, PermissionError) assert "Access denied for read on res.partner" in result.message assert result.context.model == "res.partner" assert result.context.operation == "read" def test_handle_odoo_not_found(self): """Test handling Odoo not found errors.""" error = Exception("Record does not exist") result = handle_odoo_error(error, model="res.partner") assert isinstance(result, NotFoundError) assert "Resource not found: res.partner" in result.message def test_handle_odoo_validation(self): """Test handling Odoo validation errors.""" error = Exception("Invalid field value") result = handle_odoo_error(error, operation="create") assert isinstance(result, ValidationError) assert "Validation failed for create" in result.message def test_handle_odoo_connection(self): """Test handling Odoo connection errors.""" error = Exception("Connection timeout") result = handle_odoo_error(error) assert isinstance(result, ConnectionError) assert "Connection to Odoo failed" in result.message def test_handle_odoo_generic(self): """Test handling generic Odoo errors.""" error = Exception("Some other error") result = handle_odoo_error(error, operation="search") assert isinstance(result, SystemError) assert "Odoo error during search" in result.message class TestUserErrorFormatting: """Test user-friendly error formatting.""" def test_format_validation_error(self): """Test formatting validation errors.""" error = ValidationError( "Email format is invalid", context=ErrorContext(model="res.partner"), ) formatted = format_user_error(error) assert "Email format is invalid (Model: res.partner)" in formatted assert "Please check your input and try again" in formatted def test_format_permission_error(self): """Test formatting permission errors.""" error = PermissionError("Cannot create records") formatted = format_user_error(error) assert "Cannot create records" in formatted assert "You don't have permission" in formatted assert "Contact your administrator" in formatted def test_format_not_found_error(self): """Test formatting not found errors.""" error = NotFoundError("Partner not found") formatted = format_user_error(error) assert "Partner not found" in formatted assert "doesn't exist or has been deleted" in formatted def test_format_connection_error(self): """Test formatting connection errors.""" error = ConnectionError("Cannot connect to server") formatted = format_user_error(error) assert "Cannot connect to server" in formatted assert "Unable to connect to Odoo" in formatted assert "check your connection settings" in formatted class TestLoggingConfiguration: """Test logging configuration and utilities.""" def test_structured_formatter(self): """Test JSON log formatting.""" formatter = StructuredFormatter() # Create a log record record = logging.LogRecord( name="test.logger", level=logging.INFO, pathname="test.py", lineno=42, msg="Test message", args=(), exc_info=None, ) # Add extra fields record.error_code = "TEST_ERROR" record.model = "res.partner" formatted = formatter.format(record) log_data = json.loads(formatted) assert log_data["logger"] == "test.logger" assert log_data["level"] == "INFO" assert log_data["message"] == "Test message" assert log_data["error_code"] == "TEST_ERROR" assert log_data["model"] == "res.partner" assert "timestamp" in log_data def test_request_logging_adapter(self): """Test request logging adapter.""" logger = logging.getLogger("test") adapter = RequestLoggingAdapter(logger, request_id="test-123") assert adapter.request_id == "test-123" # Test that request ID is added to extra msg, kwargs = adapter.process("Test message", {}) assert kwargs["extra"]["request_id"] == "test-123" def test_performance_logger(self): """Test performance tracking.""" logger = MagicMock() perf = PerformanceLogger(logger) with perf.track_operation("test_op", model="res.partner"): time.sleep(0.01) # Small delay # Check that info was logged logger.info.assert_called() call_args = logger.info.call_args assert "test_op" in call_args[0][0] assert "completed in" in call_args[0][0] assert call_args[1]["extra"]["operation"] == "test_op" assert call_args[1]["extra"]["model"] == "res.partner" assert call_args[1]["extra"]["duration_ms"] > 0 def test_setup_logging(self): """Test logging setup.""" with tempfile.NamedTemporaryFile(delete=False) as tmp: setup_logging( log_level="DEBUG", use_json=True, log_file=tmp.name, ) logger = logging.getLogger("test") logger.debug("Test debug message") # Check that file was written assert os.path.exists(tmp.name) assert os.path.getsize(tmp.name) > 0 # Clean up os.unlink(tmp.name) def test_logging_config_from_env(self): """Test loading logging config from environment.""" with patch.dict( os.environ, { "ODOO_MCP_LOG_LEVEL": "DEBUG", "ODOO_MCP_LOG_JSON": "true", "ODOO_MCP_LOG_FILE": "/tmp/test.log", "ODOO_MCP_SLOW_OPERATION_THRESHOLD_MS": "500", }, ): config = LoggingConfig() assert config.log_level == "DEBUG" assert config.use_json is True assert config.log_file == "/tmp/test.log" assert config.slow_operation_threshold_ms == 500 def test_log_request_response(self): """Test request/response logging helpers.""" logger = MagicMock() # Test request logging log_request( logger, method="GET", path="/api/test", params={"limit": 10}, body={"filter": "active"}, ) logger.info.assert_called() call_args = logger.info.call_args assert "GET /api/test" in call_args[0][0] assert call_args[1]["extra"]["request_method"] == "GET" assert call_args[1]["extra"]["request_params"] == {"limit": 10} # Test response logging log_response( logger, status="200 OK", duration_ms=123.45, response_size=1024, ) assert logger.info.call_count == 2 call_args = logger.info.call_args assert "200 OK (123.45ms)" in call_args[0][0] assert call_args[1]["extra"]["response_status"] == "200 OK" assert call_args[1]["extra"]["response_size"] == 1024 # Test error response logging log_response( logger, status="500 Error", duration_ms=50.0, error="Internal server error", ) logger.error.assert_called() call_args = logger.error.call_args assert "500 Error" in call_args[0][0] assert "Internal server error" in call_args[0][0] class TestGlobalInstances: """Test global error handler and logging instances.""" def test_global_error_handler(self): """Test that global error handler works correctly.""" # Clear any existing state error_handler.clear_metrics() # Generate an error with pytest.raises(ValidationError): error_handler.handle_error(ValueError("Test")) # Check metrics metrics = error_handler.get_metrics() assert metrics["total_errors"] == 1 def test_global_perf_logger(self): """Test that global performance logger works.""" with perf_logger.track_operation("test_operation"): time.sleep(0.01) # Operation should complete without error def test_global_logging_config(self): """Test that global logging config works.""" assert isinstance(logging_config, LoggingConfig) assert hasattr(logging_config, "log_level") assert hasattr(logging_config, "setup")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ivnvxd/mcp-server-odoo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server