mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
refactor: ruff autofixes
This commit is contained in:
455 files changed
+12322
-14762
No files matched your search
@@ -1,4 +1,5 @@
|
||||
"""MCP test fixtures."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ Device config tool tests with mocked topology and Nornir layers.
|
||||
Covers the VPCS node-type guard and the error contract shared by
|
||||
device_config_send / device_show_run / vpcs_config_set.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
@@ -18,16 +19,18 @@ def _topology_ports(node_type):
|
||||
|
||||
|
||||
class TestVPCSNodeTypeGuard:
|
||||
|
||||
def test_non_vpcs_node_is_rejected(self):
|
||||
from gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko import VPCSCommands
|
||||
|
||||
with patch(f"{VPCS_MOD}.get_device_ports_from_topology",
|
||||
return_value=_topology_ports("iou")) as topo:
|
||||
result = VPCSCommands()._run(json.dumps({
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}))
|
||||
with patch(f"{VPCS_MOD}.get_device_ports_from_topology", return_value=_topology_ports("iou")) as topo:
|
||||
result = VPCSCommands()._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert topo.called
|
||||
assert len(result) == 1
|
||||
assert result[0]["device_name"] == "PC1"
|
||||
@@ -37,12 +40,15 @@ class TestVPCSNodeTypeGuard:
|
||||
def test_missing_node_type_is_rejected(self):
|
||||
from gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko import VPCSCommands
|
||||
|
||||
with patch(f"{VPCS_MOD}.get_device_ports_from_topology",
|
||||
return_value={"PC1": {"port": 5000}}):
|
||||
result = VPCSCommands()._run(json.dumps({
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}))
|
||||
with patch(f"{VPCS_MOD}.get_device_ports_from_topology", return_value={"PC1": {"port": 5000}}):
|
||||
result = VPCSCommands()._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert result[0]["status"] == "failed"
|
||||
assert "unknown-type" in result[0]["error"]
|
||||
|
||||
@@ -54,13 +60,18 @@ class TestVPCSNodeTypeGuard:
|
||||
host_result = MagicMock(failed=False)
|
||||
host_result.result = "OK"
|
||||
nornir.run.return_value = {"PC1": host_result}
|
||||
with patch(f"{VPCS_MOD}.get_device_ports_from_topology",
|
||||
return_value=_topology_ports("vpcs")), \
|
||||
patch.object(VPCSCommands, "_initialize_nornir", return_value=nornir):
|
||||
result = tool._run(json.dumps({
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}))
|
||||
with (
|
||||
patch(f"{VPCS_MOD}.get_device_ports_from_topology", return_value=_topology_ports("vpcs")),
|
||||
patch.object(VPCSCommands, "_initialize_nornir", return_value=nornir),
|
||||
):
|
||||
result = tool._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert result[0]["status"] == "success"
|
||||
assert result[0]["output"] == "OK"
|
||||
|
||||
@@ -72,13 +83,18 @@ class TestVPCSNodeTypeGuard:
|
||||
host_result = MagicMock(failed=True)
|
||||
host_result.result = "Command failed (ReadTimeout)"
|
||||
nornir.run.return_value = {"PC1": host_result}
|
||||
with patch(f"{VPCS_MOD}.get_device_ports_from_topology",
|
||||
return_value=_topology_ports("vpcs")), \
|
||||
patch.object(VPCSCommands, "_initialize_nornir", return_value=nornir):
|
||||
result = tool._run(json.dumps({
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}))
|
||||
with (
|
||||
patch(f"{VPCS_MOD}.get_device_ports_from_topology", return_value=_topology_ports("vpcs")),
|
||||
patch.object(VPCSCommands, "_initialize_nornir", return_value=nornir),
|
||||
):
|
||||
result = tool._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert result[0]["status"] == "failed"
|
||||
assert result[0]["error"] == "Command failed (ReadTimeout)"
|
||||
assert "output" not in result[0]
|
||||
@@ -93,12 +109,15 @@ class TestDeviceToolErrorContract:
|
||||
def test_topology_level_error_has_status(self):
|
||||
from gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko import VPCSCommands
|
||||
|
||||
with patch(f"{VPCS_MOD}.get_device_ports_from_topology",
|
||||
side_effect=ValueError("topology unreachable")):
|
||||
result = VPCSCommands()._run(json.dumps({
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}))
|
||||
with patch(f"{VPCS_MOD}.get_device_ports_from_topology", side_effect=ValueError("topology unreachable")):
|
||||
result = VPCSCommands()._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert result == [{"status": "failed", "error": "topology unreachable"}]
|
||||
|
||||
def test_config_tool_topology_level_error_has_status(self):
|
||||
@@ -106,13 +125,18 @@ class TestDeviceToolErrorContract:
|
||||
ExecuteMultipleDeviceConfigCommands,
|
||||
)
|
||||
|
||||
with patch("gns3server.agent.gns3_copilot.tools_v2.config_tools_nornir"
|
||||
".get_device_ports_from_topology",
|
||||
side_effect=ValueError("no valid devices")):
|
||||
result = ExecuteMultipleDeviceConfigCommands()._run(json.dumps({
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "R1", "config_commands": ["int lo0"]}],
|
||||
}))
|
||||
with patch(
|
||||
"gns3server.agent.gns3_copilot.tools_v2.config_tools_nornir.get_device_ports_from_topology",
|
||||
side_effect=ValueError("no valid devices"),
|
||||
):
|
||||
result = ExecuteMultipleDeviceConfigCommands()._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291",
|
||||
"device_configs": [{"device_name": "R1", "config_commands": ["int lo0"]}],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert result == [{"status": "failed", "error": "no valid devices"}]
|
||||
|
||||
def test_mcp_handler_param_error_has_status(self, ctx=None):
|
||||
@@ -124,10 +148,12 @@ class TestDeviceToolErrorContract:
|
||||
|
||||
for handler in (device_config_send_handler, device_show_run_handler, vpcs_config_set_handler):
|
||||
result = handler({}, {"server_url": "http://x", "jwt_token": "t"})
|
||||
assert result == [{
|
||||
"status": "failed",
|
||||
"error": result[0]["error"], # message text may differ per handler
|
||||
}]
|
||||
assert result == [
|
||||
{
|
||||
"status": "failed",
|
||||
"error": result[0]["error"], # message text may differ per handler
|
||||
}
|
||||
]
|
||||
assert "required" in result[0]["error"]
|
||||
|
||||
def test_template_render_error_has_status(self):
|
||||
|
||||
+257
-116
@@ -3,6 +3,7 @@ MCP handler unit tests with mocked Gns3Connector.
|
||||
|
||||
Tests that handlers correctly transform tool parameters into HTTP calls.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
@@ -31,11 +32,11 @@ def ctx():
|
||||
|
||||
|
||||
class TestProject:
|
||||
|
||||
mod = "projects"
|
||||
|
||||
def test_list(self, ctx):
|
||||
from gns3server.agent.mcp.projects import list_projects_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn([{"project_id": "p1", "name": "Test", "status": "opened"}])
|
||||
result = list_projects_handler({}, ctx)
|
||||
@@ -43,6 +44,7 @@ class TestProject:
|
||||
|
||||
def test_get(self, ctx):
|
||||
from gns3server.agent.mcp.projects import get_project_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"project_id": "p1"})
|
||||
result = get_project_handler({"project_id": "p1"}, ctx)
|
||||
@@ -50,10 +52,12 @@ class TestProject:
|
||||
|
||||
def test_get_missing_id(self, ctx):
|
||||
from gns3server.agent.mcp.projects import get_project_handler
|
||||
|
||||
assert "error" in get_project_handler({}, ctx)
|
||||
|
||||
def test_create(self, ctx):
|
||||
from gns3server.agent.mcp.projects import create_project_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
conn = _mock_conn({"project_id": "p1"})
|
||||
m.return_value = conn
|
||||
@@ -65,16 +69,16 @@ class TestProject:
|
||||
|
||||
def test_create_without_auto_close(self, ctx):
|
||||
from gns3server.agent.mcp.projects import create_project_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
conn = _mock_conn({"project_id": "p2"})
|
||||
m.return_value = conn
|
||||
create_project_handler({"name": "New"}, ctx)
|
||||
conn.http_call.assert_called_once_with(
|
||||
"post", f"{conn.base_url}/projects", json_data={"name": "New"}
|
||||
)
|
||||
conn.http_call.assert_called_once_with("post", f"{conn.base_url}/projects", json_data={"name": "New"})
|
||||
|
||||
def test_delete(self, ctx):
|
||||
from gns3server.agent.mcp.projects import delete_project_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn({})
|
||||
result = delete_project_handler({"project_id": "p1"}, ctx)
|
||||
@@ -82,6 +86,7 @@ class TestProject:
|
||||
|
||||
def test_open(self, ctx):
|
||||
from gns3server.agent.mcp.projects import open_project_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"status": "opened"})
|
||||
result = open_project_handler({"project_id": "p1"}, ctx)
|
||||
@@ -89,6 +94,7 @@ class TestProject:
|
||||
|
||||
def test_close(self, ctx):
|
||||
from gns3server.agent.mcp.projects import close_project_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"status": "closed"})
|
||||
result = close_project_handler({"project_id": "p1"}, ctx)
|
||||
@@ -96,6 +102,7 @@ class TestProject:
|
||||
|
||||
def test_update(self, ctx):
|
||||
from gns3server.agent.mcp.projects import update_project_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"name": "Updated"})
|
||||
result = update_project_handler({"project_id": "p1", "name": "Updated"}, ctx)
|
||||
@@ -103,6 +110,7 @@ class TestProject:
|
||||
|
||||
def test_stats(self, ctx):
|
||||
from gns3server.agent.mcp.projects import get_project_stats_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"nodes": 5, "links": 3})
|
||||
result = get_project_stats_handler({"project_id": "p1"}, ctx)
|
||||
@@ -113,19 +121,21 @@ class TestProject:
|
||||
|
||||
|
||||
class TestNode:
|
||||
|
||||
|
||||
def test_list_fields(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_nodes_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn([
|
||||
{"node_id": "n1", "name": "R1", "status": "started", "node_type": "qemu", "console": 5000},
|
||||
])
|
||||
m.return_value = _mock_conn(
|
||||
[
|
||||
{"node_id": "n1", "name": "R1", "status": "started", "node_type": "qemu", "console": 5000},
|
||||
]
|
||||
)
|
||||
result = get_nodes_handler({"project_id": "p1", "fields": ["name", "status"]}, ctx)
|
||||
assert result == {"nodes": [{"name": "R1", "status": "started"}], "count": 1}
|
||||
|
||||
def test_list_invalid_fields(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_nodes_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn([])
|
||||
result = get_nodes_handler({"project_id": "p1", "fields": "not-a-list"}, ctx)
|
||||
@@ -133,6 +143,7 @@ class TestNode:
|
||||
|
||||
def test_get(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"node_id": "n1", "name": "R1"})
|
||||
result = get_node_handler({"project_id": "p1", "node_id": "n1"}, ctx)
|
||||
@@ -140,68 +151,99 @@ class TestNode:
|
||||
|
||||
def test_create_single_passes_name(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"node_id": "n1", "name": "MyRouter"})
|
||||
m.return_value = conn
|
||||
result = create_node_handler({
|
||||
"project_id": "p1", "template_id": "t1",
|
||||
"name": "MyRouter", "x": 100, "y": 200,
|
||||
}, ctx)
|
||||
result = create_node_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"template_id": "t1",
|
||||
"name": "MyRouter",
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"post", "http://192.168.1.3:3080/v3/projects/p1/templates/t1",
|
||||
"post",
|
||||
"http://192.168.1.3:3080/v3/projects/p1/templates/t1",
|
||||
json_data={"x": 100, "y": 200, "compute_id": "local", "name": "MyRouter"},
|
||||
)
|
||||
assert result == {"node_id": "n1", "name": "MyRouter"}
|
||||
|
||||
def test_create_fields_filter(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"node_id": "n1", "name": "R1", "status": "started"})
|
||||
result = create_node_handler({
|
||||
"project_id": "p1", "template_id": "t1",
|
||||
"fields": ["node_id", "name"],
|
||||
}, ctx)
|
||||
result = create_node_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"template_id": "t1",
|
||||
"fields": ["node_id", "name"],
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert result == {"node_id": "n1", "name": "R1"}
|
||||
|
||||
def test_create_fields_validation(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn()
|
||||
m.return_value = conn
|
||||
result = create_node_handler({
|
||||
"project_id": "p1", "template_id": "t1", "fields": "not-a-list",
|
||||
}, ctx)
|
||||
result = create_node_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"template_id": "t1",
|
||||
"fields": "not-a-list",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert "error" in result
|
||||
assert "fields must be a list" in result["error"]
|
||||
conn.http_call.assert_not_called()
|
||||
|
||||
def test_create_batch_inherits_template_id(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"node_id": "n1", "name": "R1"})
|
||||
result = create_node_handler({
|
||||
"project_id": "p1", "template_id": "default-tpl",
|
||||
"nodes": [{"name": "R1", "x": 0, "y": 0}],
|
||||
}, ctx)
|
||||
result = create_node_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"template_id": "default-tpl",
|
||||
"nodes": [{"name": "R1", "x": 0, "y": 0}],
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert result[0]["status"] == "success"
|
||||
|
||||
def test_create_batch_preserves_submission_order(self, ctx):
|
||||
import time
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn()
|
||||
|
||||
def _http_call(method, url, json_data=None, **kwargs):
|
||||
# first submissions sleep longest so completion order is reversed
|
||||
time.sleep({"slow": 0.25, "mid": 0.1}.get(json_data.get("name"), 0.0))
|
||||
resp = MagicMock()
|
||||
resp.json.return_value = {"node_id": "n1", "name": json_data["name"]}
|
||||
return resp
|
||||
|
||||
conn.http_call.side_effect = _http_call
|
||||
m.return_value = conn
|
||||
result = create_node_handler({
|
||||
"project_id": "p1", "template_id": "t1",
|
||||
"nodes": [{"name": "slow"}, {"name": "mid"}, {"name": "fast"}],
|
||||
}, ctx)
|
||||
result = create_node_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"template_id": "t1",
|
||||
"nodes": [{"name": "slow"}, {"name": "mid"}, {"name": "fast"}],
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert [r["node"]["name"] for r in result] == ["slow", "mid", "fast"]
|
||||
|
||||
def test_create_batch_default_names_created_sequentially(self, ctx):
|
||||
@@ -246,10 +288,12 @@ class TestNode:
|
||||
|
||||
def test_create_missing_project_id(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler
|
||||
|
||||
assert create_node_handler({}, ctx) == {"error": "project_id is required"}
|
||||
|
||||
def test_delete_batch(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import delete_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({})
|
||||
result = delete_node_handler({"project_id": "p1", "node_ids": ["n1", "n2"]}, ctx)
|
||||
@@ -259,6 +303,7 @@ class TestNode:
|
||||
|
||||
def test_start_batch(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import start_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"status": "started"})
|
||||
result = start_node_handler({"project_id": "p1", "node_ids": ["n1"]}, ctx)
|
||||
@@ -266,6 +311,7 @@ class TestNode:
|
||||
|
||||
def test_stop_batch(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import stop_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"status": "stopped"})
|
||||
result = stop_node_handler({"project_id": "p1", "node_ids": ["n1"]}, ctx)
|
||||
@@ -273,6 +319,7 @@ class TestNode:
|
||||
|
||||
def test_suspend_batch(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import suspend_node_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"status": "suspended"})
|
||||
result = suspend_node_handler({"project_id": "p1", "node_ids": ["n1"]}, ctx)
|
||||
@@ -281,6 +328,7 @@ class TestNode:
|
||||
def test_console(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_console_info_handler
|
||||
from gns3server.services import access_ticket_service
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"console_url": "ws://host/console"})
|
||||
result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, ctx)
|
||||
@@ -297,6 +345,7 @@ class TestNode:
|
||||
|
||||
def test_console_vnc_uses_same_ticket(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_console_info_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"console_type": "vnc"})
|
||||
result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, ctx)
|
||||
@@ -306,6 +355,7 @@ class TestNode:
|
||||
|
||||
def test_console_without_username_omits_token(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_console_info_handler
|
||||
|
||||
unauthenticated_ctx = {k: v for k, v in ctx.items() if k != "jwt_username"}
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"console_url": "ws://host/console"})
|
||||
@@ -321,6 +371,7 @@ class TestNode:
|
||||
|
||||
def test_file_get_keeps_trailing_newline(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = self._file_conn("line1\nline2\n")
|
||||
result = get_node_file_handler({"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg"}, ctx)
|
||||
@@ -331,6 +382,7 @@ class TestNode:
|
||||
|
||||
def test_file_get_keeps_crlf_endings(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = self._file_conn("line1\r\nline2\r\n")
|
||||
result = get_node_file_handler({"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg"}, ctx)
|
||||
@@ -339,6 +391,7 @@ class TestNode:
|
||||
|
||||
def test_file_get_without_trailing_newline(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = self._file_conn("line1\nline2")
|
||||
result = get_node_file_handler({"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg"}, ctx)
|
||||
@@ -346,6 +399,7 @@ class TestNode:
|
||||
|
||||
def test_file_get_pagination(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = self._file_conn("line1\nline2\nline3\n")
|
||||
result = get_node_file_handler(
|
||||
@@ -361,10 +415,9 @@ class TestNode:
|
||||
|
||||
|
||||
class TestLink:
|
||||
|
||||
|
||||
def test_list(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_links_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn([{"link_id": "l1", "link_type": "ethernet"}])
|
||||
result = get_links_handler({"project_id": "p1", "fields": ["link_id"]}, ctx)
|
||||
@@ -372,6 +425,7 @@ class TestLink:
|
||||
|
||||
def test_get(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_link_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"link_id": "l1", "link_type": "ethernet"})
|
||||
result = get_link_handler({"project_id": "p1", "link_id": "l1"}, ctx)
|
||||
@@ -379,49 +433,66 @@ class TestLink:
|
||||
|
||||
def test_create_compact_format(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_link_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"link_id": "l1", "link_type": "ethernet", "nodes": []})
|
||||
m.return_value = conn
|
||||
result = create_link_handler({
|
||||
"project_id": "p1",
|
||||
"nodes": ["n1", 0, 0, "n2", 0, 0],
|
||||
}, ctx)
|
||||
result = create_link_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"nodes": ["n1", 0, 0, "n2", 0, 0],
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"post", "http://192.168.1.3:3080/v3/projects/p1/links",
|
||||
json_data={"nodes": [
|
||||
{"node_id": "n1", "adapter_number": 0, "port_number": 0},
|
||||
{"node_id": "n2", "adapter_number": 0, "port_number": 0},
|
||||
]},
|
||||
"post",
|
||||
"http://192.168.1.3:3080/v3/projects/p1/links",
|
||||
json_data={
|
||||
"nodes": [
|
||||
{"node_id": "n1", "adapter_number": 0, "port_number": 0},
|
||||
{"node_id": "n2", "adapter_number": 0, "port_number": 0},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
def test_create_standard_format(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_link_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"link_id": "l1"})
|
||||
result = create_link_handler({
|
||||
"project_id": "p1",
|
||||
"nodes": [
|
||||
{"node_id": "n1", "adapter_number": 0, "port_number": 0},
|
||||
{"node_id": "n2", "adapter_number": 0, "port_number": 0},
|
||||
],
|
||||
}, ctx)
|
||||
result = create_link_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"nodes": [
|
||||
{"node_id": "n1", "adapter_number": 0, "port_number": 0},
|
||||
{"node_id": "n2", "adapter_number": 0, "port_number": 0},
|
||||
],
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert result["link_id"] == "l1"
|
||||
|
||||
def test_create_fields_validation(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_link_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn()
|
||||
m.return_value = conn
|
||||
result = create_link_handler({
|
||||
"project_id": "p1", "fields": "bad",
|
||||
"nodes": ["n1", 0, 0, "n2", 0, 0],
|
||||
}, ctx)
|
||||
result = create_link_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"fields": "bad",
|
||||
"nodes": ["n1", 0, 0, "n2", 0, 0],
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert "error" in result
|
||||
assert "fields must be a list" in result["error"]
|
||||
conn.http_call.assert_not_called()
|
||||
|
||||
def test_delete_batch(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import delete_link_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({})
|
||||
result = delete_link_handler({"project_id": "p1", "link_ids": ["l1", "l2"]}, ctx)
|
||||
@@ -432,8 +503,10 @@ class TestLink:
|
||||
def test_create_batch_preserves_submission_order(self, ctx):
|
||||
import time
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_link_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn()
|
||||
|
||||
def _http_call(method, url, json_data=None, **kwargs):
|
||||
# first submission sleeps longest so completion order is reversed
|
||||
first_node = json_data["nodes"][0]["node_id"]
|
||||
@@ -441,30 +514,41 @@ class TestLink:
|
||||
resp = MagicMock()
|
||||
resp.json.return_value = {"link_id": f"link-{first_node}"}
|
||||
return resp
|
||||
|
||||
conn.http_call.side_effect = _http_call
|
||||
m.return_value = conn
|
||||
result = create_link_handler({
|
||||
"project_id": "p1",
|
||||
"links": [
|
||||
{"nodes": ["n1", 0, 0, "n2", 0, 0]},
|
||||
{"nodes": ["n3", 0, 0, "n4", 0, 0]},
|
||||
],
|
||||
"fields": ["link_id"],
|
||||
}, ctx)
|
||||
result = create_link_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"links": [
|
||||
{"nodes": ["n1", 0, 0, "n2", 0, 0]},
|
||||
{"nodes": ["n3", 0, 0, "n4", 0, 0]},
|
||||
],
|
||||
"fields": ["link_id"],
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert [r["link"]["link_id"] for r in result] == ["link-n1", "link-n3"]
|
||||
|
||||
def test_update(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import update_link_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"link_id": "l1", "suspend": True})
|
||||
result = update_link_handler({
|
||||
"project_id": "p1", "link_id": "l1", "suspend": True,
|
||||
}, ctx)
|
||||
result = update_link_handler(
|
||||
{
|
||||
"project_id": "p1",
|
||||
"link_id": "l1",
|
||||
"suspend": True,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert result["suspend"] is True
|
||||
|
||||
def test_download_capture_file(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import download_capture_file_handler
|
||||
from gns3server.services import access_ticket_service
|
||||
|
||||
result = download_capture_file_handler({"project_id": "p1", "link_id": "l1"}, ctx)
|
||||
# a short path-bound ticket replaces the long Bearer JWT in the URL
|
||||
assert "?token=gns3t_" in result["download_url"]
|
||||
@@ -478,6 +562,7 @@ class TestLink:
|
||||
def test_download_capture_file_batch(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import download_capture_file_handler
|
||||
from gns3server.services import access_ticket_service
|
||||
|
||||
result = download_capture_file_handler({"project_id": "p1", "link_ids": ["l1", "l2"]}, ctx)
|
||||
assert result["count"] == 2
|
||||
# each link gets its own ticket bound to its own download path
|
||||
@@ -492,12 +577,12 @@ class TestLink:
|
||||
|
||||
|
||||
class TestSymbol:
|
||||
|
||||
mod = "symbols"
|
||||
|
||||
def test_get_symbol_download(self, ctx):
|
||||
from gns3server.agent.mcp.symbols import get_symbol_handler
|
||||
from gns3server.services import access_ticket_service
|
||||
|
||||
result = get_symbol_handler({"symbol_id": "router.svg"}, ctx)
|
||||
assert "?token=gns3t_" in result["download_url"]
|
||||
assert "Bearer" not in result["curl_command"]
|
||||
@@ -507,6 +592,7 @@ class TestSymbol:
|
||||
|
||||
def test_get_symbol_without_username_omits_token(self, ctx):
|
||||
from gns3server.agent.mcp.symbols import get_symbol_handler
|
||||
|
||||
unauthenticated_ctx = {k: v for k, v in ctx.items() if k != "jwt_username"}
|
||||
result = get_symbol_handler({"symbol_id": "router.svg"}, unauthenticated_ctx)
|
||||
assert "token=" not in result["download_url"]
|
||||
@@ -517,11 +603,11 @@ class TestSymbol:
|
||||
|
||||
|
||||
class TestAppliance:
|
||||
|
||||
mod = "appliances"
|
||||
|
||||
def test_get(self, ctx):
|
||||
from gns3server.agent.mcp.appliances import get_appliance_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"appliance_id": "a1", "name": "Cisco ISE"})
|
||||
result = get_appliance_handler({"appliance_id": "a1"}, ctx)
|
||||
@@ -529,24 +615,34 @@ class TestAppliance:
|
||||
|
||||
def test_install_with_version(self, ctx):
|
||||
from gns3server.agent.mcp.appliances import install_appliance_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
conn = _mock_conn({"template_id": "t1", "name": "FRR", "version": "8.2.2", "template_type": "docker"})
|
||||
m.return_value = conn
|
||||
result = install_appliance_handler({
|
||||
"appliance_id": "a1", "version": "2.7.0.356",
|
||||
}, ctx)
|
||||
result = install_appliance_handler(
|
||||
{
|
||||
"appliance_id": "a1",
|
||||
"version": "2.7.0.356",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"post", "http://192.168.1.3:3080/v3/appliances/a1/install",
|
||||
"post",
|
||||
"http://192.168.1.3:3080/v3/appliances/a1/install",
|
||||
params={"version": "2.7.0.356"},
|
||||
)
|
||||
assert result["template"] == {
|
||||
"template_id": "t1", "name": "FRR", "version": "8.2.2", "template_type": "docker",
|
||||
"template_id": "t1",
|
||||
"name": "FRR",
|
||||
"version": "8.2.2",
|
||||
"template_type": "docker",
|
||||
}
|
||||
|
||||
def test_install_empty_body(self, ctx):
|
||||
# a 204-style empty response must not blow up with a JSON decode error
|
||||
# (the template is still created server-side)
|
||||
from gns3server.agent.mcp.appliances import install_appliance_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
conn = _mock_conn()
|
||||
conn.http_call.return_value.content = b""
|
||||
@@ -558,6 +654,7 @@ class TestAppliance:
|
||||
|
||||
def test_install_missing_id(self, ctx):
|
||||
from gns3server.agent.mcp.appliances import install_appliance_handler
|
||||
|
||||
result = install_appliance_handler({}, ctx)
|
||||
assert "error" in result
|
||||
|
||||
@@ -566,21 +663,29 @@ class TestAppliance:
|
||||
|
||||
|
||||
class TestTemplate:
|
||||
|
||||
mod = "templates"
|
||||
|
||||
def test_list_fields(self, ctx):
|
||||
from gns3server.agent.mcp.templates import list_templates_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn([
|
||||
{"template_id": "t1", "name": "Cisco 7200", "template_type": "dynamips",
|
||||
"category": "router", "default_name_format": "{name}-{0}"},
|
||||
])
|
||||
m.return_value = _mock_conn(
|
||||
[
|
||||
{
|
||||
"template_id": "t1",
|
||||
"name": "Cisco 7200",
|
||||
"template_type": "dynamips",
|
||||
"category": "router",
|
||||
"default_name_format": "{name}-{0}",
|
||||
},
|
||||
]
|
||||
)
|
||||
result = list_templates_handler({"fields": ["template_id", "name"]}, ctx)
|
||||
assert result["templates"] == [{"template_id": "t1", "name": "Cisco 7200"}]
|
||||
|
||||
def test_list_invalid_field(self, ctx):
|
||||
from gns3server.agent.mcp.templates import list_templates_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn()
|
||||
result = list_templates_handler({"fields": ["does_not_exist"]}, ctx)
|
||||
@@ -588,6 +693,7 @@ class TestTemplate:
|
||||
|
||||
def test_get(self, ctx):
|
||||
from gns3server.agent.mcp.templates import get_template_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn({"template_id": "t1", "name": "Test"})
|
||||
result = get_template_handler({"template_id": "t1"}, ctx)
|
||||
@@ -595,6 +701,7 @@ class TestTemplate:
|
||||
|
||||
def test_delete(self, ctx):
|
||||
from gns3server.agent.mcp.templates import delete_template_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
m.return_value = _mock_conn({})
|
||||
result = delete_template_handler({"template_id": "t1"}, ctx)
|
||||
@@ -605,16 +712,18 @@ class TestTemplate:
|
||||
|
||||
|
||||
class TestImage:
|
||||
|
||||
mod = "images"
|
||||
|
||||
def test_install_manifest(self, ctx):
|
||||
from gns3server.agent.mcp.images import install_images_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
conn = _mock_conn({
|
||||
"created": [{"template_id": "t1", "name": "Empty VM", "version": "100G", "template_type": "qemu"}],
|
||||
"skipped": [{"name": "csr1000v.qcow2", "reason": "image is already used by one or more templates"}],
|
||||
})
|
||||
conn = _mock_conn(
|
||||
{
|
||||
"created": [{"template_id": "t1", "name": "Empty VM", "version": "100G", "template_type": "qemu"}],
|
||||
"skipped": [{"name": "csr1000v.qcow2", "reason": "image is already used by one or more templates"}],
|
||||
}
|
||||
)
|
||||
m.return_value = conn
|
||||
result = install_images_handler({}, ctx)
|
||||
assert result["created"][0]["name"] == "Empty VM"
|
||||
@@ -622,6 +731,7 @@ class TestImage:
|
||||
|
||||
def test_install_empty_body(self, ctx):
|
||||
from gns3server.agent.mcp.images import install_images_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
conn = _mock_conn()
|
||||
conn.http_call.return_value.content = b""
|
||||
@@ -632,11 +742,11 @@ class TestImage:
|
||||
|
||||
|
||||
class TestCompute:
|
||||
|
||||
mod = "computes"
|
||||
|
||||
def test_get_local_by_default(self, ctx):
|
||||
from gns3server.agent.mcp.computes import get_compute_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
conn = _mock_conn({"compute_id": "local", "name": "local"})
|
||||
m.return_value = conn
|
||||
@@ -647,6 +757,7 @@ class TestCompute:
|
||||
|
||||
def test_get_explicit_compute_id(self, ctx):
|
||||
from gns3server.agent.mcp.computes import get_compute_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
conn = _mock_conn({"compute_id": "4fcfb6b5-5b0b-4f43-bd5e-e8ae2a69c8e6"})
|
||||
m.return_value = conn
|
||||
@@ -656,6 +767,7 @@ class TestCompute:
|
||||
|
||||
def test_images_local_by_default(self, ctx):
|
||||
from gns3server.agent.mcp.computes import get_compute_images_handler
|
||||
|
||||
with patch(f"{BASE}.{self.mod}._get_connector") as m:
|
||||
conn = _mock_conn(["img1.qcow2"])
|
||||
m.return_value = conn
|
||||
@@ -666,6 +778,7 @@ class TestCompute:
|
||||
|
||||
def test_images_requires_emulator(self, ctx):
|
||||
from gns3server.agent.mcp.computes import get_compute_images_handler
|
||||
|
||||
assert "error" in get_compute_images_handler({}, ctx)
|
||||
|
||||
|
||||
@@ -675,101 +788,120 @@ class TestCompute:
|
||||
class TestLinkMarker:
|
||||
"""link_marker_handler direction tri-state: omit=preserve, tx/rx=set, both=clear (→ null)."""
|
||||
|
||||
|
||||
def test_update_direction_both_clears(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "icmp"})
|
||||
m.return_value = conn
|
||||
link_marker_handler(
|
||||
{"project_id": "p", "link_id": "l", "action": "update",
|
||||
"marker_name": "icmp", "direction": "both"}, ctx,
|
||||
{"project_id": "p", "link_id": "l", "action": "update", "marker_name": "icmp", "direction": "both"},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"put", "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp",
|
||||
"put",
|
||||
"http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp",
|
||||
json_data={"direction": None},
|
||||
)
|
||||
|
||||
def test_update_direction_tx_sets(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "icmp"})
|
||||
m.return_value = conn
|
||||
link_marker_handler(
|
||||
{"project_id": "p", "link_id": "l", "action": "update",
|
||||
"marker_name": "icmp", "direction": "tx"}, ctx,
|
||||
{"project_id": "p", "link_id": "l", "action": "update", "marker_name": "icmp", "direction": "tx"},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"put", "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp",
|
||||
"put",
|
||||
"http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp",
|
||||
json_data={"direction": "tx"},
|
||||
)
|
||||
|
||||
def test_update_direction_omitted_preserved(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "icmp"})
|
||||
m.return_value = conn
|
||||
link_marker_handler(
|
||||
{"project_id": "p", "link_id": "l", "action": "update",
|
||||
"marker_name": "icmp", "tag": 1}, ctx,
|
||||
{"project_id": "p", "link_id": "l", "action": "update", "marker_name": "icmp", "tag": 1},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"put", "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp",
|
||||
"put",
|
||||
"http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp",
|
||||
json_data={"tag": 1},
|
||||
)
|
||||
|
||||
def test_create_direction_both_omitted(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "icmp"})
|
||||
m.return_value = conn
|
||||
link_marker_handler(
|
||||
{"project_id": "p", "link_id": "l", "action": "create",
|
||||
"bpf": "icmp", "direction": "both"}, ctx,
|
||||
{"project_id": "p", "link_id": "l", "action": "create", "bpf": "icmp", "direction": "both"},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"post", "http://192.168.1.3:3080/v3/projects/p/links/l/markers",
|
||||
"post",
|
||||
"http://192.168.1.3:3080/v3/projects/p/links/l/markers",
|
||||
json_data={"bpf": "icmp"},
|
||||
)
|
||||
|
||||
def test_create_data_link_type_passthrough(self, ctx):
|
||||
"""create passes a serial WAN encapsulation through; update ignores it."""
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "icmp"})
|
||||
m.return_value = conn
|
||||
link_marker_handler(
|
||||
{"project_id": "p", "link_id": "l", "action": "create",
|
||||
"bpf": "icmp", "data_link_type": "DLT_C_HDLC"}, ctx,
|
||||
{"project_id": "p", "link_id": "l", "action": "create", "bpf": "icmp", "data_link_type": "DLT_C_HDLC"},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"post", "http://192.168.1.3:3080/v3/projects/p/links/l/markers",
|
||||
"post",
|
||||
"http://192.168.1.3:3080/v3/projects/p/links/l/markers",
|
||||
json_data={"bpf": "icmp", "data_link_type": "DLT_C_HDLC"},
|
||||
)
|
||||
|
||||
conn = _mock_conn({"name": "icmp"})
|
||||
m.return_value = conn
|
||||
link_marker_handler(
|
||||
{"project_id": "p", "link_id": "l", "action": "update",
|
||||
"marker_name": "icmp", "tag": 1, "data_link_type": "DLT_PPP_SERIAL"}, ctx,
|
||||
{
|
||||
"project_id": "p",
|
||||
"link_id": "l",
|
||||
"action": "update",
|
||||
"marker_name": "icmp",
|
||||
"tag": 1,
|
||||
"data_link_type": "DLT_PPP_SERIAL",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
# create-only: dropped from the update body
|
||||
conn.http_call.assert_called_with(
|
||||
"put", "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp",
|
||||
"put",
|
||||
"http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp",
|
||||
json_data={"tag": 1},
|
||||
)
|
||||
|
||||
def test_create_direction_tx(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "icmp"})
|
||||
m.return_value = conn
|
||||
link_marker_handler(
|
||||
{"project_id": "p", "link_id": "l", "action": "create",
|
||||
"bpf": "icmp", "direction": "tx"}, ctx,
|
||||
{"project_id": "p", "link_id": "l", "action": "create", "bpf": "icmp", "direction": "tx"},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"post", "http://192.168.1.3:3080/v3/projects/p/links/l/markers",
|
||||
"post",
|
||||
"http://192.168.1.3:3080/v3/projects/p/links/l/markers",
|
||||
json_data={"bpf": "icmp", "direction": "tx"},
|
||||
)
|
||||
|
||||
@@ -782,67 +914,76 @@ class TestMarkerDefinition:
|
||||
meaning — any direction passed is ignored, never reaching the request body.
|
||||
"""
|
||||
|
||||
|
||||
def test_create_builds_body(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "arp"})
|
||||
m.return_value = conn
|
||||
marker_definition_handler(
|
||||
{"project_id": "p", "action": "create",
|
||||
"bpf": "arp", "tag": 1, "color": "#fff"}, ctx,
|
||||
{"project_id": "p", "action": "create", "bpf": "arp", "tag": 1, "color": "#fff"},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"post", "http://192.168.1.3:3080/v3/projects/p/marker-definitions",
|
||||
"post",
|
||||
"http://192.168.1.3:3080/v3/projects/p/marker-definitions",
|
||||
json_data={"bpf": "arp", "tag": 1, "color": "#fff"},
|
||||
)
|
||||
|
||||
def test_create_ignores_direction(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "arp"})
|
||||
m.return_value = conn
|
||||
marker_definition_handler(
|
||||
{"project_id": "p", "action": "create",
|
||||
"bpf": "arp", "direction": "tx"}, ctx,
|
||||
{"project_id": "p", "action": "create", "bpf": "arp", "direction": "tx"},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"post", "http://192.168.1.3:3080/v3/projects/p/marker-definitions",
|
||||
"post",
|
||||
"http://192.168.1.3:3080/v3/projects/p/marker-definitions",
|
||||
json_data={"bpf": "arp"},
|
||||
)
|
||||
|
||||
def test_update_builds_body(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "arp"})
|
||||
m.return_value = conn
|
||||
marker_definition_handler(
|
||||
{"project_id": "p", "action": "update",
|
||||
"def_name": "arp", "tag": 1}, ctx,
|
||||
{"project_id": "p", "action": "update", "def_name": "arp", "tag": 1},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"put", "http://192.168.1.3:3080/v3/projects/p/marker-definitions/arp",
|
||||
"put",
|
||||
"http://192.168.1.3:3080/v3/projects/p/marker-definitions/arp",
|
||||
json_data={"tag": 1},
|
||||
)
|
||||
|
||||
def test_update_ignores_direction(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler
|
||||
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn({"name": "arp"})
|
||||
m.return_value = conn
|
||||
marker_definition_handler(
|
||||
{"project_id": "p", "action": "update",
|
||||
"def_name": "arp", "tag": 1, "direction": "rx"}, ctx,
|
||||
{"project_id": "p", "action": "update", "def_name": "arp", "tag": 1, "direction": "rx"},
|
||||
ctx,
|
||||
)
|
||||
conn.http_call.assert_called_with(
|
||||
"put", "http://192.168.1.3:3080/v3/projects/p/marker-definitions/arp",
|
||||
"put",
|
||||
"http://192.168.1.3:3080/v3/projects/p/marker-definitions/arp",
|
||||
json_data={"tag": 1},
|
||||
)
|
||||
|
||||
def test_update_requires_a_field(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler
|
||||
|
||||
with patch(f"{AH}._get_connector"):
|
||||
result = marker_definition_handler(
|
||||
{"project_id": "p", "action": "update", "def_name": "arp"}, ctx,
|
||||
{"project_id": "p", "action": "update", "def_name": "arp"},
|
||||
ctx,
|
||||
)
|
||||
assert "error" in result
|
||||
@@ -8,6 +8,7 @@ This catches issues like:
|
||||
- A tool parameter is defined but never passed to the handler
|
||||
- A handler reads a param that was never defined or passed
|
||||
"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
@@ -112,7 +113,7 @@ def _get_handler_params(handler_name):
|
||||
filename = HANDLER_FILES.get(handler_name)
|
||||
if not filename:
|
||||
return None
|
||||
filepath = (MCP_DIR if not filename.startswith('gns3server/') else REPO_ROOT) / filename
|
||||
filepath = (MCP_DIR if not filename.startswith("gns3server/") else REPO_ROOT) / filename
|
||||
if not filepath.exists():
|
||||
return None
|
||||
|
||||
@@ -128,10 +129,13 @@ def _get_handler_params(handler_name):
|
||||
# request body from all params) accepts any key — return a wildcard and
|
||||
# let the caller skip static consistency checks for it.
|
||||
for sub in ast.walk(node):
|
||||
if (isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute)
|
||||
and sub.func.attr == "items"
|
||||
and isinstance(sub.func.value, ast.Name)
|
||||
and sub.func.value.id in ("params", "params_data")):
|
||||
if (
|
||||
isinstance(sub, ast.Call)
|
||||
and isinstance(sub.func, ast.Attribute)
|
||||
and sub.func.attr == "items"
|
||||
and isinstance(sub.func.value, ast.Name)
|
||||
and sub.func.value.id in ("params", "params_data")
|
||||
):
|
||||
return {"*"}
|
||||
# Found the handler function, search for params.get("xxx")
|
||||
for sub in ast.walk(node):
|
||||
@@ -141,8 +145,10 @@ def _get_handler_params(handler_name):
|
||||
continue
|
||||
# params.get("xxx") or params_data.get("xxx")
|
||||
func_obj = sub.func
|
||||
if (hasattr(func_obj.value, "id") and func_obj.value.id in ("params", "params_data", "link_data", "node_data")) or \
|
||||
(hasattr(func_obj.value, "attr") and func_obj.value.attr == "get"):
|
||||
if (
|
||||
hasattr(func_obj.value, "id")
|
||||
and func_obj.value.id in ("params", "params_data", "link_data", "node_data")
|
||||
) or (hasattr(func_obj.value, "attr") and func_obj.value.attr == "get"):
|
||||
if sub.args and isinstance(sub.args[0], ast.Constant) and isinstance(sub.args[0].value, str):
|
||||
params.add(sub.args[0].value)
|
||||
return params
|
||||
@@ -180,8 +186,12 @@ def _initial_params_keys(fn_node):
|
||||
"""
|
||||
for stmt in ast.walk(fn_node):
|
||||
if isinstance(stmt, ast.Assign):
|
||||
if (len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name)
|
||||
and stmt.targets[0].id == "params" and isinstance(stmt.value, ast.Dict)):
|
||||
if (
|
||||
len(stmt.targets) == 1
|
||||
and isinstance(stmt.targets[0], ast.Name)
|
||||
and stmt.targets[0].id == "params"
|
||||
and isinstance(stmt.value, ast.Dict)
|
||||
):
|
||||
return _dict_literal_keys(stmt.value)
|
||||
return None
|
||||
|
||||
@@ -196,8 +206,13 @@ def _dispatch_args(node):
|
||||
"""
|
||||
if isinstance(node.func, ast.Name) and node.func.id == "_run_handler_sync":
|
||||
args = node.args
|
||||
elif (isinstance(node.func, ast.Attribute) and node.func.attr == "to_thread"
|
||||
and node.args and isinstance(node.args[0], ast.Name) and node.args[0].id == "_run_handler_sync"):
|
||||
elif (
|
||||
isinstance(node.func, ast.Attribute)
|
||||
and node.func.attr == "to_thread"
|
||||
and node.args
|
||||
and isinstance(node.args[0], ast.Name)
|
||||
and node.args[0].id == "_run_handler_sync"
|
||||
):
|
||||
args = node.args[1:]
|
||||
else:
|
||||
return None, None
|
||||
@@ -238,8 +253,14 @@ def test_tool_handler_param_consistency():
|
||||
exact = True
|
||||
elif isinstance(second_arg, ast.Name) and second_arg.id == "params":
|
||||
# Tool builds 'params' as a variable — resolve its initial dict literal.
|
||||
fn = next((n for n in ast.walk(tree)
|
||||
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == tool_name), None)
|
||||
fn = next(
|
||||
(
|
||||
n
|
||||
for n in ast.walk(tree)
|
||||
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == tool_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
passed_keys = _initial_params_keys(fn) if fn is not None else None
|
||||
else:
|
||||
passed_keys = None
|
||||
@@ -259,9 +280,7 @@ def test_tool_handler_param_consistency():
|
||||
|
||||
# Check: every passed key is read by the handler
|
||||
extra_passed = passed_keys - handler_params
|
||||
assert not extra_passed, (
|
||||
f"[{tool_name}] Params passed to handler '{handler_name}' but not read: {extra_passed}"
|
||||
)
|
||||
assert not extra_passed, f"[{tool_name}] Params passed to handler '{handler_name}' but not read: {extra_passed}"
|
||||
|
||||
if not group["exact"]:
|
||||
# The initial literal underestimates what the tool passes (keys may be
|
||||
@@ -271,11 +290,21 @@ def test_tool_handler_param_consistency():
|
||||
# Check: every handler param is passed (except common/optional ones)
|
||||
missing = handler_params - passed_keys
|
||||
# Filter out well-known optional params that handlers check
|
||||
known_optional = {"fields", "template", "name", "version", "compute_id",
|
||||
"x", "y", "link_type", "filters", "suspend", "link_style",
|
||||
"show_filters_icon", "label"}
|
||||
known_optional = {
|
||||
"fields",
|
||||
"template",
|
||||
"name",
|
||||
"version",
|
||||
"compute_id",
|
||||
"x",
|
||||
"y",
|
||||
"link_type",
|
||||
"filters",
|
||||
"suspend",
|
||||
"link_style",
|
||||
"show_filters_icon",
|
||||
"label",
|
||||
}
|
||||
truly_missing = missing - known_optional
|
||||
if truly_missing:
|
||||
pytest.fail(
|
||||
f"[{tool_name}] Handler '{handler_name}' reads params not passed: {truly_missing}"
|
||||
)
|
||||
pytest.fail(f"[{tool_name}] Handler '{handler_name}' reads params not passed: {truly_missing}")
|
||||
Reference in new issue
Block a user