Skip to main content
Glama

MCP Server for Odoo

by ivnvxd
Mozilla Public License 2.0
88
  • Apple
  • Linux
test_authentication.py•21.7 kB
"""Tests for authentication functionality in OdooConnection. This module tests both API key and username/password authentication flows. """ import json import os import socket import urllib.error from unittest.mock import MagicMock, Mock, patch from xmlrpc.client import Fault import pytest from mcp_server_odoo.config import OdooConfig from mcp_server_odoo.odoo_connection import OdooConnection, OdooConnectionError 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() class TestAuthentication: """Test authentication functionality.""" @pytest.fixture def config_api_key(self): """Create configuration with API key.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key="test_api_key", database=os.getenv("ODOO_DB"), ) @pytest.fixture def config_password(self): """Create configuration with username/password.""" 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"), ) @pytest.fixture def config_both(self): """Create configuration with both auth methods.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key="test_api_key", username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), database=os.getenv("ODOO_DB"), ) @pytest.fixture def connection_api_key(self, config_api_key): """Create connection with API key config.""" conn = OdooConnection(config_api_key) conn._connected = True return conn @pytest.fixture def connection_password(self, config_password): """Create connection with password config.""" conn = OdooConnection(config_password) conn._connected = True return conn def test_authenticate_not_connected(self, config_api_key): """Test authenticate raises error when not connected.""" conn = OdooConnection(config_api_key) with pytest.raises(OdooConnectionError, match="Not connected"): conn.authenticate() @patch("urllib.request.urlopen") def test_api_key_authentication_success(self, mock_urlopen, connection_api_key): """Test successful API key authentication.""" # Mock successful API response mock_response = MagicMock() mock_response.read.return_value = json.dumps( {"success": True, "data": {"valid": True, "user_id": 2}} ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Authenticate connection_api_key.authenticate("mcp") # Verify authentication state assert connection_api_key.is_authenticated() assert connection_api_key.uid == 2 assert connection_api_key.database == "mcp" assert connection_api_key.auth_method == "api_key" @patch("urllib.request.urlopen") def test_api_key_authentication_invalid(self, mock_urlopen, connection_api_key): """Test API key authentication with invalid key.""" # Mock 401 response mock_urlopen.side_effect = urllib.error.HTTPError(None, 401, "Unauthorized", {}, None) # Should raise error since no fallback with pytest.raises(OdooConnectionError, match="Authentication failed"): connection_api_key.authenticate("mcp") # Verify not authenticated assert not connection_api_key.is_authenticated() def test_password_authentication_success(self, connection_password): """Test successful username/password authentication.""" # Mock common proxy mock_common = Mock() mock_common.authenticate.return_value = 2 connection_password._common_proxy = mock_common # Authenticate connection_password.authenticate("mcp") # Verify authentication state assert connection_password.is_authenticated() assert connection_password.uid == 2 assert connection_password.database == "mcp" assert connection_password.auth_method == "password" # Verify authenticate was called correctly mock_common.authenticate.assert_called_once_with( "mcp", os.getenv("ODOO_USER", "admin"), os.getenv("ODOO_PASSWORD", "admin"), {} ) def test_password_authentication_failed(self, connection_password): """Test failed username/password authentication.""" # Mock common proxy mock_common = Mock() mock_common.authenticate.return_value = False connection_password._common_proxy = mock_common # Should raise error with pytest.raises(OdooConnectionError, match="Authentication failed"): connection_password.authenticate("mcp") # Verify not authenticated assert not connection_password.is_authenticated() def test_password_authentication_fault(self, connection_password): """Test username/password authentication with XML-RPC fault.""" # Mock common proxy mock_common = Mock() mock_common.authenticate.side_effect = Fault(1, "Access Denied") connection_password._common_proxy = mock_common # Should raise error with pytest.raises(OdooConnectionError, match="Authentication failed"): connection_password.authenticate("mcp") # Verify not authenticated assert not connection_password.is_authenticated() @patch("urllib.request.urlopen") def test_authentication_fallback(self, mock_urlopen, config_both): """Test fallback from API key to username/password.""" # Create connection with both auth methods conn = OdooConnection(config_both) conn._connected = True # Mock failed API key response mock_urlopen.side_effect = urllib.error.HTTPError(None, 401, "Unauthorized", {}, None) # Mock successful password auth mock_common = Mock() mock_common.authenticate.return_value = 3 conn._common_proxy = mock_common # Authenticate - should fallback to password conn.authenticate("mcp") # Verify authenticated with password assert conn.is_authenticated() assert conn.uid == 3 assert conn.auth_method == "password" def test_authenticate_with_auto_database(self, connection_api_key): """Test authentication with automatic database selection.""" # Mock database list to return the configured database mock_db = Mock() db_name = os.getenv("ODOO_DB") mock_db.list.return_value = [db_name] connection_api_key._db_proxy = mock_db # Mock API key auth with patch("urllib.request.urlopen") as mock_urlopen: mock_response = MagicMock() mock_response.read.return_value = json.dumps( {"success": True, "data": {"valid": True, "user_id": 2}} ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Authenticate without specifying database connection_api_key.authenticate() assert connection_api_key.database == db_name def test_authentication_state_cleared_on_disconnect(self, connection_api_key): """Test authentication state is cleared on disconnect.""" # Set authentication state connection_api_key._authenticated = True connection_api_key._uid = 2 connection_api_key._database = "mcp" connection_api_key._auth_method = "api_key" # Disconnect connection_api_key.disconnect() # Verify state cleared assert not connection_api_key.is_authenticated() assert connection_api_key.uid is None assert connection_api_key.database is None assert connection_api_key.auth_method is None @pytest.mark.skipif( not is_odoo_server_running(), reason="Odoo server not running at localhost:8069" ) class TestAuthenticationIntegration: """Integration tests with real Odoo server.""" @pytest.fixture def real_config_api_key(self): """Create configuration with real API key.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key=os.getenv("ODOO_API_KEY"), database=None, # Let it auto-select ) @pytest.fixture def real_config_password(self): """Create configuration with username/password.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), database=None, # Let it auto-select ) def test_real_api_key_authentication(self, real_config_api_key): """Test API key authentication with real server.""" with OdooConnection(real_config_api_key) as conn: # Authenticate conn.authenticate() # Verify authenticated assert conn.is_authenticated() assert conn.uid is not None assert conn.database is not None assert conn.auth_method == "api_key" print(f"Authenticated with API key: uid={conn.uid}, db={conn.database}") def test_real_password_authentication(self, real_config_password): """Test username/password authentication with real server.""" with OdooConnection(real_config_password) as conn: # Authenticate conn.authenticate() # Verify authenticated assert conn.is_authenticated() assert conn.uid is not None assert conn.database is not None assert conn.auth_method == "password" print(f"Authenticated with password: uid={conn.uid}, db={conn.database}") def test_real_invalid_api_key(self): """Test authentication with invalid API key.""" config = OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key="invalid_key_12345", database=os.getenv("ODOO_DB"), ) with OdooConnection(config) as conn: with pytest.raises(OdooConnectionError, match="Authentication failed"): conn.authenticate() def test_real_invalid_password(self): """Test authentication with invalid password.""" config = OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), username=os.getenv("ODOO_USER", "admin"), password="wrong_password", database=os.getenv("ODOO_DB"), ) with OdooConnection(config) as conn: with pytest.raises(OdooConnectionError, match="Authentication failed"): conn.authenticate() class TestYoloModeAuthentication: """Test authentication 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_yolo_api_key(self): """Create configuration for YOLO mode with API key.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), username=os.getenv("ODOO_USER", "admin"), api_key="test_api_key", database=os.getenv("ODOO_DB"), yolo_mode="true", ) def test_yolo_mode_endpoints(self, config_yolo_read): """Test that YOLO mode uses standard Odoo endpoints.""" conn = OdooConnection(config_yolo_read) # Check that standard endpoints are used assert conn.DB_ENDPOINT == "/xmlrpc/db" assert conn.COMMON_ENDPOINT == "/xmlrpc/2/common" assert conn.OBJECT_ENDPOINT == "/xmlrpc/2/object" def test_standard_mode_endpoints(self): """Test that standard mode uses MCP endpoints.""" config = OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key="test_api_key", database=os.getenv("ODOO_DB"), yolo_mode="off", ) conn = OdooConnection(config) # Check that MCP endpoints are used assert conn.DB_ENDPOINT == "/mcp/xmlrpc/db" assert conn.COMMON_ENDPOINT == "/mcp/xmlrpc/common" assert conn.OBJECT_ENDPOINT == "/mcp/xmlrpc/object" def test_yolo_api_key_auth_success(self, config_yolo_api_key): """Test successful API key authentication in YOLO mode.""" conn = OdooConnection(config_yolo_api_key) conn._connected = True # Create a mock for common_proxy mock_proxy = MagicMock() mock_proxy.authenticate.return_value = 2 conn._common_proxy = mock_proxy # Test that API key is used as password in YOLO mode success = conn._authenticate_api_key_standard("testdb") # Should use standard authenticate with API key as password mock_proxy.authenticate.assert_called_once_with( "testdb", config_yolo_api_key.username, config_yolo_api_key.api_key, {} ) assert success is True assert conn.uid == 2 assert conn._auth_method == "api_key" def test_yolo_api_key_auth_failure(self, config_yolo_api_key): """Test failed API key authentication in YOLO mode.""" conn = OdooConnection(config_yolo_api_key) conn._connected = True # Create a mock for common_proxy mock_proxy = MagicMock() mock_proxy.authenticate.return_value = False conn._common_proxy = mock_proxy # Test authentication failure success = conn._authenticate_api_key_standard("testdb") assert success is False assert not conn.is_authenticated() def test_yolo_api_key_auth_xmlrpc_fault(self, config_yolo_api_key): """Test API key authentication with XML-RPC fault in YOLO mode.""" from xmlrpc.client import Fault conn = OdooConnection(config_yolo_api_key) conn._connected = True # Create a mock for common_proxy mock_proxy = MagicMock() mock_proxy.authenticate.side_effect = Fault(1, "Access Denied") conn._common_proxy = mock_proxy # Test authentication with fault success = conn._authenticate_api_key_standard("testdb") assert success is False assert not conn.is_authenticated() def test_yolo_password_auth(self, config_yolo_full): """Test password authentication in YOLO mode.""" conn = OdooConnection(config_yolo_full) conn._connected = True # Create a mock for common_proxy mock_proxy = MagicMock() mock_proxy.authenticate.return_value = 2 conn._common_proxy = mock_proxy # Test password authentication success = conn._authenticate_password("testdb") # Should use standard authenticate mock_proxy.authenticate.assert_called_once_with( "testdb", config_yolo_full.username, config_yolo_full.password, {} ) assert success is True assert conn.uid == 2 assert conn._auth_method == "password" def test_yolo_mode_logging_read(self, config_yolo_read, caplog): """Test that read-only YOLO mode logs appropriate warning.""" import logging with caplog.at_level(logging.WARNING): _ = OdooConnection(config_yolo_read) # Check for read-only warning assert "YOLO MODE: READ-ONLY" in caplog.text assert "Write operations will be blocked" in caplog.text def test_yolo_mode_logging_full(self, config_yolo_full, caplog): """Test that full YOLO mode logs security warning.""" import logging with caplog.at_level(logging.WARNING): _ = OdooConnection(config_yolo_full) # Check for full access warning assert "YOLO MODE: FULL ACCESS" in caplog.text assert "NEVER USE IN PRODUCTION" in caplog.text def test_authentication_routing_standard_mode(self): """Test that standard mode routes to MCP authentication.""" config = OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key="test_api_key", database=os.getenv("ODOO_DB"), yolo_mode="off", ) conn = OdooConnection(config) conn._connected = True # Mock the MCP authentication method with patch.object(conn, "_authenticate_api_key_mcp", return_value=True) as mock_mcp: with patch.object( conn, "_authenticate_api_key_standard", return_value=False ) as mock_std: success = conn._authenticate_api_key("testdb") # Should call MCP method, not standard mock_mcp.assert_called_once_with("testdb") mock_std.assert_not_called() assert success is True def test_authentication_routing_yolo_mode(self, config_yolo_full): """Test that YOLO mode routes to standard authentication.""" conn = OdooConnection(config_yolo_full) conn._connected = True # Mock the authentication methods with patch.object(conn, "_authenticate_api_key_standard", return_value=True) as mock_std: with patch.object(conn, "_authenticate_api_key_mcp", return_value=False) as mock_mcp: # Use API key config for this test conn.config.api_key = "test_key" success = conn._authenticate_api_key("testdb") # Should call standard method, not MCP mock_std.assert_called_once_with("testdb") mock_mcp.assert_not_called() assert success is True def test_authentication_fallback_in_standard_mode(self): """Test fallback from API key to password in standard mode.""" config = OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key="test_api_key", username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), database=os.getenv("ODOO_DB"), yolo_mode="off", ) conn = OdooConnection(config) conn._connected = True # Mock database operations mock_db = Mock() mock_db.list.return_value = ["testdb"] conn._db_proxy = mock_db # Mock authentication methods with patch.object(conn, "_authenticate_api_key", return_value=False): with patch.object(conn, "_authenticate_password", return_value=True) as mock_pwd: # Set authentication state when password auth succeeds def set_auth_state(db): conn._authenticated = True conn._uid = 2 conn._database = db conn._auth_method = "password" return True mock_pwd.side_effect = set_auth_state # Should fallback to password auth conn.authenticate("testdb") mock_pwd.assert_called_once_with("testdb") assert conn.is_authenticated() def test_authentication_error_messages(self): """Test detailed error messages for authentication failures.""" config = OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key="test_api_key", username=os.getenv("ODOO_USER", "admin"), password=os.getenv("ODOO_PASSWORD", "admin"), database=os.getenv("ODOO_DB"), yolo_mode="off", ) conn = OdooConnection(config) conn._connected = True # Mock database operations mock_db = Mock() mock_db.list.return_value = ["testdb"] conn._db_proxy = mock_db # Mock all authentication methods to fail with patch.object(conn, "_authenticate_api_key", return_value=False): with patch.object(conn, "_authenticate_password", return_value=False): # Should raise detailed error with pytest.raises(OdooConnectionError) as exc_info: conn.authenticate("testdb") error_msg = str(exc_info.value) assert "Authentication failed" in error_msg assert "Standard mode" in error_msg assert "MCP module" in error_msg if __name__ == "__main__": # Run integration tests when executed directly pytest.main([__file__, "-v", "-k", "Integration"])

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