Files
homelable/mcp/tests/test_tools.py
T
Pouzor 300567c88d feat(mcp): expose parent_id in update_node tool
- Add parent_id to NodeUpdate schema in backend so PATCH /nodes/{id}
  accepts it (was silently ignored before)
- Expose parent_id in update_node MCP tool schema so the MCP SDK
  forwards it to the backend instead of stripping it
- Add regression tests in both backend and MCP layers
2026-03-13 21:29:27 +01:00

123 lines
4.4 KiB
Python

import pytest
from unittest.mock import AsyncMock, patch
from app.tools import _dispatch
@pytest.fixture
def mock_backend():
with patch("app.tools.backend") as m:
m.post = AsyncMock(return_value={"id": "1"})
m.patch = AsyncMock(return_value={"id": "1"})
m.delete = AsyncMock(return_value={})
m.get = AsyncMock(return_value=[])
yield m
@pytest.mark.anyio
async def test_create_node(mock_backend):
result = await _dispatch("create_node", {"type": "server", "label": "Proxmox"})
mock_backend.post.assert_called_once_with("/api/v1/nodes", {"type": "server", "label": "Proxmox"})
assert result == {"id": "1"}
@pytest.mark.anyio
async def test_update_node(mock_backend):
await _dispatch("update_node", {"id": "42", "label": "New name"})
mock_backend.patch.assert_called_once_with("/api/v1/nodes/42", {"label": "New name"})
@pytest.mark.anyio
async def test_update_node_parent_id(mock_backend):
await _dispatch("update_node", {"id": "42", "parent_id": "proxmox-1"})
mock_backend.patch.assert_called_once_with("/api/v1/nodes/42", {"parent_id": "proxmox-1"})
@pytest.mark.anyio
async def test_delete_node(mock_backend):
await _dispatch("delete_node", {"id": "42"})
mock_backend.delete.assert_called_once_with("/api/v1/nodes/42")
@pytest.mark.anyio
async def test_create_edge(mock_backend):
await _dispatch("create_edge", {"source": "1", "target": "2", "type": "ethernet"})
mock_backend.post.assert_called_once_with("/api/v1/edges", {"source": "1", "target": "2", "type": "ethernet"})
@pytest.mark.anyio
async def test_delete_edge(mock_backend):
await _dispatch("delete_edge", {"id": "99"})
mock_backend.delete.assert_called_once_with("/api/v1/edges/99")
@pytest.mark.anyio
async def test_trigger_scan_no_ranges(mock_backend):
await _dispatch("trigger_scan", {})
mock_backend.post.assert_called_once_with("/api/v1/scan/trigger", {})
@pytest.mark.anyio
async def test_trigger_scan_with_ranges(mock_backend):
await _dispatch("trigger_scan", {"ranges": ["192.168.1.0/24"]})
mock_backend.post.assert_called_once_with("/api/v1/scan/trigger", {"ranges": ["192.168.1.0/24"]})
@pytest.mark.anyio
async def test_approve_device(mock_backend):
await _dispatch("approve_device", {"id": "5", "type": "server", "label": "MyServer"})
mock_backend.post.assert_called_once_with("/api/v1/scan/pending/5/approve", {"type": "server", "label": "MyServer"})
@pytest.mark.anyio
async def test_hide_device(mock_backend):
await _dispatch("hide_device", {"id": "5"})
mock_backend.post.assert_called_once_with("/api/v1/scan/pending/5/hide", {})
@pytest.mark.anyio
async def test_get_canvas(mock_backend):
mock_backend.get = AsyncMock(return_value={
"nodes": [
{
"id": "n1",
"type": "router",
"position": {"x": 100, "y": 200},
"width": 160,
"height": 80,
"data": {"label": "Freebox", "ip": "192.168.1.1", "status": "online"},
}
],
"edges": [
{"id": "e1", "source": "n1", "target": "n2", "type": "ethernet", "animated": True, "style": {"stroke": "#fff"}},
],
"viewport": {"x": 0, "y": 0, "zoom": 1},
})
result = await _dispatch("get_canvas", {})
mock_backend.get.assert_called_once_with("/api/v1/canvas")
# Layout/style fields stripped, only semantic data kept
assert result["nodes"] == [{"id": "n1", "node_type": "router", "label": "Freebox", "ip": "192.168.1.1", "status": "online"}]
assert result["edges"] == [{"id": "e1", "source": "n1", "target": "n2", "type": "ethernet"}]
assert "viewport" not in result
@pytest.mark.anyio
async def test_list_nodes(mock_backend):
mock_backend.get = AsyncMock(return_value=[{"id": "1", "label": "Freebox"}])
result = await _dispatch("list_nodes", {})
mock_backend.get.assert_called_once_with("/api/v1/nodes")
assert result == [{"id": "1", "label": "Freebox"}]
@pytest.mark.anyio
async def test_list_pending_devices(mock_backend):
mock_backend.get = AsyncMock(return_value=[{"id": "p1", "ip": "192.168.1.50"}])
result = await _dispatch("list_pending_devices", {})
mock_backend.get.assert_called_once_with("/api/v1/scan/pending")
assert result == [{"id": "p1", "ip": "192.168.1.50"}]
@pytest.mark.anyio
async def test_unknown_tool():
with pytest.raises(ValueError, match="Unknown tool"):
await _dispatch("nonexistent", {})