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