404 lines
13 KiB
Python
404 lines
13 KiB
Python
|
|
"""
|
||
|
|
Tests for configuration schema module.
|
||
|
|
|
||
|
|
Following TDD approach - these tests are written before implementation.
|
||
|
|
Tests cover all validation requirements from todo.md section 1.1.1.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from pathlib import Path
|
||
|
|
from pydantic import ValidationError
|
||
|
|
|
||
|
|
|
||
|
|
def test_valid_server_config_loads_successfully():
|
||
|
|
"""Test that a valid ServerConfig loads without errors."""
|
||
|
|
from mcp_forge.config.schema import ServerConfig
|
||
|
|
|
||
|
|
config = ServerConfig(
|
||
|
|
host="0.0.0.0",
|
||
|
|
port=3000,
|
||
|
|
podman_socket=Path("/run/user/1000/podman/podman.sock")
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.host == "0.0.0.0"
|
||
|
|
assert config.port == 3000
|
||
|
|
assert config.podman_socket == Path("/run/user/1000/podman/podman.sock")
|
||
|
|
|
||
|
|
|
||
|
|
def test_server_config_invalid_port_raises_validation_error():
|
||
|
|
"""Test that invalid port numbers raise ValidationError."""
|
||
|
|
from mcp_forge.config.schema import ServerConfig
|
||
|
|
|
||
|
|
# Port too high
|
||
|
|
with pytest.raises(ValidationError) as exc_info:
|
||
|
|
ServerConfig(
|
||
|
|
host="localhost",
|
||
|
|
port=70000,
|
||
|
|
podman_socket=Path("/run/podman.sock")
|
||
|
|
)
|
||
|
|
assert "port" in str(exc_info.value).lower()
|
||
|
|
|
||
|
|
# Port too low
|
||
|
|
with pytest.raises(ValidationError) as exc_info:
|
||
|
|
ServerConfig(
|
||
|
|
host="localhost",
|
||
|
|
port=0,
|
||
|
|
podman_socket=Path("/run/podman.sock")
|
||
|
|
)
|
||
|
|
assert "port" in str(exc_info.value).lower()
|
||
|
|
|
||
|
|
# Negative port
|
||
|
|
with pytest.raises(ValidationError) as exc_info:
|
||
|
|
ServerConfig(
|
||
|
|
host="localhost",
|
||
|
|
port=-1,
|
||
|
|
podman_socket=Path("/run/podman.sock")
|
||
|
|
)
|
||
|
|
assert "port" in str(exc_info.value).lower()
|
||
|
|
|
||
|
|
|
||
|
|
def test_execution_config_defaults_are_applied():
|
||
|
|
"""Test that ExecutionConfig has correct default values."""
|
||
|
|
from mcp_forge.config.schema import ExecutionConfig
|
||
|
|
|
||
|
|
config = ExecutionConfig()
|
||
|
|
|
||
|
|
assert config.default_backend == "simple"
|
||
|
|
assert config.default_timeout == 300
|
||
|
|
assert config.max_timeout == 1800
|
||
|
|
assert config.default_memory == "512m"
|
||
|
|
assert config.max_memory == "2g"
|
||
|
|
assert config.default_cpu_quota == 50000
|
||
|
|
assert config.max_cpu_quota == 100000
|
||
|
|
|
||
|
|
|
||
|
|
def test_execution_config_max_timeout_validation():
|
||
|
|
"""Test that max_timeout must be >= default_timeout."""
|
||
|
|
from mcp_forge.config.schema import ExecutionConfig
|
||
|
|
|
||
|
|
# Valid: max >= default
|
||
|
|
config = ExecutionConfig(default_timeout=300, max_timeout=600)
|
||
|
|
assert config.max_timeout == 600
|
||
|
|
|
||
|
|
# Invalid: max < default
|
||
|
|
with pytest.raises(ValidationError) as exc_info:
|
||
|
|
ExecutionConfig(default_timeout=600, max_timeout=300)
|
||
|
|
assert "max_timeout" in str(exc_info.value).lower()
|
||
|
|
|
||
|
|
|
||
|
|
def test_image_config_defaults():
|
||
|
|
"""Test ImageConfig default values."""
|
||
|
|
from mcp_forge.config.schema import ImageConfig
|
||
|
|
|
||
|
|
config = ImageConfig()
|
||
|
|
|
||
|
|
assert config.python_3_11 == "mcp-forge/python:3.11"
|
||
|
|
assert config.python_3_12 == "mcp-forge/python:3.12"
|
||
|
|
assert config.jupyter == "mcp-forge/jupyter:latest"
|
||
|
|
assert config.auto_pull is True
|
||
|
|
assert config.pull_interval == 86400
|
||
|
|
|
||
|
|
|
||
|
|
def test_session_config_defaults():
|
||
|
|
"""Test SessionConfig default values."""
|
||
|
|
from mcp_forge.config.schema import SessionConfig
|
||
|
|
|
||
|
|
config = SessionConfig()
|
||
|
|
|
||
|
|
assert config.idle_timeout == 3600
|
||
|
|
assert config.max_concurrent == 10
|
||
|
|
assert config.cleanup_interval == 300
|
||
|
|
|
||
|
|
|
||
|
|
def test_volume_config_with_base_path():
|
||
|
|
"""Test VolumeConfig with required base_path."""
|
||
|
|
from mcp_forge.config.schema import VolumeConfig
|
||
|
|
|
||
|
|
config = VolumeConfig(base_path=Path("/mcp-forge/volumes"))
|
||
|
|
|
||
|
|
assert config.base_path == Path("/mcp-forge/volumes")
|
||
|
|
assert config.session_quota == "1g"
|
||
|
|
assert config.max_session_quota == "10g"
|
||
|
|
|
||
|
|
|
||
|
|
def test_security_config_defaults():
|
||
|
|
"""Test SecurityConfig default values."""
|
||
|
|
from mcp_forge.config.schema import SecurityConfig
|
||
|
|
|
||
|
|
config = SecurityConfig(audit_log=Path("/var/log/mcp-forge/audit.log"))
|
||
|
|
|
||
|
|
assert config.audit_log == Path("/var/log/mcp-forge/audit.log")
|
||
|
|
assert config.enforce_resource_limits is True
|
||
|
|
assert config.allow_network is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_package_validation_config():
|
||
|
|
"""Test PackageValidationConfig structure."""
|
||
|
|
from mcp_forge.config.schema import PackageValidationConfig
|
||
|
|
|
||
|
|
config = PackageValidationConfig(
|
||
|
|
use_allowlist=True,
|
||
|
|
allowlist_path=Path("/etc/mcp-forge/allowlist.txt"),
|
||
|
|
blocklist_path=Path("/etc/mcp-forge/blocklist.txt"),
|
||
|
|
require_approval_patterns=["*crypto*", "*network*"]
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.use_allowlist is True
|
||
|
|
assert config.allowlist_path == Path("/etc/mcp-forge/allowlist.txt")
|
||
|
|
assert config.blocklist_path == Path("/etc/mcp-forge/blocklist.txt")
|
||
|
|
assert "*crypto*" in config.require_approval_patterns
|
||
|
|
|
||
|
|
|
||
|
|
def test_environment_builder_config():
|
||
|
|
"""Test EnvironmentBuilderConfig structure and defaults."""
|
||
|
|
from mcp_forge.config.schema import EnvironmentBuilderConfig, PackageValidationConfig
|
||
|
|
|
||
|
|
pkg_validation = PackageValidationConfig(
|
||
|
|
use_allowlist=True,
|
||
|
|
allowlist_path=Path("/etc/allowlist.txt"),
|
||
|
|
blocklist_path=Path("/etc/blocklist.txt"),
|
||
|
|
require_approval_patterns=[]
|
||
|
|
)
|
||
|
|
|
||
|
|
config = EnvironmentBuilderConfig(
|
||
|
|
enabled=True,
|
||
|
|
uv_cache_path=Path("/var/cache/mcp-forge/uv"),
|
||
|
|
package_validation=pkg_validation
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.enabled is True
|
||
|
|
assert config.uv_cache_path == Path("/var/cache/mcp-forge/uv")
|
||
|
|
assert config.max_packages_per_build == 50
|
||
|
|
assert config.max_build_time == 600
|
||
|
|
assert config.max_image_size == 2147483648
|
||
|
|
assert config.max_concurrent_builds == 3
|
||
|
|
|
||
|
|
|
||
|
|
def test_mcp_tool_config():
|
||
|
|
"""Test MCPToolConfig structure."""
|
||
|
|
from mcp_forge.config.schema import MCPToolConfig
|
||
|
|
|
||
|
|
config = MCPToolConfig(
|
||
|
|
command="uvx",
|
||
|
|
args=["mcp-server-git"],
|
||
|
|
env={"GIT_AUTHOR": "test"}
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.command == "uvx"
|
||
|
|
assert config.args == ["mcp-server-git"]
|
||
|
|
assert config.env == {"GIT_AUTHOR": "test"}
|
||
|
|
|
||
|
|
|
||
|
|
def test_mcp_tool_config_empty_env_defaults():
|
||
|
|
"""Test MCPToolConfig with empty env defaults to empty dict."""
|
||
|
|
from mcp_forge.config.schema import MCPToolConfig
|
||
|
|
|
||
|
|
config = MCPToolConfig(command="test", args=[])
|
||
|
|
|
||
|
|
assert config.env == {}
|
||
|
|
|
||
|
|
|
||
|
|
def test_forge_config_full_structure():
|
||
|
|
"""Test complete ForgeConfig with all nested structures."""
|
||
|
|
from mcp_forge.config.schema import (
|
||
|
|
ForgeConfig, ServerConfig, ExecutionConfig, ImageConfig,
|
||
|
|
SessionConfig, VolumeConfig, SecurityConfig,
|
||
|
|
EnvironmentBuilderConfig, PackageValidationConfig, MCPToolConfig
|
||
|
|
)
|
||
|
|
|
||
|
|
pkg_validation = PackageValidationConfig(
|
||
|
|
use_allowlist=True,
|
||
|
|
allowlist_path=Path("/etc/allowlist.txt"),
|
||
|
|
blocklist_path=Path("/etc/blocklist.txt"),
|
||
|
|
require_approval_patterns=[]
|
||
|
|
)
|
||
|
|
|
||
|
|
config = ForgeConfig(
|
||
|
|
server=ServerConfig(
|
||
|
|
host="localhost",
|
||
|
|
port=3000,
|
||
|
|
podman_socket=Path("/run/podman.sock")
|
||
|
|
),
|
||
|
|
execution=ExecutionConfig(),
|
||
|
|
images=ImageConfig(),
|
||
|
|
sessions=SessionConfig(),
|
||
|
|
volumes=VolumeConfig(base_path=Path("/mcp-forge/volumes")),
|
||
|
|
security=SecurityConfig(audit_log=Path("/var/log/audit.log")),
|
||
|
|
environment_builder=EnvironmentBuilderConfig(
|
||
|
|
enabled=True,
|
||
|
|
uv_cache_path=Path("/var/cache/uv"),
|
||
|
|
package_validation=pkg_validation
|
||
|
|
),
|
||
|
|
mcp_tools={
|
||
|
|
"git": MCPToolConfig(command="uvx", args=["mcp-server-git"])
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.server.host == "localhost"
|
||
|
|
assert config.execution.default_backend == "simple"
|
||
|
|
assert config.images.python_3_11 == "mcp-forge/python:3.11"
|
||
|
|
assert config.sessions.max_concurrent == 10
|
||
|
|
assert config.volumes.base_path == Path("/mcp-forge/volumes")
|
||
|
|
assert config.security.enforce_resource_limits is True
|
||
|
|
assert config.environment_builder.enabled is True
|
||
|
|
assert "git" in config.mcp_tools
|
||
|
|
|
||
|
|
|
||
|
|
def test_nested_configuration_validation_error_includes_field_path():
|
||
|
|
"""Test that ValidationError for nested config includes full field path."""
|
||
|
|
from mcp_forge.config.schema import ForgeConfig, ServerConfig
|
||
|
|
|
||
|
|
with pytest.raises(ValidationError) as exc_info:
|
||
|
|
ForgeConfig(
|
||
|
|
server=ServerConfig(
|
||
|
|
host="localhost",
|
||
|
|
port=99999, # Invalid port
|
||
|
|
podman_socket=Path("/run/podman.sock")
|
||
|
|
),
|
||
|
|
execution={},
|
||
|
|
images={},
|
||
|
|
sessions={},
|
||
|
|
volumes={"base_path": "/volumes"},
|
||
|
|
security={"audit_log": "/audit.log"},
|
||
|
|
environment_builder={
|
||
|
|
"uv_cache_path": "/cache",
|
||
|
|
"package_validation": {
|
||
|
|
"allowlist_path": "/allow.txt",
|
||
|
|
"blocklist_path": "/block.txt",
|
||
|
|
"require_approval_patterns": []
|
||
|
|
}
|
||
|
|
},
|
||
|
|
mcp_tools={}
|
||
|
|
)
|
||
|
|
|
||
|
|
error_str = str(exc_info.value)
|
||
|
|
# Should include nested path like "server.port"
|
||
|
|
assert "port" in error_str.lower()
|
||
|
|
|
||
|
|
|
||
|
|
def test_execution_config_backend_literal_validation():
|
||
|
|
"""Test that default_backend only accepts 'simple' or 'jupyter'."""
|
||
|
|
from mcp_forge.config.schema import ExecutionConfig
|
||
|
|
|
||
|
|
# Valid values
|
||
|
|
config1 = ExecutionConfig(default_backend="simple")
|
||
|
|
assert config1.default_backend == "simple"
|
||
|
|
|
||
|
|
config2 = ExecutionConfig(default_backend="jupyter")
|
||
|
|
assert config2.default_backend == "jupyter"
|
||
|
|
|
||
|
|
# Invalid value
|
||
|
|
with pytest.raises(ValidationError) as exc_info:
|
||
|
|
ExecutionConfig(default_backend="invalid")
|
||
|
|
assert "default_backend" in str(exc_info.value).lower()
|
||
|
|
|
||
|
|
|
||
|
|
def test_environment_builder_config_rate_limit_dict():
|
||
|
|
"""Test that EnvironmentBuilderConfig accepts rate_limit dict."""
|
||
|
|
from mcp_forge.config.schema import EnvironmentBuilderConfig, PackageValidationConfig
|
||
|
|
|
||
|
|
pkg_validation = PackageValidationConfig(
|
||
|
|
use_allowlist=True,
|
||
|
|
allowlist_path=Path("/allow.txt"),
|
||
|
|
blocklist_path=Path("/block.txt"),
|
||
|
|
require_approval_patterns=[]
|
||
|
|
)
|
||
|
|
|
||
|
|
config = EnvironmentBuilderConfig(
|
||
|
|
enabled=True,
|
||
|
|
uv_cache_path=Path("/cache"),
|
||
|
|
package_validation=pkg_validation,
|
||
|
|
build_rate_limit={"max_requests": 10, "period_seconds": 3600}
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.build_rate_limit == {"max_requests": 10, "period_seconds": 3600}
|
||
|
|
|
||
|
|
|
||
|
|
def test_environment_builder_config_auto_cleanup_dict():
|
||
|
|
"""Test that EnvironmentBuilderConfig accepts auto_cleanup dict."""
|
||
|
|
from mcp_forge.config.schema import EnvironmentBuilderConfig, PackageValidationConfig
|
||
|
|
|
||
|
|
pkg_validation = PackageValidationConfig(
|
||
|
|
use_allowlist=True,
|
||
|
|
allowlist_path=Path("/allow.txt"),
|
||
|
|
blocklist_path=Path("/block.txt"),
|
||
|
|
require_approval_patterns=[]
|
||
|
|
)
|
||
|
|
|
||
|
|
config = EnvironmentBuilderConfig(
|
||
|
|
enabled=True,
|
||
|
|
uv_cache_path=Path("/cache"),
|
||
|
|
package_validation=pkg_validation,
|
||
|
|
auto_cleanup={"enabled": True, "max_age_days": 30}
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.auto_cleanup == {"enabled": True, "max_age_days": 30}
|
||
|
|
|
||
|
|
|
||
|
|
def test_environment_builder_config_templates_dict():
|
||
|
|
"""Test that EnvironmentBuilderConfig accepts templates dict."""
|
||
|
|
from mcp_forge.config.schema import EnvironmentBuilderConfig, PackageValidationConfig
|
||
|
|
|
||
|
|
pkg_validation = PackageValidationConfig(
|
||
|
|
use_allowlist=True,
|
||
|
|
allowlist_path=Path("/allow.txt"),
|
||
|
|
blocklist_path=Path("/block.txt"),
|
||
|
|
require_approval_patterns=[]
|
||
|
|
)
|
||
|
|
|
||
|
|
config = EnvironmentBuilderConfig(
|
||
|
|
enabled=True,
|
||
|
|
uv_cache_path=Path("/cache"),
|
||
|
|
package_validation=pkg_validation,
|
||
|
|
templates={
|
||
|
|
"data-science": {"packages": ["numpy", "pandas", "matplotlib"]},
|
||
|
|
"web": {"packages": ["fastapi", "uvicorn"]}
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
assert "data-science" in config.templates
|
||
|
|
assert "web" in config.templates
|
||
|
|
assert config.templates["data-science"]["packages"] == ["numpy", "pandas", "matplotlib"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_config_constraint_validation_memory_strings():
|
||
|
|
"""Test that config validates memory strings are comparable."""
|
||
|
|
from mcp_forge.config.schema import ExecutionConfig
|
||
|
|
|
||
|
|
# This should succeed - just testing structure, not actual parsing yet
|
||
|
|
config = ExecutionConfig(
|
||
|
|
default_memory="512m",
|
||
|
|
max_memory="2g"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.default_memory == "512m"
|
||
|
|
assert config.max_memory == "2g"
|
||
|
|
|
||
|
|
|
||
|
|
def test_mcp_tool_config_list_of_args():
|
||
|
|
"""Test MCPToolConfig args is a list."""
|
||
|
|
from mcp_forge.config.schema import MCPToolConfig
|
||
|
|
|
||
|
|
config = MCPToolConfig(
|
||
|
|
command="python",
|
||
|
|
args=["-m", "mymodule", "--flag"]
|
||
|
|
)
|
||
|
|
|
||
|
|
assert isinstance(config.args, list)
|
||
|
|
assert config.args == ["-m", "mymodule", "--flag"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_package_validation_require_approval_patterns_list():
|
||
|
|
"""Test PackageValidationConfig require_approval_patterns is a list."""
|
||
|
|
from mcp_forge.config.schema import PackageValidationConfig
|
||
|
|
|
||
|
|
config = PackageValidationConfig(
|
||
|
|
use_allowlist=False,
|
||
|
|
allowlist_path=Path("/allow.txt"),
|
||
|
|
blocklist_path=Path("/block.txt"),
|
||
|
|
require_approval_patterns=["*crypto*", "*security*", "paramiko"]
|
||
|
|
)
|
||
|
|
|
||
|
|
assert isinstance(config.require_approval_patterns, list)
|
||
|
|
assert len(config.require_approval_patterns) == 3
|