Files
mcp-synology-container/tests/test_modules/test_networks.py
T
marcus 4cee16922f Add list_networks, create_network, delete_network tools (Gruppe 4)
list_networks:
  SYNO.Docker.Network/list → shows name, driver, subnet, gateway,
  attached containers. Response key is "network" (not "networks").

create_network(name, driver, subnet, gateway, ip_range, enable_ipv6, confirmed):
  Dry-run preview without confirmed=True. Passes enable_ipv6 as
  json.dumps(bool) per N4S4 reference. Optional params (subnet,
  gateway, iprange) omitted from request when not provided.

delete_network(name, confirmed):
  Validates network exists and has no attached containers before
  deleting. Clear error listing attached container names if blocked.

15 unit tests covering all paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:42:10 +02:00

345 lines
11 KiB
Python

"""Tests for modules/networks.py."""
import json
from unittest.mock import AsyncMock
import pytest
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),
)
SAMPLE_NETWORKS = {
"network": [
{
"id": "b741915823aa",
"name": "vault_default",
"driver": "bridge",
"subnet": "172.22.0.0/16",
"gateway": "172.22.0.1",
"iprange": "",
"enable_ipv6": False,
"containers": ["vault"],
},
{
"id": "2fc7ebae3901",
"name": "host",
"driver": "host",
"subnet": "",
"gateway": "",
"iprange": "",
"enable_ipv6": False,
"containers": [],
},
{
"id": "aabbcc112233",
"name": "my_bridge",
"driver": "bridge",
"subnet": "10.10.0.0/24",
"gateway": "10.10.0.1",
"iprange": "",
"enable_ipv6": False,
"containers": [],
},
]
}
# ──────────────────────────────────────────────────────────────────────────────
# list_networks
# ──────────────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_list_networks_shows_all():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = SAMPLE_NETWORKS
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["list_networks"]()
assert "vault_default" in result
assert "host" in result
assert "my_bridge" in result
assert "3 total" in result
@pytest.mark.asyncio
async def test_list_networks_shows_subnet_and_gateway():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = SAMPLE_NETWORKS
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["list_networks"]()
assert "172.22.0.0/16" in result
assert "172.22.0.1" in result
@pytest.mark.asyncio
async def test_list_networks_shows_attached_containers():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = SAMPLE_NETWORKS
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["list_networks"]()
assert "vault" in result
@pytest.mark.asyncio
async def test_list_networks_empty():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = {"network": []}
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["list_networks"]()
assert "No networks found" in result
@pytest.mark.asyncio
async def test_list_networks_api_error():
from mcp_synology_container.dsm_client import SynologyError
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.side_effect = SynologyError("API error", code=102)
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["list_networks"]()
assert "Error" in result
# ──────────────────────────────────────────────────────────────────────────────
# create_network
# ──────────────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_create_network_preview():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["create_network"](name="mynet", driver="bridge")
assert "Preview" in result
assert "mynet" in result
assert "confirmed=True" in result
client.request.assert_not_called()
@pytest.mark.asyncio
async def test_create_network_confirmed():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = {"id": "deadbeef1234567890"}
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["create_network"](
name="mynet", driver="bridge", subnet="192.168.100.0/24", confirmed=True
)
assert "created" in result
assert "mynet" in result
call = client.request.call_args
params = call.kwargs.get("params") or {}
assert params["name"] == "mynet"
assert params["driver"] == "bridge"
assert params["subnet"] == "192.168.100.0/24"
assert json.loads(params["enable_ipv6"]) is False
@pytest.mark.asyncio
async def test_create_network_with_ipv6():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = {"id": "abc123"}
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
await tools["create_network"](name="ipv6net", enable_ipv6=True, confirmed=True)
call = client.request.call_args
params = call.kwargs.get("params") or {}
assert json.loads(params["enable_ipv6"]) is True
@pytest.mark.asyncio
async def test_create_network_optional_params_not_sent():
"""subnet/gateway/ip_range must not appear in params when not provided."""
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = {}
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
await tools["create_network"](name="bare", confirmed=True)
call = client.request.call_args
params = call.kwargs.get("params") or {}
assert "subnet" not in params
assert "gateway" not in params
assert "iprange" not in params
@pytest.mark.asyncio
async def test_create_network_api_error():
from mcp_synology_container.dsm_client import SynologyError
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.side_effect = SynologyError("create failed", code=100)
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["create_network"](name="mynet", confirmed=True)
assert "Error" in result
# ──────────────────────────────────────────────────────────────────────────────
# delete_network
# ──────────────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_delete_network_preview():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = SAMPLE_NETWORKS
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["delete_network"](name="my_bridge")
assert "Preview" in result
assert "my_bridge" in result
assert "confirmed=True" in result
# Only the list call should have been made, not delete
assert client.request.call_count == 1
assert client.request.call_args.args[1] == "list"
@pytest.mark.asyncio
async def test_delete_network_confirmed():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
async def mock_request(api, method, **kwargs):
if method == "list":
return SAMPLE_NETWORKS
if method == "delete":
return {}
return {}
client.request.side_effect = mock_request
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["delete_network"](name="my_bridge", confirmed=True)
assert "deleted" in result
assert "my_bridge" in result
@pytest.mark.asyncio
async def test_delete_network_not_found():
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = SAMPLE_NETWORKS
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["delete_network"](name="nonexistent", confirmed=True)
assert "not found" in result
@pytest.mark.asyncio
async def test_delete_network_blocked_by_containers():
"""Deletion must be refused when containers are attached."""
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
client.request.return_value = SAMPLE_NETWORKS
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
# vault_default has ["vault"] attached
result = await tools["delete_network"](name="vault_default", confirmed=True)
assert "Cannot delete" in result
assert "vault" in result
# delete API must not be called
assert client.request.call_count == 1
assert client.request.call_args.args[1] == "list"
@pytest.mark.asyncio
async def test_delete_network_api_error():
from mcp_synology_container.dsm_client import SynologyError
from mcp_synology_container.modules.networks import register_networks
client = AsyncMock()
async def mock_request(api, method, **kwargs):
if method == "list":
return SAMPLE_NETWORKS
raise SynologyError("delete failed", code=100)
client.request.side_effect = mock_request
mcp, tools = make_mock_mcp()
register_networks(mcp, make_config(), client)
result = await tools["delete_network"](name="my_bridge", confirmed=True)
assert "Error" in result