22-tool MCP server for Node-RED flow management with Pydantic models, incremental flow patching, layout linting, and flexible auth (token, basic, OAuth2). 137 tests, full ruff compliance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
252 lines
7.0 KiB
Python
252 lines
7.0 KiB
Python
"""Shared test fixtures for nodered_mcp tests."""
|
|
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from nodered_mcp.client import NodeRedClient
|
|
|
|
|
|
# ── Sample API data fixtures ──────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_flows_data() -> list[dict[str, Any]]:
|
|
"""Raw Node-RED flows API response (heterogeneous array of tabs, nodes, subflows)."""
|
|
return [
|
|
# Two tabs
|
|
{
|
|
"id": "tab1",
|
|
"type": "tab",
|
|
"label": "Main Flow",
|
|
"disabled": False,
|
|
"info": "Primary workspace",
|
|
},
|
|
{
|
|
"id": "tab2",
|
|
"type": "tab",
|
|
"label": "Test Flow",
|
|
"disabled": True,
|
|
"info": "",
|
|
},
|
|
# Several nodes of different types
|
|
{
|
|
"id": "inject1",
|
|
"type": "inject",
|
|
"z": "tab1",
|
|
"name": "Test Inject",
|
|
"topic": "",
|
|
"payload": "test",
|
|
"payloadType": "str",
|
|
"repeat": "",
|
|
"crontab": "",
|
|
"once": False,
|
|
"onceDelay": 0.1,
|
|
"x": 100,
|
|
"y": 100,
|
|
"wires": [["function1"]],
|
|
},
|
|
{
|
|
"id": "function1",
|
|
"type": "function",
|
|
"z": "tab1",
|
|
"name": "Process Data",
|
|
"func": "return msg;",
|
|
"outputs": 1,
|
|
"noerr": 0,
|
|
"x": 300,
|
|
"y": 100,
|
|
"wires": [["debug1"]],
|
|
},
|
|
{
|
|
"id": "debug1",
|
|
"type": "debug",
|
|
"z": "tab1",
|
|
"name": "Output",
|
|
"active": True,
|
|
"tosidebar": True,
|
|
"console": False,
|
|
"tostatus": False,
|
|
"complete": "payload",
|
|
"x": 500,
|
|
"y": 100,
|
|
"wires": [],
|
|
},
|
|
{
|
|
"id": "inject2",
|
|
"type": "inject",
|
|
"z": "tab2",
|
|
"name": "Another Inject",
|
|
"payload": "",
|
|
"payloadType": "date",
|
|
"repeat": "",
|
|
"crontab": "",
|
|
"once": False,
|
|
"x": 100,
|
|
"y": 200,
|
|
"wires": [[]],
|
|
},
|
|
# One subflow
|
|
{
|
|
"id": "subflow1",
|
|
"type": "subflow",
|
|
"name": "Test Subflow",
|
|
"info": "",
|
|
"in": [{"x": 50, "y": 50, "wires": [{"id": "sf-node1"}]}],
|
|
"out": [{"x": 250, "y": 50, "wires": [{"id": "sf-node1", "port": 0}]}],
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_flow_data() -> dict[str, Any]:
|
|
"""Single flow API response (GET /flow/:id)."""
|
|
return {
|
|
"id": "tab1",
|
|
"label": "Main Flow",
|
|
"nodes": [
|
|
{
|
|
"id": "inject1",
|
|
"type": "inject",
|
|
"name": "Test",
|
|
"x": 100,
|
|
"y": 100,
|
|
"wires": [["debug1"]],
|
|
},
|
|
{
|
|
"id": "debug1",
|
|
"type": "debug",
|
|
"name": "Output",
|
|
"x": 300,
|
|
"y": 100,
|
|
"wires": [],
|
|
},
|
|
],
|
|
"configs": [],
|
|
"subflows": [],
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_node_module_data() -> dict[str, Any]:
|
|
"""NodeModule API response (GET /nodes/:module)."""
|
|
return {
|
|
"name": "node-red-contrib-test",
|
|
"version": "1.0.0",
|
|
"nodes": [
|
|
{
|
|
"id": "node-red-contrib-test/test-node",
|
|
"name": "test-node",
|
|
"types": ["test-input", "test-output"],
|
|
"enabled": True,
|
|
"module": "node-red-contrib-test",
|
|
"version": "1.0.0",
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_node_set_data() -> dict[str, Any]:
|
|
"""NodeSet API response."""
|
|
return {
|
|
"id": "node-red/inject",
|
|
"name": "inject",
|
|
"types": ["inject"],
|
|
"enabled": True,
|
|
"module": "node-red",
|
|
"version": "3.1.0",
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_settings_data() -> dict[str, Any]:
|
|
"""Settings API response (GET /settings)."""
|
|
return {
|
|
"httpNodeRoot": "/api",
|
|
"version": "3.1.0",
|
|
"user": {
|
|
"username": "admin",
|
|
"permissions": "*",
|
|
},
|
|
"flowFilePretty": True,
|
|
"editorTheme": {
|
|
"page": {"title": "Node-RED"},
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_diagnostics_data() -> dict[str, Any]:
|
|
"""Diagnostics API response (GET /diagnostics)."""
|
|
return {
|
|
"report": {
|
|
"versions": {
|
|
"node-red": "3.1.0",
|
|
"nodejs": "18.17.0",
|
|
"npm": "9.6.7",
|
|
},
|
|
"platform": {
|
|
"os": "darwin",
|
|
"arch": "arm64",
|
|
"cpus": 8,
|
|
"mem": 17179869184,
|
|
},
|
|
"modules": {
|
|
"node-red-contrib-test": "1.0.0",
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_flows_state_data() -> dict[str, Any]:
|
|
"""Flow state API response (GET /flows/state)."""
|
|
return {"state": "start"}
|
|
|
|
|
|
# ── Mock client fixture ───────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_client(
|
|
sample_flows_data: list[dict[str, Any]],
|
|
sample_flow_data: dict[str, Any],
|
|
sample_node_module_data: dict[str, Any],
|
|
sample_node_set_data: dict[str, Any],
|
|
sample_settings_data: dict[str, Any],
|
|
sample_diagnostics_data: dict[str, Any],
|
|
sample_flows_state_data: dict[str, Any],
|
|
) -> NodeRedClient:
|
|
"""Mock NodeRedClient with all methods stubbed."""
|
|
client = AsyncMock(spec=NodeRedClient)
|
|
|
|
# Flow endpoints
|
|
client.get_flows.return_value = sample_flows_data
|
|
client.get_flow.return_value = sample_flow_data
|
|
client.update_flows.return_value = None
|
|
client.update_flow.return_value = None
|
|
client.create_flow.return_value = {"id": "new-flow-id"}
|
|
client.delete_flow.return_value = None
|
|
client.get_flows_state.return_value = sample_flows_state_data
|
|
client.set_flows_state.return_value = sample_flows_state_data
|
|
|
|
# Node endpoints
|
|
client.get_nodes.return_value = [sample_node_set_data]
|
|
client.get_node_info.return_value = sample_node_module_data
|
|
client.toggle_node_module.return_value = sample_node_module_data
|
|
client.inject.return_value = None
|
|
|
|
# Settings endpoints
|
|
client.get_settings.return_value = sample_settings_data
|
|
client.get_diagnostics.return_value = sample_diagnostics_data
|
|
|
|
with (
|
|
patch("nodered_mcp.tools.flows.get_client", return_value=client),
|
|
patch("nodered_mcp.tools.nodes.get_client", return_value=client),
|
|
patch("nodered_mcp.tools.settings.get_client", return_value=client),
|
|
patch("nodered_mcp.tools.layout.get_client", return_value=client),
|
|
):
|
|
yield client
|