feat: Add comprehensive tests for Docker integration, security, and volume persistence
- Introduced tests for Docker deployment scripts to ensure existence, permissions, and proper command usage. - Added tests for Docker integration with Claude Desktop, validating MCP configuration and command formats. - Implemented health check tests for Docker, ensuring script functionality and proper configuration in Docker setup. - Created tests for Docker MCP validation, focusing on command validation and security configurations. - Developed security tests for Docker configurations, checking for non-root user setups, privilege restrictions, and sensitive data handling. - Added volume persistence tests to ensure configuration and logs are correctly managed across container runs. - Updated .dockerignore to exclude sensitive files and added relevant tests for Docker secrets handling.
This commit is contained in:
310
tests/test_docker_claude_desktop_integration.py
Normal file
310
tests/test_docker_claude_desktop_integration.py
Normal file
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
Tests for Docker integration with Claude Desktop MCP
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestDockerClaudeDesktopIntegration:
|
||||
"""Test Docker integration with Claude Desktop"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
"""Setup for each test"""
|
||||
self.project_root = Path(__file__).parent.parent
|
||||
|
||||
def test_mcp_config_docker_run_format(self):
|
||||
"""Test MCP configuration for direct docker run"""
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"zen-mcp": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"--env-file",
|
||||
"/path/to/.env",
|
||||
"-v",
|
||||
"/path/to/logs:/app/logs",
|
||||
"zen-mcp-server:latest",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Validate configuration structure
|
||||
assert "mcpServers" in config
|
||||
assert "zen-mcp" in config["mcpServers"]
|
||||
assert config["mcpServers"]["zen-mcp"]["command"] == "docker"
|
||||
|
||||
args = config["mcpServers"]["zen-mcp"]["args"]
|
||||
assert "run" in args
|
||||
assert "--rm" in args
|
||||
assert "-i" in args
|
||||
assert "--env-file" in args
|
||||
|
||||
def test_mcp_config_docker_compose_format(self):
|
||||
"""Test MCP configuration for docker-compose run"""
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"zen-mcp": {
|
||||
"command": "docker-compose",
|
||||
"args": ["-f", "/path/to/docker-compose.yml", "run", "--rm", "zen-mcp"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Validate configuration structure
|
||||
assert config["mcpServers"]["zen-mcp"]["command"] == "docker-compose"
|
||||
|
||||
args = config["mcpServers"]["zen-mcp"]["args"]
|
||||
assert "-f" in args
|
||||
assert "run" in args
|
||||
assert "--rm" in args
|
||||
assert "zen-mcp" in args
|
||||
|
||||
def test_mcp_config_environment_variables(self):
|
||||
"""Test MCP configuration with inline environment variables"""
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"zen-mcp": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"-e",
|
||||
"GEMINI_API_KEY=test_key",
|
||||
"-e",
|
||||
"LOG_LEVEL=INFO",
|
||||
"zen-mcp-server:latest",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args = config["mcpServers"]["zen-mcp"]["args"]
|
||||
|
||||
# Check that environment variables are properly formatted
|
||||
env_args = [arg for arg in args if arg.startswith("-e")]
|
||||
assert len(env_args) > 0, "Environment variables should be present"
|
||||
|
||||
# Check for API key environment variable
|
||||
api_key_present = any("GEMINI_API_KEY=" in args[i + 1] for i, arg in enumerate(args[:-1]) if arg == "-e")
|
||||
assert api_key_present, "API key environment variable should be set"
|
||||
|
||||
def test_windows_path_format(self):
|
||||
"""Test Windows-specific path formatting"""
|
||||
windows_config = {
|
||||
"mcpServers": {
|
||||
"zen-mcp": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"--env-file",
|
||||
"C:/Users/User/zen-mcp-server/.env",
|
||||
"-v",
|
||||
"C:/Users/User/zen-mcp-server/logs:/app/logs",
|
||||
"zen-mcp-server:latest",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args = windows_config["mcpServers"]["zen-mcp"]["args"]
|
||||
|
||||
# Check Windows path format
|
||||
windows_paths = [arg for arg in args if arg.startswith("C:/")]
|
||||
assert len(windows_paths) > 0, "Windows paths should use forward slashes"
|
||||
|
||||
for path in windows_paths:
|
||||
assert "\\" not in path, "Windows paths should use forward slashes"
|
||||
|
||||
def test_mcp_config_validation(self):
|
||||
"""Test validation of MCP configuration"""
|
||||
# Valid configuration
|
||||
valid_config = {
|
||||
"mcpServers": {"zen-mcp": {"command": "docker", "args": ["run", "--rm", "-i", "zen-mcp-server:latest"]}}
|
||||
}
|
||||
|
||||
# Validate JSON serialization
|
||||
config_json = json.dumps(valid_config)
|
||||
loaded_config = json.loads(config_json)
|
||||
assert loaded_config == valid_config
|
||||
|
||||
def test_mcp_stdio_communication(self):
|
||||
"""Test that MCP configuration supports stdio communication"""
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"zen-mcp": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i", # Interactive mode for stdio
|
||||
"zen-mcp-server:latest",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args = config["mcpServers"]["zen-mcp"]["args"]
|
||||
|
||||
# Check for interactive mode
|
||||
assert "-i" in args, "Interactive mode required for stdio communication"
|
||||
|
||||
# Should not expose network ports for stdio communication
|
||||
port_args = [arg for arg in args if arg.startswith("-p")]
|
||||
assert len(port_args) == 0, "No ports should be exposed for stdio mode"
|
||||
|
||||
def test_docker_image_reference(self):
|
||||
"""Test that Docker image is properly referenced"""
|
||||
configs = [
|
||||
{"image": "zen-mcp-server:latest"},
|
||||
{"image": "zen-mcp-server:v1.0.0"},
|
||||
{"image": "registry/zen-mcp-server:latest"},
|
||||
]
|
||||
|
||||
for config in configs:
|
||||
image = config["image"]
|
||||
|
||||
# Basic image format validation
|
||||
assert ":" in image, "Image should have a tag"
|
||||
assert len(image.split(":")) == 2, "Image should have exactly one tag"
|
||||
|
||||
@pytest.fixture
|
||||
def temp_mcp_config(self):
|
||||
"""Create temporary MCP configuration file"""
|
||||
config = {
|
||||
"mcpServers": {
|
||||
"zen-mcp": {
|
||||
"command": "docker",
|
||||
"args": ["run", "--rm", "-i", "--env-file", "/tmp/.env", "zen-mcp-server:latest"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
temp_file_path = f.name
|
||||
|
||||
yield temp_file_path
|
||||
os.unlink(temp_file_path)
|
||||
|
||||
def test_mcp_config_file_parsing(self, temp_mcp_config):
|
||||
"""Test parsing of MCP configuration file"""
|
||||
# Read and parse the temporary config file
|
||||
with open(temp_mcp_config, encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
assert "mcpServers" in config
|
||||
assert "zen-mcp" in config["mcpServers"]
|
||||
|
||||
def test_environment_file_integration(self):
|
||||
"""Test integration with .env file"""
|
||||
# Test .env file format expected by Docker
|
||||
env_content = """GEMINI_API_KEY=test_key
|
||||
OPENAI_API_KEY=test_key_2
|
||||
LOG_LEVEL=INFO
|
||||
DEFAULT_MODEL=auto
|
||||
"""
|
||||
|
||||
# Parse environment content
|
||||
env_vars = {}
|
||||
for line in env_content.strip().split("\n"):
|
||||
if "=" in line and not line.startswith("#"):
|
||||
key, value = line.split("=", 1)
|
||||
env_vars[key] = value
|
||||
|
||||
# Validate required environment variables
|
||||
assert "GEMINI_API_KEY" in env_vars
|
||||
assert len(env_vars["GEMINI_API_KEY"]) > 0
|
||||
|
||||
def test_docker_volume_mount_paths(self):
|
||||
"""Test Docker volume mount path configurations"""
|
||||
mount_configs = [
|
||||
{"host": "./logs", "container": "/app/logs"},
|
||||
{"host": "/absolute/path/logs", "container": "/app/logs"},
|
||||
{"host": "C:/Windows/path/logs", "container": "/app/logs"},
|
||||
]
|
||||
|
||||
for config in mount_configs:
|
||||
mount_arg = f"{config['host']}:{config['container']}"
|
||||
|
||||
# Validate mount format
|
||||
assert ":" in mount_arg
|
||||
parts = mount_arg.split(":")
|
||||
assert len(parts) >= 2
|
||||
assert parts[-1].startswith("/"), "Container path should be absolute"
|
||||
|
||||
|
||||
class TestDockerMCPErrorHandling:
|
||||
"""Test error handling for Docker MCP integration"""
|
||||
|
||||
def test_missing_docker_image_handling(self):
|
||||
"""Test handling of missing Docker image"""
|
||||
# This would test what happens when the image doesn't exist
|
||||
# In practice, Claude Desktop would show an error
|
||||
nonexistent_config = {
|
||||
"mcpServers": {"zen-mcp": {"command": "docker", "args": ["run", "--rm", "-i", "nonexistent:latest"]}}
|
||||
}
|
||||
|
||||
# Configuration should be valid even if image doesn't exist
|
||||
assert "zen-mcp" in nonexistent_config["mcpServers"]
|
||||
|
||||
def test_invalid_env_file_path(self):
|
||||
"""Test handling of invalid .env file path"""
|
||||
config_with_invalid_env = {
|
||||
"mcpServers": {
|
||||
"zen-mcp": {
|
||||
"command": "docker",
|
||||
"args": ["run", "--rm", "-i", "--env-file", "/nonexistent/.env", "zen-mcp-server:latest"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Configuration structure should still be valid
|
||||
args = config_with_invalid_env["mcpServers"]["zen-mcp"]["args"]
|
||||
assert "--env-file" in args
|
||||
|
||||
def test_docker_permission_issues(self):
|
||||
"""Test configuration for potential Docker permission issues"""
|
||||
# On some systems, Docker requires specific permissions
|
||||
# The configuration should work with both cases
|
||||
|
||||
configs = [
|
||||
# Regular Docker command
|
||||
{"command": "docker"},
|
||||
# Sudo Docker command (if needed)
|
||||
{"command": "sudo", "extra_args": ["docker"]},
|
||||
]
|
||||
|
||||
for config in configs:
|
||||
assert len(config["command"]) > 0
|
||||
|
||||
def test_resource_limit_configurations(self):
|
||||
"""Test Docker resource limit configurations"""
|
||||
config_with_limits = {
|
||||
"mcpServers": {
|
||||
"zen-mcp": {
|
||||
"command": "docker",
|
||||
"args": ["run", "--rm", "-i", "--memory=512m", "--cpus=1.0", "zen-mcp-server:latest"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args = config_with_limits["mcpServers"]["zen-mcp"]["args"]
|
||||
|
||||
# Check for resource limits
|
||||
memory_limit = any("--memory" in arg for arg in args)
|
||||
cpu_limit = any("--cpus" in arg for arg in args)
|
||||
|
||||
assert memory_limit or cpu_limit, "Resource limits should be configurable"
|
||||
Reference in New Issue
Block a user