""" 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