diff --git a/tests/test_resources.py b/tests/test_resources.py new file mode 100644 index 0000000..d3dd74c --- /dev/null +++ b/tests/test_resources.py @@ -0,0 +1,97 @@ +"""Tests for MCP subscription resources.""" + +import json +from unittest.mock import AsyncMock, patch + +import pytest +from fastmcp import FastMCP + +from unraid_mcp.subscriptions.resources import register_subscription_resources + + +def _make_resources(): + """Register resources on a test FastMCP instance and return it.""" + test_mcp = FastMCP("test") + register_subscription_resources(test_mcp) + return test_mcp + + +@pytest.fixture +def _mock_subscribe_once(): + with patch( + "unraid_mcp.subscriptions.resources.subscribe_once", + new_callable=AsyncMock, + ) as mock: + yield mock + + +@pytest.fixture +def _mock_ensure_started(): + with patch( + "unraid_mcp.subscriptions.resources.ensure_subscriptions_started", + new_callable=AsyncMock, + ) as mock: + yield mock + + +class TestLiveResources: + @pytest.mark.parametrize( + "action", + [ + "cpu", + "memory", + "cpu_telemetry", + "array_state", + "parity_progress", + "ups_status", + "notifications_overview", + "owner", + "server_status", + ], + ) + async def test_resource_returns_json( + self, + action: str, + _mock_subscribe_once: AsyncMock, + _mock_ensure_started: AsyncMock, + ) -> None: + _mock_subscribe_once.return_value = {"data": "ok"} + mcp = _make_resources() + + local_provider = mcp.providers[0] + resource_key = f"resource:unraid://live/{action}@" + resource = local_provider._components[resource_key] + result = await resource.fn() + + parsed = json.loads(result) + assert parsed == {"data": "ok"} + + async def test_resource_returns_error_dict_on_failure( + self, + _mock_subscribe_once: AsyncMock, + _mock_ensure_started: AsyncMock, + ) -> None: + from fastmcp.exceptions import ToolError + + _mock_subscribe_once.side_effect = ToolError("WebSocket timeout") + mcp = _make_resources() + + local_provider = mcp.providers[0] + resource = local_provider._components["resource:unraid://live/cpu@"] + result = await resource.fn() + + parsed = json.loads(result) + assert "error" in parsed + assert "WebSocket timeout" in parsed["error"] + + +class TestLogsStreamResource: + async def test_logs_stream_no_data(self, _mock_ensure_started: AsyncMock) -> None: + with patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr: + mock_mgr.get_resource_data = AsyncMock(return_value=None) + mcp = _make_resources() + local_provider = mcp.providers[0] + resource = local_provider._components["resource:unraid://logs/stream@"] + result = await resource.fn() + parsed = json.loads(result) + assert "status" in parsed diff --git a/unraid_mcp/subscriptions/resources.py b/unraid_mcp/subscriptions/resources.py index 715cf78..5e19b90 100644 --- a/unraid_mcp/subscriptions/resources.py +++ b/unraid_mcp/subscriptions/resources.py @@ -14,6 +14,8 @@ from fastmcp import FastMCP from ..config.logging import logger from .manager import subscription_manager +from .queries import SNAPSHOT_ACTIONS +from .snapshot import subscribe_once # Global flag to track subscription startup @@ -102,4 +104,22 @@ def register_subscription_resources(mcp: FastMCP) -> None: } ) + def _make_resource_fn(action: str, query: str): + async def _live_resource() -> str: + await ensure_subscriptions_started() + try: + data = await subscribe_once(query) + return json.dumps(data, indent=2) + except Exception as exc: + return json.dumps({"error": str(exc), "action": action}) + + _live_resource.__name__ = f"{action}_resource" + _live_resource.__doc__ = ( + f"Real-time {action.replace('_', ' ')} data via WebSocket subscription." + ) + return _live_resource + + for _action, _query in SNAPSHOT_ACTIONS.items(): + mcp.resource(f"unraid://live/{_action}")(_make_resource_fn(_action, _query)) + logger.info("Subscription resources registered successfully")