Skip to main content
Glama

MCP Server for Odoo

test_basic_resources.py•10.7 kB
"""Tests for basic MCP resource handling.""" from unittest.mock import Mock import pytest from mcp.server.fastmcp import FastMCP from mcp_server_odoo.access_control import AccessControlError, AccessController from mcp_server_odoo.config import OdooConfig from mcp_server_odoo.error_handling import ( NotFoundError, ValidationError, ) from mcp_server_odoo.error_handling import ( PermissionError as MCPPermissionError, ) from mcp_server_odoo.odoo_connection import OdooConnection, OdooConnectionError from mcp_server_odoo.resources import OdooResourceHandler, register_resources @pytest.fixture def test_config(): """Create test configuration.""" # Load real config from environment for integration tests from mcp_server_odoo.config import get_config return get_config() @pytest.fixture def mock_config(): """Create mock configuration.""" config = Mock(spec=OdooConfig) config.default_limit = 10 config.max_limit = 100 return config @pytest.fixture def mock_connection(): """Create mock OdooConnection.""" conn = Mock(spec=OdooConnection) conn.is_authenticated = True conn.search = Mock() conn.read = Mock() conn.fields_get = Mock(return_value={}) # Mock fields_get for formatter return conn @pytest.fixture def mock_access_controller(): """Create mock AccessController.""" controller = Mock(spec=AccessController) controller.validate_model_access = Mock() return controller @pytest.fixture def mock_app(): """Create mock FastMCP app.""" app = Mock(spec=FastMCP) app.resource = Mock() # Make resource decorator return the function as-is app.resource.return_value = lambda func: func return app @pytest.fixture def resource_handler(mock_app, mock_connection, mock_access_controller, mock_config): """Create OdooResourceHandler instance.""" return OdooResourceHandler(mock_app, mock_connection, mock_access_controller, mock_config) class TestOdooResourceHandler: """Test OdooResourceHandler functionality.""" def test_init(self, mock_app, mock_connection, mock_access_controller, mock_config): """Test handler initialization.""" handler = OdooResourceHandler( mock_app, mock_connection, mock_access_controller, mock_config ) assert handler.app == mock_app assert handler.connection == mock_connection assert handler.access_controller == mock_access_controller assert handler.config == mock_config # Check that resources were registered assert mock_app.resource.call_count >= 1 @pytest.mark.asyncio async def test_handle_record_retrieval_success( self, resource_handler, mock_connection, mock_access_controller ): """Test successful record retrieval.""" # Setup mocks mock_connection.search.return_value = [1] mock_connection.read.return_value = [ { "id": 1, "name": "Test Partner", "display_name": "Test Partner", "email": "test@example.com", "is_company": True, "country_id": (1, "United States"), "child_ids": [2, 3, 4], "phone": False, "__last_update": "2025-01-01 00:00:00", } ] # Test retrieval result = await resource_handler._handle_record_retrieval("res.partner", "1") # Verify calls mock_access_controller.validate_model_access.assert_called_once_with("res.partner", "read") mock_connection.search.assert_called_once_with("res.partner", [("id", "=", 1)]) mock_connection.read.assert_called_once_with("res.partner", [1]) # Check result format (using new formatter) assert "Record: res.partner/1" in result assert "Name: Test Partner" in result assert "=" * 50 in result # Separator line assert "Fields:" in result or "Relationships:" in result @pytest.mark.asyncio async def test_handle_record_retrieval_not_found( self, resource_handler, mock_connection, mock_access_controller ): """Test record not found error.""" # Setup mocks mock_connection.search.return_value = [] # Test retrieval with pytest.raises(NotFoundError) as exc_info: await resource_handler._handle_record_retrieval("res.partner", "999") assert "Record not found: res.partner with ID 999 does not exist" in str(exc_info.value) # Verify calls mock_access_controller.validate_model_access.assert_called_once_with("res.partner", "read") mock_connection.search.assert_called_once_with("res.partner", [("id", "=", 999)]) mock_connection.read.assert_not_called() @pytest.mark.asyncio async def test_handle_record_retrieval_invalid_id(self, resource_handler): """Test invalid record ID.""" # Test with non-numeric ID with pytest.raises(ValidationError) as exc_info: await resource_handler._handle_record_retrieval("res.partner", "abc") assert "Invalid record ID 'abc'" in str(exc_info.value) # Test with negative ID with pytest.raises(ValidationError) as exc_info: await resource_handler._handle_record_retrieval("res.partner", "-5") assert "Record ID must be positive" in str(exc_info.value) @pytest.mark.asyncio async def test_handle_record_retrieval_permission_denied( self, resource_handler, mock_access_controller ): """Test permission denied error.""" # Setup mock to raise permission error mock_access_controller.validate_model_access.side_effect = AccessControlError( "Access denied for res.partner" ) # Test retrieval with pytest.raises(MCPPermissionError) as exc_info: await resource_handler._handle_record_retrieval("res.partner", "1") assert "Access denied:" in str(exc_info.value) @pytest.mark.asyncio async def test_handle_record_retrieval_not_authenticated( self, resource_handler, mock_connection ): """Test error when not authenticated.""" # Setup mock mock_connection.is_authenticated = False # Test retrieval with pytest.raises(ValidationError) as exc_info: await resource_handler._handle_record_retrieval("res.partner", "1") assert "Not authenticated with Odoo" in str(exc_info.value) @pytest.mark.asyncio async def test_handle_record_retrieval_connection_error( self, resource_handler, mock_connection, mock_access_controller ): """Test connection error during retrieval.""" # Setup mock to raise connection error mock_connection.search.side_effect = OdooConnectionError("Connection lost") # Test retrieval with pytest.raises(ValidationError) as exc_info: await resource_handler._handle_record_retrieval("res.partner", "1") assert "Connection error:" in str(exc_info.value) class TestRegisterResources: """Test register_resources function.""" def test_register_resources( self, mock_app, mock_connection, mock_access_controller, mock_config ): """Test resource registration.""" handler = register_resources(mock_app, mock_connection, mock_access_controller, mock_config) assert isinstance(handler, OdooResourceHandler) assert handler.app == mock_app assert handler.connection == mock_connection assert handler.access_controller == mock_access_controller assert handler.config == mock_config # Check that resources were registered assert mock_app.resource.call_count >= 1 class TestResourceIntegration: """Integration tests with real Odoo server.""" @pytest.mark.integration @pytest.mark.asyncio async def test_real_record_retrieval(self, test_config): """Test record retrieval with real server.""" # Create real connection connection = OdooConnection(test_config) connection.connect() connection.authenticate() # Create access controller access_controller = AccessController(test_config) # Create FastMCP app app = FastMCP("test-app") # Register resources handler = register_resources(app, connection, access_controller, test_config) try: # Search for a partner record partner_ids = connection.search("res.partner", [], limit=1) if partner_ids: # Test retrieval result = await handler._handle_record_retrieval("res.partner", str(partner_ids[0])) # Verify result format assert f"Record: res.partner/{partner_ids[0]}" in result assert "Name:" in result assert "=" * 50 in result # Separator line finally: connection.disconnect() @pytest.mark.integration @pytest.mark.asyncio async def test_real_record_not_found(self, test_config): """Test record not found with real server.""" # Create real connection connection = OdooConnection(test_config) connection.connect() connection.authenticate() # Create access controller access_controller = AccessController(test_config) # Create FastMCP app app = FastMCP("test-app") # Register resources handler = register_resources(app, connection, access_controller, test_config) try: # Test with non-existent ID with pytest.raises(NotFoundError): await handler._handle_record_retrieval("res.partner", "999999999") finally: connection.disconnect() @pytest.mark.integration @pytest.mark.asyncio async def test_real_permission_denied(self, test_config): """Test permission denied with real server.""" # Create real connection connection = OdooConnection(test_config) connection.connect() connection.authenticate() # Create access controller access_controller = AccessController(test_config) # Create FastMCP app app = FastMCP("test-app") # Register resources handler = register_resources(app, connection, access_controller, test_config) try: # Test with non-enabled model with pytest.raises(MCPPermissionError): await handler._handle_record_retrieval("account.invoice", "1") finally: connection.disconnect()

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/AlejandroLaraPolanco/mcp-odoo'

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