Skip to main content
Glama

MCP Server for Odoo

by ivnvxd
Mozilla Public License 2.0
88
  • Apple
  • Linux
test_access_control.py•17.5 kB
"""Tests for access control integration with Odoo MCP module. This module tests the AccessController class and its integration with the Odoo MCP module's REST API endpoints. """ import json import os import socket import urllib.error from unittest.mock import MagicMock, patch import pytest from mcp_server_odoo.access_control import ( AccessControlError, AccessController, ) from mcp_server_odoo.config import OdooConfig 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 TestAccessControl: """Test access control functionality.""" @pytest.fixture def config(self): """Create test configuration.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key="test_api_key", database=os.getenv("ODOO_DB"), ) @pytest.fixture def controller(self, config): """Create AccessController instance.""" return AccessController(config, cache_ttl=60) def test_init_without_api_key(self): """Test initialization fails without API key.""" config = 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"), ) with pytest.raises(AccessControlError, match="API key required"): AccessController(config) @patch("urllib.request.urlopen") def test_make_request_success(self, mock_urlopen, controller): """Test successful REST API request.""" # Mock response mock_response = MagicMock() mock_response.read.return_value = json.dumps( {"success": True, "data": {"test": "value"}} ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Make request result = controller._make_request("/test/endpoint") assert result["success"] is True assert result["data"]["test"] == "value" @patch("urllib.request.urlopen") def test_make_request_api_error(self, mock_urlopen, controller): """Test REST API request with API error response.""" # Mock error response mock_response = MagicMock() mock_response.read.return_value = json.dumps( {"success": False, "error": {"message": "Test error"}} ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Should raise error with pytest.raises(AccessControlError, match="API error: Test error"): controller._make_request("/test/endpoint") @patch("urllib.request.urlopen") def test_make_request_http_401(self, mock_urlopen, controller): """Test REST API request with 401 error.""" mock_urlopen.side_effect = urllib.error.HTTPError(None, 401, "Unauthorized", {}, None) with pytest.raises(AccessControlError, match="Invalid API key"): controller._make_request("/test/endpoint") @patch("urllib.request.urlopen") def test_make_request_http_404(self, mock_urlopen, controller): """Test REST API request with 404 error.""" mock_urlopen.side_effect = urllib.error.HTTPError(None, 404, "Not Found", {}, None) with pytest.raises(AccessControlError, match="Endpoint not found"): controller._make_request("/test/endpoint") def test_cache_operations(self, controller): """Test cache get/set operations.""" # Test cache miss assert controller._get_from_cache("test_key") is None # Test cache set and hit controller._set_cache("test_key", {"data": "value"}) assert controller._get_from_cache("test_key") == {"data": "value"} # Test cache clear controller.clear_cache() assert controller._get_from_cache("test_key") is None def test_cache_expiration(self, controller): """Test cache expiration.""" # Set cache with short TTL controller.cache_ttl = 0 # Immediate expiration controller._set_cache("test_key", "value") # Should be expired assert controller._get_from_cache("test_key") is None @patch("urllib.request.urlopen") def test_get_enabled_models(self, mock_urlopen, controller): """Test getting enabled models list.""" # Mock response mock_response = MagicMock() mock_response.read.return_value = json.dumps( { "success": True, "data": { "models": [ {"model": "res.partner", "name": "Contact"}, {"model": "res.users", "name": "Users"}, ] }, } ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Get models models = controller.get_enabled_models() assert len(models) == 2 assert models[0]["model"] == "res.partner" assert models[1]["name"] == "Users" # Second call should use cache models2 = controller.get_enabled_models() assert models2 == models mock_urlopen.assert_called_once() # Only called once due to cache @patch("urllib.request.urlopen") def test_is_model_enabled(self, mock_urlopen, controller): """Test checking if model is enabled.""" # Mock response mock_response = MagicMock() mock_response.read.return_value = json.dumps( { "success": True, "data": { "models": [ {"model": "res.partner", "name": "Contact"}, {"model": "res.users", "name": "Users"}, ] }, } ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Check models assert controller.is_model_enabled("res.partner") is True assert controller.is_model_enabled("res.users") is True assert controller.is_model_enabled("account.move") is False @patch("urllib.request.urlopen") def test_get_model_permissions(self, mock_urlopen, controller): """Test getting model permissions.""" # Mock response mock_response = MagicMock() mock_response.read.return_value = json.dumps( { "success": True, "data": { "model": "res.partner", "enabled": True, "operations": {"read": True, "write": True, "create": False, "unlink": False}, }, } ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Get permissions perms = controller.get_model_permissions("res.partner") assert perms.model == "res.partner" assert perms.enabled is True assert perms.can_read is True assert perms.can_write is True assert perms.can_create is False assert perms.can_unlink is False # Test can_perform method assert perms.can_perform("read") is True assert perms.can_perform("write") is True assert perms.can_perform("create") is False assert perms.can_perform("delete") is False # Alias for unlink @patch("urllib.request.urlopen") def test_check_operation_allowed(self, mock_urlopen, controller): """Test checking if operation is allowed.""" # Mock response mock_response = MagicMock() mock_response.read.return_value = json.dumps( { "success": True, "data": { "model": "res.partner", "enabled": True, "operations": {"read": True, "write": False, "create": False, "unlink": False}, }, } ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Check operations allowed, msg = controller.check_operation_allowed("res.partner", "read") assert allowed is True assert msg is None allowed, msg = controller.check_operation_allowed("res.partner", "write") assert allowed is False assert "Operation 'write' not allowed" in msg @patch("urllib.request.urlopen") def test_check_operation_model_disabled(self, mock_urlopen, controller): """Test checking operation on disabled model.""" # Mock response mock_response = MagicMock() mock_response.read.return_value = json.dumps( {"success": True, "data": {"model": "res.partner", "enabled": False, "operations": {}}} ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Check operation allowed, msg = controller.check_operation_allowed("res.partner", "read") assert allowed is False assert "not enabled for MCP access" in msg @patch("urllib.request.urlopen") def test_validate_model_access(self, mock_urlopen, controller): """Test validate_model_access method.""" # Mock allowed response mock_response = MagicMock() mock_response.read.return_value = json.dumps( { "success": True, "data": {"model": "res.partner", "enabled": True, "operations": {"read": True}}, } ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Should not raise for allowed operation controller.validate_model_access("res.partner", "read") # Mock denied response mock_response.read.return_value = json.dumps( { "success": True, "data": {"model": "res.partner", "enabled": True, "operations": {"read": False}}, } ).encode("utf-8") # Clear cache to force new request controller.clear_cache() # Should raise for denied operation with pytest.raises(AccessControlError): controller.validate_model_access("res.partner", "read") @patch("urllib.request.urlopen") def test_filter_enabled_models(self, mock_urlopen, controller): """Test filtering enabled models.""" # Mock response mock_response = MagicMock() mock_response.read.return_value = json.dumps( { "success": True, "data": { "models": [ {"model": "res.partner", "name": "Contact"}, {"model": "res.users", "name": "Users"}, ] }, } ).encode("utf-8") mock_urlopen.return_value.__enter__.return_value = mock_response # Filter models models = ["res.partner", "account.move", "res.users", "stock.picking"] filtered = controller.filter_enabled_models(models) assert filtered == ["res.partner", "res.users"] @patch("urllib.request.urlopen") def test_get_all_permissions(self, mock_urlopen, controller): """Test getting permissions for all models.""" # Mock models list response models_response = MagicMock() models_response.read.return_value = json.dumps( { "success": True, "data": { "models": [ {"model": "res.partner", "name": "Contact"}, {"model": "res.users", "name": "Users"}, ] }, } ).encode("utf-8") # Mock permissions responses partner_response = MagicMock() partner_response.read.return_value = json.dumps( { "success": True, "data": { "model": "res.partner", "enabled": True, "operations": {"read": True, "write": True}, }, } ).encode("utf-8") users_response = MagicMock() users_response.read.return_value = json.dumps( { "success": True, "data": { "model": "res.users", "enabled": True, "operations": {"read": True, "write": False}, }, } ).encode("utf-8") # Configure mock to return different responses mock_urlopen.return_value.__enter__.side_effect = [ models_response, partner_response, users_response, ] # Get all permissions all_perms = controller.get_all_permissions() assert len(all_perms) == 2 assert all_perms["res.partner"].can_write is True assert all_perms["res.users"].can_write is False @pytest.mark.skipif( not is_odoo_server_running(), reason="Odoo server not running at localhost:8069" ) class TestAccessControlIntegration: """Integration tests with real Odoo server.""" @pytest.fixture def real_config(self): """Create configuration with real credentials.""" return OdooConfig( url=os.getenv("ODOO_URL", "http://localhost:8069"), api_key=os.getenv("ODOO_API_KEY"), database=os.getenv("ODOO_DB"), ) def test_real_get_enabled_models(self, real_config): """Test getting enabled models from real server.""" controller = AccessController(real_config) models = controller.get_enabled_models() assert isinstance(models, list) print(f"Found {len(models)} enabled models") # Just verify we got some models if models: # Print first few models as example for model in models[:3]: print(f" - {model.get('model', 'unknown')}") def test_real_model_permissions(self, real_config, readable_model): """Test getting model permissions from real server.""" controller = AccessController(real_config) # Use the discovered readable model model_name = readable_model.model # Get model permissions perms = controller.get_model_permissions(model_name) assert perms.model == model_name assert perms.enabled is True assert perms.can_read is True # We specifically requested a readable model print( f"{model_name} permissions: read={perms.can_read}, " f"write={perms.can_write}, create={perms.can_create}, " f"unlink={perms.can_unlink}" ) def test_real_check_operations(self, real_config, readable_model, disabled_model): """Test checking operations on real server.""" controller = AccessController(real_config) # Check enabled model operations allowed, msg = controller.check_operation_allowed(readable_model.model, "read") print(f"{readable_model.model} read: allowed={allowed}, msg={msg}") assert allowed is True # Check a model we know is not enabled allowed, msg = controller.check_operation_allowed(disabled_model, "read") print(f"{disabled_model} read: allowed={allowed}, msg={msg}") assert allowed is False def test_real_validate_access(self, real_config, readable_model, disabled_model): """Test access validation on real server.""" controller = AccessController(real_config) # Should not raise for enabled model with permission try: controller.validate_model_access(readable_model.model, "read") print(f"{readable_model.model} read access validated") except AccessControlError as e: print(f"{readable_model.model} read access denied: {e}") # Should raise for non-enabled model with pytest.raises(AccessControlError): controller.validate_model_access(disabled_model, "read") def test_real_cache_performance(self, real_config): """Test cache improves performance.""" controller = AccessController(real_config) import time # First call - no cache start = time.time() models1 = controller.get_enabled_models() time1 = time.time() - start # Second call - from cache start = time.time() models2 = controller.get_enabled_models() time2 = time.time() - start assert models1 == models2 assert time2 < time1 # Cache should be faster print(f"First call: {time1:.3f}s, Cached call: {time2:.3f}s") def test_real_all_permissions(self, real_config): """Test getting all permissions from real server.""" controller = AccessController(real_config) all_perms = controller.get_all_permissions() print(f"Retrieved permissions for {len(all_perms)} models") # Print a sample for model, perms in list(all_perms.items())[:3]: print(f"{model}: read={perms.can_read}, write={perms.can_write}") 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