Initial commit: Node-RED MCP server (Python/FastMCP)
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>
This commit is contained in:
251
tests/conftest.py
Normal file
251
tests/conftest.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user