Skip to main content
Glama

MCP Server for Odoo

test_tools_yolo.py•11.3 kB
"""Tests for tools functionality in YOLO mode. This module tests the tool handlers behavior in YOLO modes. """ import os from unittest.mock import MagicMock import pytest from mcp_server_odoo.config import OdooConfig from mcp_server_odoo.tools import OdooToolHandler class TestYoloModeTools: """Test tools in YOLO mode.""" @pytest.fixture def config_yolo_read(self): """Create configuration for read-only YOLO mode.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), database=os.getenv("ODOO_DB"), yolo_mode="read", ) @pytest.fixture def config_yolo_full(self): """Create configuration for full access YOLO mode.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), database=os.getenv("ODOO_DB"), yolo_mode="true", ) @pytest.fixture def config_standard(self): """Create configuration for standard mode.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key="test_api_key", database=os.getenv("ODOO_DB"), yolo_mode="off", ) @pytest.fixture def mock_connection(self): """Create mock OdooConnection.""" mock = MagicMock() mock.is_authenticated = True mock.search_read = MagicMock() return mock @pytest.fixture def mock_access_controller(self): """Create mock AccessController.""" mock = MagicMock() mock.get_enabled_models = MagicMock() mock.get_model_permissions = MagicMock() return mock @pytest.fixture def mock_app(self): """Create mock FastMCP app.""" mock = MagicMock() return mock @pytest.mark.asyncio async def test_list_models_yolo_read_mode( self, config_yolo_read, mock_connection, mock_access_controller, mock_app ): """Test list_models in read-only YOLO mode.""" # Setup mock data mock_connection.search_read.return_value = [ {"model": "res.partner", "name": "Contact"}, {"model": "product.product", "name": "Product"}, {"model": "sale.order", "name": "Sales Order"}, ] # Create handler handler = OdooToolHandler( mock_app, mock_connection, mock_access_controller, config_yolo_read ) # Call the method result = await handler._handle_list_models_tool() # Verify connection was called to query models mock_connection.search_read.assert_called_once() call_args = mock_connection.search_read.call_args assert call_args[0][0] == "ir.model" # Model name assert "transient" in str(call_args[0][1]) # Domain includes transient filter # Check result structure assert "yolo_mode" in result assert "models" in result assert "total" in result # Check YOLO mode metadata yolo_meta = result["yolo_mode"] assert yolo_meta["enabled"] is True assert yolo_meta["level"] == "read" assert "READ-ONLY" in yolo_meta["description"] assert "🚨" in yolo_meta["warning"] assert yolo_meta["operations"]["read"] is True assert yolo_meta["operations"]["write"] is False assert yolo_meta["operations"]["create"] is False assert yolo_meta["operations"]["unlink"] is False # Check actual models are clean (no operations field) models = result["models"] assert len(models) == 3 # Should match mock data for model in models: assert "operations" not in model assert "model" in model assert "name" in model @pytest.mark.asyncio async def test_list_models_yolo_full_mode( self, config_yolo_full, mock_connection, mock_access_controller, mock_app ): """Test list_models in full access YOLO mode.""" # Setup mock data mock_connection.search_read.return_value = [ {"model": "res.partner", "name": "Contact"}, {"model": "account.move", "name": "Journal Entry"}, ] # Create handler handler = OdooToolHandler( mock_app, mock_connection, mock_access_controller, config_yolo_full ) # Call the method result = await handler._handle_list_models_tool() # Check result structure assert "yolo_mode" in result assert "models" in result assert "total" in result # Check YOLO mode metadata yolo_meta = result["yolo_mode"] assert yolo_meta["enabled"] is True assert yolo_meta["level"] == "true" assert "FULL ACCESS" in yolo_meta["description"] assert "🚨" in yolo_meta["warning"] assert yolo_meta["operations"]["read"] is True assert yolo_meta["operations"]["write"] is True assert yolo_meta["operations"]["create"] is True assert yolo_meta["operations"]["unlink"] is True # Check actual models are clean (no operations field) models = result["models"] assert len(models) == 2 # Should match mock data for model in models: assert "operations" not in model assert "model" in model assert "name" in model @pytest.mark.asyncio async def test_list_models_standard_mode( self, config_standard, mock_connection, mock_access_controller, mock_app ): """Test list_models in standard mode uses MCP access controller.""" # Setup mock data mock_access_controller.get_enabled_models.return_value = [ {"model": "res.partner", "name": "Contact"}, {"model": "res.users", "name": "Users"}, ] def mock_get_permissions(model): mock_perm = MagicMock() mock_perm.can_read = True mock_perm.can_write = True mock_perm.can_create = False mock_perm.can_unlink = False return mock_perm mock_access_controller.get_model_permissions.side_effect = mock_get_permissions # Create handler handler = OdooToolHandler( mock_app, mock_connection, mock_access_controller, config_standard ) # Call the method result = await handler._handle_list_models_tool() # Verify connection was NOT called (standard mode uses access controller) mock_connection.search_read.assert_not_called() # Verify access controller was called mock_access_controller.get_enabled_models.assert_called_once() # Check result structure assert "models" in result models = result["models"] assert len(models) == 2 # No YOLO warning in standard mode for model in models: assert "YOLO MODE" not in model["model"] assert model["model"] in ["res.partner", "res.users"] @pytest.mark.asyncio async def test_list_models_yolo_error_handling( self, config_yolo_read, mock_connection, mock_access_controller, mock_app ): """Test error handling in YOLO mode model listing.""" # Setup connection to raise error mock_connection.search_read.side_effect = Exception("Database connection failed") # Create handler handler = OdooToolHandler( mock_app, mock_connection, mock_access_controller, config_yolo_read ) # Call the method result = await handler._handle_list_models_tool() # Check error response structure assert "yolo_mode" in result assert "models" in result assert "error" in result # Check YOLO mode metadata in error case yolo_meta = result["yolo_mode"] assert yolo_meta["enabled"] is True assert yolo_meta["level"] == "read" assert "Error querying models" in yolo_meta["warning"] assert yolo_meta["operations"]["read"] is False assert yolo_meta["operations"]["write"] is False # Models should be empty on error assert result["models"] == [] assert result["total"] == 0 assert "Database connection failed" in result["error"] @pytest.mark.asyncio async def test_list_models_yolo_domain_construction( self, config_yolo_read, mock_connection, mock_access_controller, mock_app ): """Test that domain is properly constructed in YOLO mode.""" mock_connection.search_read.return_value = [] # Create handler handler = OdooToolHandler( mock_app, mock_connection, mock_access_controller, config_yolo_read ) # Call the method await handler._handle_list_models_tool() # Verify the domain passed to search_read call_args = mock_connection.search_read.call_args domain = call_args[0][1] # Check domain structure assert isinstance(domain, list) # Should filter out transient models assert ("transient", "=", False) in domain # Should have complex domain with OR conditions assert "&" in domain or "|" in domain @pytest.mark.asyncio async def test_list_models_yolo_includes_common_system_models( self, config_yolo_full, mock_connection, mock_access_controller, mock_app ): """Test that common system models are included in YOLO mode.""" mock_connection.search_read.return_value = [ {"model": "res.partner", "name": "Contact"}, {"model": "ir.attachment", "name": "Attachment"}, {"model": "ir.model", "name": "Models"}, ] # Create handler handler = OdooToolHandler( mock_app, mock_connection, mock_access_controller, config_yolo_full ) # Call the method result = await handler._handle_list_models_tool() # Check that system models are included models = result["models"] model_names = [m["model"] for m in models] assert "ir.attachment" in model_names assert "ir.model" in model_names @pytest.mark.asyncio async def test_yolo_mode_logging( self, config_yolo_read, mock_connection, mock_access_controller, mock_app, caplog ): """Test that appropriate logging occurs in YOLO mode.""" import logging # Set logging level to capture INFO messages caplog.set_level(logging.INFO) mock_connection.search_read.return_value = [ {"model": "res.partner", "name": "Contact"}, ] # Create handler handler = OdooToolHandler( mock_app, mock_connection, mock_access_controller, config_yolo_read ) # Call the method await handler._handle_list_models_tool() # Check logs assert "YOLO mode (READ-ONLY)" in caplog.text assert "Listed 1 models from database" in caplog.text if __name__ == "__main__": pytest.main([__file__, "-v"])

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