gns3-server/tests/agent/mcp/test_handlers.py
YueGuobin df25e037ea
fix: accept the 'local' compute id in compute tools
The compute_get/compute_images MCP tools typed compute_id as a UUID, so
passing 'local' (the actual id of the built-in compute, which the
compute_images description itself pointed to) was rejected by schema
validation. Both tools now take a string defaulting to 'local', and the
compute_get REST route resolves 'local' through the controller since the
local compute has no database entry.
2026-08-26 00:44:57 +08:00

771 lines
36 KiB
Python

"""
MCP handler unit tests with mocked Gns3Connector.
Tests that handlers correctly transform tool parameters into HTTP calls.
"""
import json
import pytest
from unittest.mock import MagicMock, patch
def _mock_conn(json_result=None):
"""Create a mocked Gns3Connector with base_url and http_call."""
conn = MagicMock()
conn.base_url = "http://192.168.1.3:3080/v3"
conn.http_call.return_value.json.return_value = json_result or {"status": "ok"}
conn.http_call.return_value.content = b"{}" # non-empty body by default
return conn
BASE = "gns3server.agent.mcp"
AH = "gns3server.agent.gns3_copilot.gns3_client.api_handlers" # node/link handlers sunk here
@pytest.fixture
def ctx():
return {"server_url": "http://192.168.1.3:3080", "jwt_token": "token", "jwt_username": "admin"}
# ── Project ─────────────────────────────────────────────────────────────
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)
assert result["count"] == 1
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)
assert result["project_id"] == "p1"
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
result = create_project_handler({"name": "New", "auto_close": False}, ctx)
assert result["project_id"] == "p1"
conn.http_call.assert_called_once_with(
"post", f"{conn.base_url}/projects", json_data={"name": "New", "auto_close": False}
)
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"}
)
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)
assert "message" in result
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)
assert result["status"] == "opened"
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)
assert "error" not in result
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)
assert result["name"] == "Updated"
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)
assert result["nodes"] == 5
# ── Node ────────────────────────────────────────────────────────────────
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},
])
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)
assert "error" in result
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)
assert result["name"] == "R1"
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)
conn.http_call.assert_called_with(
"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)
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)
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)
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)
assert [r["node"]["name"] for r in result] == ["slow", "mid", "fast"]
def test_create_batch_default_names_created_sequentially(self, ctx):
import threading
import time
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler
def _run(nodes_param):
with patch(f"{AH}._get_connector") as m:
conn = _mock_conn()
lock = threading.Lock()
active = [0, 0] # in-flight requests, high-water mark
counter = [0]
def _http_call(method, url, json_data=None, **kwargs):
with lock:
active[0] += 1
active[1] = max(active[1], active[0])
counter[0] += 1
seq = counter[0]
time.sleep(0.05) # wide enough that parallel calls would overlap
with lock:
active[0] -= 1
resp = MagicMock()
resp.json.return_value = {"node_id": f"n{seq}", "name": json_data.get("name", f"R-{seq}")}
return resp
conn.http_call.side_effect = _http_call
m.return_value = conn
result = create_node_handler({"project_id": "p1", "template_id": "t1", "nodes": nodes_param}, ctx)
return result, active[1]
# nodes relying on default naming are created one at a time so the
# server assigns default names/console ports in submission order
result, max_active = _run([{}, {}, {}])
assert [r["node"]["name"] for r in result] == ["R-1", "R-2", "R-3"]
assert max_active == 1
# one nameless node is enough to serialize the whole batch
result, max_active = _run([{"name": "explicit"}, {}])
assert [r["node"]["name"] for r in result] == ["explicit", "R-2"]
assert max_active == 1
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)
assert len(result) == 2
# same status vocabulary as create/start/stop batches
assert all(r["status"] == "success" for r in result)
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)
assert result[0]["status"] == "success"
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)
assert result[0]["status"] == "success"
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)
assert result[0]["status"] == "success"
def test_console(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_url": "ws://host/console"})
result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, ctx)
assert "command" in result
@staticmethod
def _file_conn(text):
conn = _mock_conn()
conn.http_call.return_value.text = text
return conn
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)
assert result["content"] == "line1\nline2\n"
assert result["metadata"]["total_bytes"] == 12
assert result["metadata"]["returned_bytes"] == 12
assert result["metadata"]["has_more"] is False
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)
assert result["content"] == "line1\r\nline2\r\n"
assert result["metadata"]["returned_bytes"] == 14
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)
assert result["content"] == "line1\nline2"
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(
{"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg", "offset": 1, "limit": 1}, ctx
)
assert result["content"] == "line2\n"
assert result["metadata"]["total_lines"] == 3
assert result["metadata"]["returned_lines"] == 1
assert result["metadata"]["has_more"] is True
# ── Link ────────────────────────────────────────────────────────────────
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)
assert result["links"] == [{"link_id": "l1"}]
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)
assert result["link_id"] == "l1"
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)
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},
]},
)
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)
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)
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)
assert len(result) == 2
# same status vocabulary as create batches
assert all(r["status"] == "success" for r in result)
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"]
time.sleep(0.25 if first_node == "n1" else 0.0)
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)
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)
assert result["suspend"] is True
# ── Appliance ───────────────────────────────────────────────────────────
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)
assert result["name"] == "Cisco ISE"
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)
conn.http_call.assert_called_with(
"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",
}
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""
conn.http_call.return_value.json.side_effect = json.JSONDecodeError("Expecting value", "", 0)
m.return_value = conn
result = install_appliance_handler({"appliance_id": "a1"}, ctx)
assert "template" not in result
assert result["message"] == "Appliance a1 installed"
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
# ── Template ────────────────────────────────────────────────────────────
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}"},
])
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)
assert "error" in result
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)
assert result["name"] == "Test"
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)
assert "deleted" in str(result).lower()
# ── Image ───────────────────────────────────────────────────────────────
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"}],
})
m.return_value = conn
result = install_images_handler({}, ctx)
assert result["created"][0]["name"] == "Empty VM"
assert result["skipped"][0]["name"] == "csr1000v.qcow2"
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""
conn.http_call.return_value.json.side_effect = json.JSONDecodeError("Expecting value", "", 0)
m.return_value = conn
result = install_images_handler({}, ctx)
assert result == {"message": "Image installation completed"}
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
result = get_compute_handler({}, ctx)
assert result["compute_id"] == "local"
url = conn.http_call.call_args[0][1]
assert url.endswith("/computes/local")
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
get_compute_handler({"compute_id": "4fcfb6b5-5b0b-4f43-bd5e-e8ae2a69c8e6"}, ctx)
url = conn.http_call.call_args[0][1]
assert url.endswith("/computes/4fcfb6b5-5b0b-4f43-bd5e-e8ae2a69c8e6")
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
result = get_compute_images_handler({"emulator": "qemu"}, ctx)
assert result["count"] == 1
url = conn.http_call.call_args[0][1]
assert url.endswith("/computes/local/qemu/images")
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)
# ── Marker (traffic-insight) ────────────────────────────────────────────
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,
)
conn.http_call.assert_called_with(
"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,
)
conn.http_call.assert_called_with(
"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,
)
conn.http_call.assert_called_with(
"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,
)
conn.http_call.assert_called_with(
"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,
)
conn.http_call.assert_called_with(
"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,
)
# 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",
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,
)
conn.http_call.assert_called_with(
"post", "http://192.168.1.3:3080/v3/projects/p/links/l/markers",
json_data={"bpf": "icmp", "direction": "tx"},
)
class TestMarkerDefinition:
"""marker_definition_handler build create/update bodies.
A definition has NO direction: it fans out to every link and auto-selects its
capture node on each, so tx/rx (relative to that node) has no consistent
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,
)
conn.http_call.assert_called_with(
"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,
)
conn.http_call.assert_called_with(
"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,
)
conn.http_call.assert_called_with(
"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,
)
conn.http_call.assert_called_with(
"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,
)
assert "error" in result