mcp-forge/tests/podman/test_podman_client.py

325 lines
12 KiB
Python
Raw Normal View History

"""
Tests for Podman client wrapper.
Following TDD approach - these tests are written before implementation.
Tests cover all requirements from todo.md section 1.3.1.
All tests use mocked Podman client (no actual Podman needed).
"""
import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
def test_connection_to_podman_socket_succeeds(tmp_path):
"""Test that connection to Podman socket succeeds."""
from pod_executor.containers.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch() # Create fake socket file
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client_instance = MagicMock()
mock_client_instance.ping.return_value = "OK"
mock_podman.return_value = mock_client_instance
client.connect()
assert client._client is not None
mock_podman.assert_called_once_with(base_url=f"unix://{socket_path}")
def test_connection_failure_raises_clear_error(tmp_path):
"""Test that connection failure raises clear error."""
from pod_executor.containers.client import PodmanClient, PodmanConnectionError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "nonexistent.sock"
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient', side_effect=Exception("Connection failed")):
with pytest.raises(PodmanConnectionError) as exc_info:
client.connect()
assert "connection" in str(exc_info.value).lower() or "failed" in str(exc_info.value).lower()
def test_socket_path_validation(tmp_path):
"""Test that socket path is validated before connecting."""
from pod_executor.containers.client import PodmanClient, PodmanConnectionError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
nonexistent_socket = tmp_path / "nonexistent.sock"
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=nonexistent_socket,
validator=validator,
audit_logger=audit_logger
)
with pytest.raises(PodmanConnectionError) as exc_info:
client.verify_socket_access()
assert "not found" in str(exc_info.value).lower() or "does not exist" in str(exc_info.value).lower()
def test_socket_permissions_check(tmp_path):
"""Test that socket permissions are checked."""
from pod_executor.containers.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
socket_path.chmod(0o000) # Remove all permissions
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
# verify_socket_access should check readability
# Depending on implementation, might raise error or just warn
try:
client.verify_socket_access()
except Exception as e:
# Should mention permissions or access
assert "permission" in str(e).lower() or "access" in str(e).lower() or "readable" in str(e).lower()
finally:
socket_path.chmod(0o644) # Restore for cleanup
def test_api_version_compatibility_check(tmp_path):
"""Test that API version is checked."""
from pod_executor.containers.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client = MagicMock()
mock_client.version.return_value = {
"Version": "4.5.0",
"ApiVersion": "4.5.0"
}
mock_client.ping.return_value = "OK"
mock_podman.return_value = mock_client
client.connect()
version_info = client.check_api_version()
assert "Version" in version_info or "ApiVersion" in version_info
def test_ping_health_check(tmp_path):
"""Test that ping/health check works."""
from pod_executor.containers.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client = MagicMock()
mock_client.ping.return_value = "OK"
mock_podman.return_value = mock_client
client.connect()
result = client.ping()
assert result is True or result == "OK"
def test_lazy_connection(tmp_path):
"""Test that connection is lazy (only connects when needed)."""
from pod_executor.containers.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
# Creating client should not connect
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
assert client._client is None # Not connected yet
# Accessing client property should trigger connection
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client_instance = MagicMock()
mock_client_instance.ping.return_value = "OK"
mock_podman.return_value = mock_client_instance
_ = client.client
assert client._client is not None
def test_disconnect_cleanup(tmp_path):
"""Test that disconnect cleans up properly."""
from pod_executor.containers.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client = MagicMock()
mock_client.ping.return_value = "OK"
mock_podman.return_value = mock_client
client.connect()
assert client._client is not None
client.disconnect()
assert client._client is None
def test_connection_error_includes_socket_path(tmp_path):
"""Test that connection errors include the socket path for debugging."""
from pod_executor.containers.client import PodmanClient, PodmanConnectionError
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "test.sock"
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with pytest.raises(PodmanConnectionError) as exc_info:
client.verify_socket_access()
assert str(socket_path) in str(exc_info.value) or socket_path.name in str(exc_info.value)
def test_client_property_auto_connects(tmp_path):
"""Test that accessing client property auto-connects if not connected."""
from pod_executor.containers.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
socket_path.touch()
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
with patch('mcp_forge.podman.client.BasePodmanClient') as mock_podman:
mock_client = MagicMock()
mock_client.ping.return_value = "OK"
mock_podman.return_value = mock_client
# First access should trigger connect
_ = client.client
assert mock_podman.called
# Second access should reuse connection
mock_podman.reset_mock()
_ = client.client
assert not mock_podman.called # Should not connect again
def test_validator_and_audit_logger_stored(tmp_path):
"""Test that validator and audit logger are stored for later use."""
from pod_executor.containers.client import PodmanClient
from mcp_forge.security.audit import AuditLogger
from mcp_forge.security.allowlist import OperationValidator
from mcp_forge.config.schema import SecurityConfig
socket_path = tmp_path / "podman.sock"
audit_logger = AuditLogger(tmp_path / "audit.log")
validator = OperationValidator(SecurityConfig(audit_log=tmp_path / "audit.log"))
client = PodmanClient(
socket_path=socket_path,
validator=validator,
audit_logger=audit_logger
)
assert client.validator is validator
assert client.audit_logger is audit_logger