"""Tests for modules/system.py.""" 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_IMAGES = { "images": [ { "id": "sha256:aaaa", "repository": "nginx", "tags": ["1.24"], "size": 50 * 1024 * 1024, }, { "id": "sha256:bbbb", "repository": "postgres", "tags": ["15"], "size": 80 * 1024 * 1024, }, { "id": "sha256:cccc", "repository": "", "tags": [], "size": 10 * 1024 * 1024, }, ] } SAMPLE_CONTAINERS = { "containers": [ {"name": "web", "status": "running", "image_id": "sha256:aaaa"}, {"name": "db", "status": "running", "image_id": "sha256:bbbb"}, {"name": "old", "status": "stopped", "image_id": "sha256:aaaa"}, ] } # ────────────────────────────────────────────────────────────────────────────── # system_df # ────────────────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_system_df_shows_image_stats(): from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": return SAMPLE_IMAGES if api == "SYNO.Docker.Container": return SAMPLE_CONTAINERS return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_df"]() assert "Images" in result assert "3" in result # total images assert "Containers" in result @pytest.mark.asyncio async def test_system_df_reclaimable(): """Unused images (not referenced by any container) are counted as reclaimable.""" from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": return SAMPLE_IMAGES if api == "SYNO.Docker.Container": return SAMPLE_CONTAINERS return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_df"]() # sha256:cccc () is not referenced by any container → reclaimable assert "reclaimable" in result assert "unused" in result @pytest.mark.asyncio async def test_system_df_running_vs_stopped(): from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": return SAMPLE_IMAGES if api == "SYNO.Docker.Container": return SAMPLE_CONTAINERS return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_df"]() assert "running" in result assert "stopped" in result @pytest.mark.asyncio async def test_system_df_image_api_error_graceful(): """Container data is still shown even when image API fails.""" from mcp_synology_container.dsm_client import SynologyError from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": raise SynologyError("image API down", code=102) if api == "SYNO.Docker.Container": return SAMPLE_CONTAINERS return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_df"]() # Should still show container section and a warning assert "Containers" in result or "Warnings" in result @pytest.mark.asyncio async def test_system_df_no_images(): from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": return {"images": []} if api == "SYNO.Docker.Container": return {"containers": []} return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_df"]() assert "0" in result # ────────────────────────────────────────────────────────────────────────────── # system_prune # ────────────────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_system_prune_preview(): from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": return SAMPLE_IMAGES if api == "SYNO.Docker.Container": return SAMPLE_CONTAINERS return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_prune"]() assert "preview" in result.lower() assert "confirmed=True" in result # prune API must NOT be called calls = [c.args[:2] for c in client.request.call_args_list] assert ("SYNO.Docker.Utils", "prune") not in calls @pytest.mark.asyncio async def test_system_prune_preview_lists_dangling(): """Preview names dangling/unused images and stopped containers.""" from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": return SAMPLE_IMAGES if api == "SYNO.Docker.Container": return SAMPLE_CONTAINERS return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_prune"]() # : is dangling assert "" in result # "old" container is stopped assert "old" in result @pytest.mark.asyncio async def test_system_prune_confirmed(): from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": return SAMPLE_IMAGES if api == "SYNO.Docker.Container": return SAMPLE_CONTAINERS if api == "SYNO.Docker.Utils" and method == "prune": return {"SpaceReclaimed": 10 * 1024 * 1024} return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_prune"](confirmed=True) assert "completed" in result.lower() assert "MiB" in result or "reclaimed" in result.lower() @pytest.mark.asyncio async def test_system_prune_confirmed_no_space_reported(): """Prune works even when DSM doesn't report reclaimed bytes.""" from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": return SAMPLE_IMAGES if api == "SYNO.Docker.Container": return SAMPLE_CONTAINERS if api == "SYNO.Docker.Utils" and method == "prune": return {} return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_prune"](confirmed=True) assert "completed" in result.lower() assert "not reported" in result @pytest.mark.asyncio async def test_system_prune_api_error(): from mcp_synology_container.dsm_client import SynologyError from mcp_synology_container.modules.system import register_system client = AsyncMock() async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image": return SAMPLE_IMAGES if api == "SYNO.Docker.Container": return SAMPLE_CONTAINERS if api == "SYNO.Docker.Utils" and method == "prune": raise SynologyError("prune failed", code=100) return {} client.request.side_effect = mock_request mcp, tools = make_mock_mcp() register_system(mcp, make_config(), client) result = await tools["system_prune"](confirmed=True) assert "Error" in result