Skip to main content
Glama

MCP Server for Odoo

test_e2e_yolo.py•19.9 kB
"""End-to-end tests for YOLO mode functionality. This module tests complete YOLO mode workflows with real Odoo instances. Tests are marked with @pytest.mark.e2e and require a running Odoo instance. """ import os import socket import time from unittest.mock import MagicMock import pytest from mcp_server_odoo.access_control import AccessController from mcp_server_odoo.config import OdooConfig from mcp_server_odoo.odoo_connection import OdooConnection from mcp_server_odoo.tools import OdooToolHandler def is_odoo_server_running(host="localhost", port=8069): """Check if Odoo server is running.""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(1) try: result = sock.connect_ex((host, port)) return result == 0 except Exception: return False finally: sock.close() @pytest.mark.skipif( not is_odoo_server_running(), reason="Odoo server not running at localhost:8069" ) @pytest.mark.e2e class TestYoloModeE2E: """End-to-end tests for YOLO mode with real Odoo.""" @pytest.fixture def config_read_only(self): """Create configuration for read-only YOLO mode.""" return OdooConfig( url="http://localhost:8069", database=os.getenv("ODOO_DB", "mcp-18"), username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), yolo_mode="read", ) @pytest.fixture def config_full_access(self): """Create configuration for full access YOLO mode.""" return OdooConfig( url="http://localhost:8069", database=os.getenv("ODOO_DB", "mcp-18"), username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), yolo_mode="true", ) @pytest.fixture def config_standard(self): """Create configuration for standard mode.""" return OdooConfig( url="http://localhost:8069", database=os.getenv("ODOO_DB", "mcp-18"), api_key=os.getenv("ODOO_API_KEY"), ) @pytest.mark.asyncio async def test_yolo_complete_workflow_read_only(self, config_read_only): """Test complete workflow in read-only YOLO mode.""" # 1. Connect and authenticate connection = OdooConnection(config_read_only) connection.connect() connection.authenticate() assert connection.is_authenticated, "Failed to authenticate in read-only mode" # Setup tool handler app = MagicMock() access_controller = AccessController(config_read_only) handler = OdooToolHandler(app, connection, access_controller, config_read_only) # 2. List models - should work and show indicator models_result = await handler._handle_list_models_tool() assert "models" in models_result assert "yolo_mode" in models_result assert len(models_result["models"]) > 0 # Check for YOLO metadata yolo_meta = models_result["yolo_mode"] assert yolo_meta["enabled"] is True assert yolo_meta["level"] == "read" assert "READ-ONLY" in yolo_meta["description"] # 3. Search records - should work search_result = await handler._handle_search_tool( model="res.partner", domain=[], fields=["id", "name", "email"], limit=5, offset=0, order=None, ) assert "records" in search_result assert "total" in search_result assert search_result["total"] >= 0 # 4. Get a specific record - should work if search_result["records"]: first_record = search_result["records"][0] get_result = await handler._handle_get_record_tool( model="res.partner", record_id=first_record["id"], fields=None, ) assert "id" in get_result assert get_result["id"] == first_record["id"] # 5. Attempt to create record - should fail with pytest.raises(Exception) as exc_info: await handler._handle_create_record_tool( model="res.partner", values={"name": "YOLO Test Partner - Should Fail"}, ) assert "not allowed in read-only" in str(exc_info.value).lower() # 6. Attempt to update record - should fail if search_result["records"]: with pytest.raises(Exception) as exc_info: await handler._handle_update_record_tool( model="res.partner", record_id=first_record["id"], values={"email": "test@fail.com"}, ) assert "not allowed in read-only" in str(exc_info.value).lower() # 7. Attempt to delete record - should fail if search_result["records"]: with pytest.raises(Exception) as exc_info: await handler._handle_delete_record_tool( model="res.partner", record_id=first_record["id"], ) assert "not allowed in read-only" in str(exc_info.value).lower() connection.disconnect() @pytest.mark.asyncio async def test_yolo_complete_workflow_full_access(self, config_full_access): """Test complete workflow in full access YOLO mode.""" # 1. Connect and authenticate connection = OdooConnection(config_full_access) connection.connect() connection.authenticate() assert connection.is_authenticated, "Failed to authenticate in full access mode" # Setup tool handler app = MagicMock() access_controller = AccessController(config_full_access) handler = OdooToolHandler(app, connection, access_controller, config_full_access) # 2. List models - should work and show warning models_result = await handler._handle_list_models_tool() assert "models" in models_result assert "yolo_mode" in models_result # Check for YOLO metadata yolo_meta = models_result["yolo_mode"] assert yolo_meta["enabled"] is True assert yolo_meta["level"] == "true" assert "FULL ACCESS" in yolo_meta["description"] # 3. Create a test record - should work create_result = await handler._handle_create_record_tool( model="res.partner", values={ "name": "YOLO E2E Test Partner", "email": "yolo.e2e@test.com", "is_company": True, }, ) # The result has a different structure assert create_result["success"] is True assert "record" in create_result created_id = create_result["record"]["id"] assert created_id > 0 # 4. Search for the created record - should work search_result = await handler._handle_search_tool( model="res.partner", domain=[["id", "=", created_id]], fields=["id", "name", "email"], limit=1, offset=0, order=None, ) assert search_result["total"] == 1 assert search_result["records"][0]["name"] == "YOLO E2E Test Partner" # 5. Update the record - should work update_result = await handler._handle_update_record_tool( model="res.partner", record_id=created_id, values={"email": "updated.yolo@test.com", "phone": "+1234567890"}, ) assert update_result["success"] is True # 6. Verify update get_result = await handler._handle_get_record_tool( model="res.partner", record_id=created_id, fields=["id", "name", "email", "phone"], ) assert get_result["email"] == "updated.yolo@test.com" assert get_result["phone"] == "+1234567890" # 7. Delete the record - should work delete_result = await handler._handle_delete_record_tool( model="res.partner", record_id=created_id, ) assert delete_result["success"] is True # 8. Verify deletion search_after_delete = await handler._handle_search_tool( model="res.partner", domain=[["id", "=", created_id]], fields=["id"], limit=1, offset=0, order=None, ) assert search_after_delete["total"] == 0 connection.disconnect() @pytest.mark.asyncio async def test_model_access_different_types(self, config_full_access): """Test accessing different types of models in YOLO mode.""" connection = OdooConnection(config_full_access) connection.connect() connection.authenticate() app = MagicMock() access_controller = AccessController(config_full_access) handler = OdooToolHandler(app, connection, access_controller, config_full_access) # Test standard models standard_models = ["res.partner", "res.users", "res.company"] for model in standard_models: result = await handler._handle_search_tool( model=model, domain=[], fields=["id"], limit=1, offset=0, order=None, ) assert "records" in result, f"Failed to access standard model: {model}" # Test system models (usually restricted in standard mode) system_models = ["ir.model", "ir.model.fields", "ir.config_parameter"] for model in system_models: result = await handler._handle_search_tool( model=model, domain=[], fields=["id"], limit=1, offset=0, order=None, ) assert "records" in result, f"Failed to access system model: {model}" # Test accounting models if available try: account_result = await handler._handle_search_tool( model="account.account", domain=[], fields=["id", "name"], limit=1, offset=0, order=None, ) assert "records" in account_result except Exception: # Accounting module might not be installed pass connection.disconnect() @pytest.mark.asyncio async def test_error_handling(self, config_full_access): """Test error handling in YOLO mode.""" connection = OdooConnection(config_full_access) connection.connect() connection.authenticate() app = MagicMock() access_controller = AccessController(config_full_access) handler = OdooToolHandler(app, connection, access_controller, config_full_access) # Test invalid model name with pytest.raises(Exception) as exc_info: await handler._handle_search_tool( model="invalid.model.name", domain=[], fields=["id"], limit=1, offset=0, order=None, ) # Should get Odoo error, not MCP error assert ( "invalid.model.name" in str(exc_info.value) or "not found" in str(exc_info.value).lower() ) # Test invalid field name with pytest.raises(Exception) as exc_info: await handler._handle_search_tool( model="res.partner", domain=[], fields=["id", "invalid_field_xyz"], limit=1, offset=0, order=None, ) assert "invalid_field_xyz" in str(exc_info.value) or "field" in str(exc_info.value).lower() # Test invalid record ID with pytest.raises(Exception) as exc_info: await handler._handle_get_record_tool( model="res.partner", record_id=999999999, # Very unlikely to exist fields=["id", "name"], ) # Should indicate record not found # Test creating record with missing required fields with pytest.raises(Exception) as exc_info: await handler._handle_create_record_tool( model="res.users", # Requires login field values={"name": "Test User Without Login"}, ) # Should get validation error from Odoo connection.disconnect() @pytest.mark.asyncio async def test_mode_indicators_in_responses(self, config_read_only, config_full_access): """Test that mode indicators appear correctly in responses.""" # Test read-only mode indicators connection = OdooConnection(config_read_only) connection.connect() connection.authenticate() app = MagicMock() access_controller = AccessController(config_read_only) handler = OdooToolHandler(app, connection, access_controller, config_read_only) # Check list_models indicator models_result = await handler._handle_list_models_tool() yolo_meta = models_result["yolo_mode"] assert "READ-ONLY" in yolo_meta["description"] assert yolo_meta["operations"]["read"] is True assert yolo_meta["operations"]["write"] is False connection.disconnect() # Test full access mode indicators connection = OdooConnection(config_full_access) connection.connect() connection.authenticate() access_controller = AccessController(config_full_access) handler = OdooToolHandler(app, connection, access_controller, config_full_access) # Check list_models indicator models_result = await handler._handle_list_models_tool() yolo_meta = models_result["yolo_mode"] assert "FULL ACCESS" in yolo_meta["description"] assert all( [ yolo_meta["operations"]["read"], yolo_meta["operations"]["write"], yolo_meta["operations"]["create"], yolo_meta["operations"]["unlink"], ] ) connection.disconnect() @pytest.mark.asyncio async def test_performance_comparison(self, config_full_access): """Test performance of YOLO mode operations.""" connection = OdooConnection(config_full_access) connection.connect() connection.authenticate() app = MagicMock() access_controller = AccessController(config_full_access) handler = OdooToolHandler(app, connection, access_controller, config_full_access) # Measure list_models performance start_time = time.time() models_result = await handler._handle_list_models_tool() list_models_time = time.time() - start_time assert list_models_time < 2.0, f"list_models took too long: {list_models_time:.2f}s" assert len(models_result["models"]) > 50, "Should list many models in YOLO mode" # Measure search performance start_time = time.time() search_result = await handler._handle_search_tool( model="res.partner", domain=[], fields=["id", "name"], limit=100, offset=0, order="id ASC", ) search_time = time.time() - start_time assert search_time < 1.0, f"Search took too long: {search_time:.2f}s" # Measure bulk operations performance if search_result["total"] >= 10: # Read 10 records individually start_time = time.time() for record in search_result["records"][:10]: await handler._handle_get_record_tool( model="res.partner", record_id=record["id"], fields=["id", "name", "email"], ) bulk_read_time = time.time() - start_time assert bulk_read_time < 2.0, f"Bulk read took too long: {bulk_read_time:.2f}s" connection.disconnect() @pytest.mark.asyncio async def test_explicit_opt_in_requirement(self): """Test that only 'true' enables full access, not other truthy values.""" # Test valid YOLO modes valid_cases = [ ("true", True), # Should enable full access ("read", False), # Should be read-only ("off", False), # Should be disabled (standard mode) ] for value, should_allow_write in valid_cases: config = OdooConfig( url="http://localhost:8069", database=os.getenv("ODOO_DB", "mcp-18"), username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), yolo_mode=value, ) if value in ["true", "read"]: # These enable YOLO mode assert config.is_yolo_enabled if value == "true": assert config.yolo_mode == "true" # Check permissions allow write access_controller = AccessController(config) allowed, _ = access_controller.check_operation_allowed("res.partner", "write") assert ( allowed == should_allow_write ), f"Value '{value}' should {'allow' if should_allow_write else 'not allow'} write" else: assert config.yolo_mode == "read" # Check permissions block write access_controller = AccessController(config) allowed, _ = access_controller.check_operation_allowed("res.partner", "write") assert not allowed, "Read mode should not allow write" else: # "off" mode assert not config.is_yolo_enabled assert config.yolo_mode == "off" # Test invalid YOLO mode values - should raise ValueError invalid_cases = [ "True", # Wrong case "1", # Not accepted "yes", # Not accepted "on", # Not accepted "false", # Not accepted "full", # Not accepted "", # Empty string ] for value in invalid_cases: with pytest.raises(ValueError, match="Invalid YOLO mode"): OdooConfig( url="http://localhost:8069", database=os.getenv("ODOO_DB", "mcp-18"), username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), yolo_mode=value, ) @pytest.mark.asyncio async def test_no_mcp_module_required(self, config_full_access): """Test that YOLO mode works without MCP module installed in Odoo.""" # This test verifies YOLO mode connects to standard endpoints connection = OdooConnection(config_full_access) # Check that we're using standard endpoints assert connection.COMMON_ENDPOINT == "/xmlrpc/2/common" assert connection.OBJECT_ENDPOINT == "/xmlrpc/2/object" assert connection.DB_ENDPOINT == "/xmlrpc/db" # Should be able to connect and work connection.connect() connection.authenticate() assert connection.is_authenticated # Should be able to perform operations result = connection.search_read( "res.partner", [["is_company", "=", True]], ["id", "name"], limit=5, ) assert isinstance(result, list) connection.disconnect() if __name__ == "__main__": pytest.main([__file__, "-v", "-s"])

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