237 lines
7.0 KiB
Python
237 lines
7.0 KiB
Python
"""Tests for modules/compose.py."""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import yaml
|
|
|
|
|
|
def make_mock_mcp():
|
|
tools: dict = {}
|
|
|
|
class MockMCP:
|
|
def tool(self):
|
|
def decorator(fn):
|
|
tools[fn.__name__] = fn
|
|
return fn
|
|
return decorator
|
|
|
|
return MockMCP(), tools
|
|
|
|
|
|
def make_config():
|
|
from mcp_synology_container.config import AppConfig, ConnectionConfig
|
|
return AppConfig(
|
|
schema_version=1,
|
|
connection=ConnectionConfig(host="nas.local", port=443, https=True, verify_ssl=True),
|
|
compose_base_path="/volume1/docker",
|
|
)
|
|
|
|
|
|
SAMPLE_COMPOSE = """
|
|
services:
|
|
web:
|
|
image: nginx:1.24
|
|
ports:
|
|
- "80:80"
|
|
environment:
|
|
- APP_ENV=production
|
|
- LOG_LEVEL=info
|
|
db:
|
|
image: postgres:15
|
|
environment:
|
|
POSTGRES_DB: mydb
|
|
POSTGRES_USER: admin
|
|
"""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_compose():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
# Simulate FileStation.Info success for the first filename
|
|
client.request.return_value = {}
|
|
client.download_text.return_value = SAMPLE_COMPOSE
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["read_compose"]("myapp")
|
|
assert "nginx:1.24" in result
|
|
assert "postgres:15" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_compose_not_found():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
# Simulate all FileStation.Info calls failing
|
|
from mcp_synology_container.dsm_client import SynologyError
|
|
client.request.side_effect = SynologyError("not found", code=408)
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["read_compose"]("nonexistent")
|
|
assert "No compose file found" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_image_tag_requires_confirmation():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
client.request.return_value = {}
|
|
client.download_text.return_value = SAMPLE_COMPOSE
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["update_image_tag"]("myapp", "web", "1.25", confirmed=False)
|
|
assert "confirmed=True" in result
|
|
assert "nginx:1.24" in result
|
|
assert "nginx:1.25" in result
|
|
client.upload_text.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_image_tag_confirmed():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
client.request.return_value = {}
|
|
client.download_text.return_value = SAMPLE_COMPOSE
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["update_image_tag"]("myapp", "web", "1.25", confirmed=True)
|
|
assert "nginx:1.24 → nginx:1.25" in result
|
|
assert "redeploy_project" in result
|
|
client.upload_text.assert_called_once()
|
|
|
|
# Verify the uploaded content has the new tag
|
|
uploaded_content = client.upload_text.call_args[0][2]
|
|
parsed = yaml.safe_load(uploaded_content)
|
|
assert parsed["services"]["web"]["image"] == "nginx:1.25"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_image_tag_service_not_found():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
client.request.return_value = {}
|
|
client.download_text.return_value = SAMPLE_COMPOSE
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["update_image_tag"]("myapp", "nonexistent", "1.25", confirmed=True)
|
|
assert "not found" in result
|
|
assert "web" in result # should list available services
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_env_var_new_var_list_format():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
client.request.return_value = {}
|
|
client.download_text.return_value = SAMPLE_COMPOSE
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["update_env_var"]("myapp", "web", "NEW_VAR", "value123", confirmed=True)
|
|
assert "NEW_VAR=value123" in result
|
|
|
|
uploaded_content = client.upload_text.call_args[0][2]
|
|
parsed = yaml.safe_load(uploaded_content)
|
|
env = parsed["services"]["web"]["environment"]
|
|
assert any("NEW_VAR=value123" in str(e) for e in env)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_env_var_update_existing_list():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
client.request.return_value = {}
|
|
client.download_text.return_value = SAMPLE_COMPOSE
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["update_env_var"]("myapp", "web", "LOG_LEVEL", "debug", confirmed=True)
|
|
assert "LOG_LEVEL=debug" in result
|
|
|
|
uploaded_content = client.upload_text.call_args[0][2]
|
|
parsed = yaml.safe_load(uploaded_content)
|
|
env = parsed["services"]["web"]["environment"]
|
|
assert "LOG_LEVEL=debug" in env
|
|
assert "LOG_LEVEL=info" not in env
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_env_var_dict_format():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
client.request.return_value = {}
|
|
client.download_text.return_value = SAMPLE_COMPOSE
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
# db service has dict-format environment
|
|
result = await tools["update_env_var"]("myapp", "db", "POSTGRES_DB", "newdb", confirmed=True)
|
|
assert "POSTGRES_DB=newdb" in result
|
|
|
|
uploaded_content = client.upload_text.call_args[0][2]
|
|
parsed = yaml.safe_load(uploaded_content)
|
|
assert parsed["services"]["db"]["environment"]["POSTGRES_DB"] == "newdb"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_compose_invalid_yaml():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["update_compose"]("myapp", "not: valid: yaml: {{{{", confirmed=True)
|
|
assert "Invalid YAML" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_compose_missing_services_key():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["update_compose"]("myapp", "version: '3'\n", confirmed=True)
|
|
assert "services" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_compose_requires_confirmation():
|
|
from mcp_synology_container.modules.compose import register_compose
|
|
|
|
client = AsyncMock()
|
|
client.request.return_value = {}
|
|
|
|
mcp, tools = make_mock_mcp()
|
|
register_compose(mcp, make_config(), client)
|
|
|
|
result = await tools["update_compose"]("myapp", SAMPLE_COMPOSE, confirmed=False)
|
|
assert "confirmed=True" in result
|
|
client.upload_text.assert_not_called()
|