"""Pytest configuration and fixtures for Odoo MCP Server tests."""
import os
import socket
import xmlrpc.client
import pytest
from dotenv import load_dotenv
from mcp_server_odoo.config import OdooConfig
# Load .env file for tests
load_dotenv()
# Import model discovery helper
try:
from tests.helpers.model_discovery import ModelDiscovery
MODEL_DISCOVERY_AVAILABLE = True
except ImportError:
MODEL_DISCOVERY_AVAILABLE = False
def is_odoo_server_available(host: str = "localhost", port: int = 8069) -> bool:
"""Check if Odoo server is available at the given host and port."""
try:
# Try to connect to the server
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
result = sock.connect_ex((host, port))
sock.close()
if result != 0:
return False
# Try to access the XML-RPC endpoint
try:
proxy = xmlrpc.client.ServerProxy(f"http://{host}:{port}/xmlrpc/2/common")
proxy.version()
return True
except Exception:
return False
except Exception:
return False
# Global flag for Odoo server availability — derive host/port from ODOO_URL
def _parse_odoo_host_port() -> tuple[str, int]:
from urllib.parse import urlparse
url = os.getenv("ODOO_URL", "http://localhost:8069")
parsed = urlparse(url)
return parsed.hostname or "localhost", parsed.port or 8069
_host, _port = _parse_odoo_host_port()
ODOO_SERVER_AVAILABLE = is_odoo_server_available(_host, _port)
def pytest_configure(config):
"""Configure pytest with custom markers."""
config.addinivalue_line("markers", "yolo: needs running Odoo instance (vanilla XML-RPC)")
config.addinivalue_line("markers", "mcp: needs running Odoo with MCP module installed")
def pytest_collection_modifyitems(config, items):
"""Modify test collection to skip tests that require Odoo when it's not available."""
if ODOO_SERVER_AVAILABLE:
# Server is available, don't skip anything
return
skip_odoo = pytest.mark.skip(reason=f"Odoo server not available at {_host}:{_port}")
for item in items:
if "yolo" in item.keywords or "mcp" in item.keywords:
item.add_marker(skip_odoo)
@pytest.fixture(autouse=True)
def rate_limit_delay(request):
"""Add a delay between tests to avoid rate limiting (only when needed)."""
if "yolo" in request.keywords or "mcp" in request.keywords:
import time
time.sleep(0.5) # Brief delay before integration tests to avoid rate limiting
yield
@pytest.fixture
def odoo_server_required():
"""Fixture that skips test if Odoo server is not available."""
if not ODOO_SERVER_AVAILABLE:
pytest.skip(f"Odoo server not available at {_host}:{_port}")
@pytest.fixture
def handle_rate_limit():
"""Fixture that handles rate limiting errors gracefully."""
import urllib.error
try:
yield
except Exception as e:
# Check if this is a rate limit error
if isinstance(e, urllib.error.HTTPError) and e.code == 429:
pytest.skip("Skipping due to rate limiting")
elif "429" in str(e) or "TOO MANY REQUESTS" in str(e):
pytest.skip("Skipping due to rate limiting")
else:
raise
@pytest.fixture
def test_config_with_server_check(odoo_server_required) -> OdooConfig:
"""Create test configuration, but skip if server not available."""
# Require environment variables to be set
if not os.getenv("ODOO_URL"):
pytest.skip("ODOO_URL environment variable not set. Please configure .env file.")
if not os.getenv("ODOO_API_KEY") and not os.getenv("ODOO_PASSWORD"):
pytest.skip("Neither ODOO_API_KEY nor ODOO_PASSWORD set. Please configure .env file.")
return OdooConfig(
url=os.getenv("ODOO_URL"),
api_key=os.getenv("ODOO_API_KEY") or None,
username=os.getenv("ODOO_USER") or None,
password=os.getenv("ODOO_PASSWORD") or None,
database=os.getenv("ODOO_DB"), # DB can be auto-detected
log_level=os.getenv("ODOO_MCP_LOG_LEVEL", "INFO"),
default_limit=int(os.getenv("ODOO_MCP_DEFAULT_LIMIT", "10")),
max_limit=int(os.getenv("ODOO_MCP_MAX_LIMIT", "100")),
)
# MCP Model Discovery Fixtures
# These fixtures help make tests model-agnostic by discovering
# and adapting to whatever models are currently available
@pytest.fixture
def model_discovery():
"""Create a model discovery helper.
Creates a fresh discovery instance for each test.
"""
if not MODEL_DISCOVERY_AVAILABLE:
pytest.skip("Model Discovery not available")
if not ODOO_SERVER_AVAILABLE:
pytest.skip("Odoo server not available")
# Create config for discovery
config = OdooConfig(
url=os.getenv("ODOO_URL"),
api_key=os.getenv("ODOO_API_KEY") or None,
username=os.getenv("ODOO_USER") or None,
password=os.getenv("ODOO_PASSWORD") or None,
database=os.getenv("ODOO_DB"),
)
discovery = ModelDiscovery(config)
return discovery
@pytest.fixture
def readable_model(model_discovery):
"""Get a model with read permission.
Skips test if no readable models are available.
"""
return model_discovery.require_readable_model()
@pytest.fixture
def writable_model(model_discovery):
"""Get a model with write permission.
Skips test if no writable models are available.
"""
return model_discovery.require_writable_model()
@pytest.fixture
def disabled_model(model_discovery):
"""Get a model name that is NOT enabled.
Returns a model that should fail access checks.
"""
return model_discovery.get_disabled_model()
@pytest.fixture
def test_models(model_discovery):
"""Get commonly available models for testing.
Returns a list of models that are commonly enabled,
or skips if none are available.
"""
models = model_discovery.get_common_models()
if not models:
models = [model_discovery.require_readable_model()]
return models