"""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() client.post_request = AsyncMock(return_value={}) 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() client.post_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.post_request = AsyncMock(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 params = client.post_request.call_args.kwargs.get("params", {}) assert json.loads(params["name"]) == "mynet" assert params["driver"] == "bridge" assert json.loads(params["subnet"]) == "192.168.100.0/24" assert json.loads(params["enable_ipv6"]) is False assert json.loads(params["disable_masquerade"]) is False @pytest.mark.asyncio async def test_create_network_with_ipv6(): from mcp_synology_container.modules.networks import register_networks client = AsyncMock() client.post_request = AsyncMock(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) params = client.post_request.call_args.kwargs.get("params", {}) 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.post_request = AsyncMock(return_value={}) mcp, tools = make_mock_mcp() register_networks(mcp, make_config(), client) await tools["create_network"](name="bare", confirmed=True) params = client.post_request.call_args.kwargs.get("params", {}) 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.post_request = AsyncMock(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 client.post_request = AsyncMock(return_value={}) 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 GET was called; post_request must not be called client.request.assert_called_once() assert client.request.call_args.args[1] == "list" client.post_request.assert_not_called() @pytest.mark.asyncio async def test_delete_network_confirmed(): from mcp_synology_container.modules.networks import register_networks client = AsyncMock() client.request.return_value = SAMPLE_NETWORKS client.post_request = AsyncMock(return_value={}) 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 # post_request must use method "remove" with networks JSON array post_call = client.post_request.call_args assert post_call.args[1] == "remove" params = post_call.kwargs.get("params", {}) networks_list = json.loads(params["networks"]) assert len(networks_list) == 1 obj = networks_list[0] assert obj["name"] == "my_bridge" assert obj["id"] == "aabbcc112233" assert obj["_key"] == "aabbcc112233" @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 client.post_request = AsyncMock(return_value={}) 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 client.post_request.assert_not_called() @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 client.post_request = AsyncMock(return_value={}) 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 client.post_request.assert_not_called() @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() client.request.return_value = SAMPLE_NETWORKS client.post_request = AsyncMock(side_effect=SynologyError("remove failed", code=100)) 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