"""Tool-level tests — every MCP tool exercised through Client(mcp) in-memory transport.""" import json import pytest from fastmcp import Client from fastmcp.exceptions import ToolError # --------------------------------------------------------------------------- # Discovery tools # --------------------------------------------------------------------------- class TestListServices: async def test_returns_services(self, client: Client): result = await client.call_tool("list_services", {"bus": "session"}) text = result.content[0].text assert "org.freedesktop.DBus" in text assert "D-Bus session bus" in text async def test_include_unique(self, client: Client): result = await client.call_tool( "list_services", {"bus": "session", "include_unique": True} ) text = result.content[0].text assert ":1." in text class TestIntrospect: async def test_dbus_daemon(self, client: Client): result = await client.call_tool("introspect", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "/org/freedesktop/DBus", }) text = result.content[0].text assert "org.freedesktop.DBus" in text assert "ListNames" in text async def test_invalid_path(self, client: Client): with pytest.raises(ToolError, match="Invalid D-Bus object path"): await client.call_tool("introspect", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "not-a-path", }) class TestListObjects: async def test_dbus_tree(self, client: Client): result = await client.call_tool("list_objects", { "bus": "session", "service": "org.freedesktop.DBus", }) text = result.content[0].text assert "Object tree" in text assert "/org/freedesktop/DBus" in text # --------------------------------------------------------------------------- # Interaction tools # --------------------------------------------------------------------------- class TestCallMethod: async def test_ping_void(self, client: Client): result = await client.call_tool("call_method", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus.Peer", "method": "Ping", }) text = result.content[0].text assert "void" in text.lower() async def test_get_id_returns_hex(self, client: Client): result = await client.call_tool("call_method", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "method": "GetId", }) text = result.content[0].text # GetId returns a hex string, JSON-encoded with quotes bus_id = json.loads(text) assert isinstance(bus_id, str) assert len(bus_id) > 10 async def test_bad_method_raises_tool_error(self, client: Client): with pytest.raises(ToolError, match="D-Bus error"): await client.call_tool("call_method", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "method": "NoSuchMethod", }) class TestGetProperty: async def test_read_property(self, client: Client): # Read the Features property from the DBus daemon result = await client.call_tool("get_property", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "property_name": "Features", }) text = result.content[0].text # Features returns an array of strings features = json.loads(text) assert isinstance(features, list) class TestGetAllProperties: async def test_dbus_daemon(self, client: Client): result = await client.call_tool("get_all_properties", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", }) text = result.content[0].text assert "Properties of" in text assert "Features" in text # --------------------------------------------------------------------------- # Validation # --------------------------------------------------------------------------- class TestSignatureValidation: async def test_invalid_signature_rejected(self, client: Client): with pytest.raises(ToolError, match="Invalid D-Bus signature"): await client.call_tool("call_method", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "method": "GetId", "signature": "INVALID!@#", "args": '["x"]', }) async def test_signature_too_long(self, client: Client): with pytest.raises(ToolError, match="Signature too long"): await client.call_tool("call_method", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "method": "GetId", "signature": "s" * 256, "args": '["x"]', }) async def test_malformed_signature_rejected(self, client: Client): with pytest.raises(ToolError, match="Malformed D-Bus signature"): await client.call_tool("call_method", { "bus": "session", "service": "org.freedesktop.DBus", "object_path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "method": "GetId", "signature": "((", "args": '["x"]', }) # --------------------------------------------------------------------------- # Shortcut tools — existing # --------------------------------------------------------------------------- class TestSendNotification: async def test_sends(self, client: Client): result = await client.call_tool("send_notification", { "summary": "mcdbus test", "body": "automated test notification", "timeout": 1000, }) text = result.content[0].text assert "Notification sent" in text assert "id:" in text class TestListSystemdUnits: async def test_returns_table(self, client: Client): result = await client.call_tool("list_systemd_units", {}) text = result.content[0].text assert "Systemd units" in text assert "| Unit |" in text async def test_pattern_filter(self, client: Client): result = await client.call_tool( "list_systemd_units", {"pattern": "dbus*"} ) text = result.content[0].text assert "dbus" in text.lower() class TestMediaPlayerControl: async def test_no_players_graceful(self, client: Client): # With no explicit player, it auto-discovers; if none found, returns message result = await client.call_tool( "media_player_control", {"action": "play"} ) text = result.content[0].text # Either "No MPRIS media players" or a player response — both are valid assert "Player:" in text or "No MPRIS" in text async def test_bad_action(self, client: Client): result = await client.call_tool( "media_player_control", {"action": "invalid_action"} ) text = result.content[0].text # auto-discovers player first; if none found, "No MPRIS" message # if player found, "Unknown action" message assert "Unknown action" in text or "No MPRIS" in text # --------------------------------------------------------------------------- # Shortcut tools — new (service may or may not be available) # --------------------------------------------------------------------------- class TestNetworkStatus: async def test_returns_status(self, client: Client): try: result = await client.call_tool("network_status", {}) except ToolError: return # NetworkManager not available on this system text = result.content[0].text assert "Network Status" in text async def test_has_state(self, client: Client): try: result = await client.call_tool("network_status", {}) except ToolError: return # NetworkManager not available text = result.content[0].text assert "State:" in text class TestBatteryStatus: async def test_returns_result(self, client: Client): try: result = await client.call_tool("battery_status", {}) except ToolError: return # UPower not available on this system text = result.content[0].text assert ( "Battery Status" in text or "No batteries found" in text or "No power devices" in text ) class TestBluetoothDevices: async def test_returns_result(self, client: Client): try: result = await client.call_tool("bluetooth_devices", {}) except ToolError: return # bluez not available on this system text = result.content[0].text assert ( "Bluetooth Devices" in text or "No Bluetooth devices" in text or "No Bluetooth objects" in text ) class TestKwinWindows: async def test_returns_windows(self, client: Client): try: result = await client.call_tool("kwin_windows", {}) except ToolError: return # KWin not available on this system text = result.content[0].text assert ( "KWin Windows" in text or "No windows found" in text or "No open windows" in text ) async def test_has_table(self, client: Client): try: result = await client.call_tool("kwin_windows", {}) except ToolError: return # KWin not available text = result.content[0].text if "KWin Windows" in text: assert "| Window |" in text