106 lines
3.5 KiB
Python
106 lines
3.5 KiB
Python
import sys
|
|
from pathlib import Path
|
|
import pytest
|
|
import types
|
|
|
|
# locate server src dynamically to avoid hardcoded layout assumptions
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
|
|
sys.path.insert(0, str(SRC))
|
|
|
|
# Stub telemetry modules to avoid file I/O during import of tools package
|
|
telemetry = types.ModuleType("telemetry")
|
|
def _noop(*args, **kwargs):
|
|
pass
|
|
class MilestoneType: # minimal placeholder
|
|
pass
|
|
telemetry.record_resource_usage = _noop
|
|
telemetry.record_tool_usage = _noop
|
|
telemetry.record_milestone = _noop
|
|
telemetry.MilestoneType = MilestoneType
|
|
telemetry.get_package_version = lambda: "0.0.0"
|
|
sys.modules.setdefault("telemetry", telemetry)
|
|
|
|
telemetry_decorator = types.ModuleType("telemetry_decorator")
|
|
def telemetry_tool(*_args, **_kwargs):
|
|
def _wrap(fn):
|
|
return fn
|
|
return _wrap
|
|
telemetry_decorator.telemetry_tool = telemetry_tool
|
|
sys.modules.setdefault("telemetry_decorator", telemetry_decorator)
|
|
|
|
|
|
class DummyMCP:
|
|
def __init__(self):
|
|
self._tools = {}
|
|
|
|
def tool(self, *args, **kwargs): # accept kwargs like description
|
|
def deco(fn):
|
|
self._tools[fn.__name__] = fn
|
|
return fn
|
|
return deco
|
|
|
|
|
|
from tests.test_helpers import DummyContext
|
|
|
|
|
|
@pytest.fixture()
|
|
def resource_tools():
|
|
mcp = DummyMCP()
|
|
# Import the tools module to trigger decorator registration
|
|
import tools.resource_tools
|
|
# Get the registered tools from the registry
|
|
from registry import get_registered_tools
|
|
tools = get_registered_tools()
|
|
# Add all resource-related tools to our dummy MCP
|
|
for tool_info in tools:
|
|
tool_name = tool_info['name']
|
|
if any(keyword in tool_name for keyword in ['find_in_file', 'list_resources', 'read_resource']):
|
|
mcp._tools[tool_name] = tool_info['func']
|
|
return mcp._tools
|
|
|
|
|
|
def test_resource_list_filters_and_rejects_traversal(resource_tools, tmp_path, monkeypatch):
|
|
# Create fake project structure
|
|
proj = tmp_path
|
|
assets = proj / "Assets" / "Scripts"
|
|
assets.mkdir(parents=True)
|
|
(assets / "A.cs").write_text("// a", encoding="utf-8")
|
|
(assets / "B.txt").write_text("b", encoding="utf-8")
|
|
outside = tmp_path / "Outside.cs"
|
|
outside.write_text("// outside", encoding="utf-8")
|
|
# Symlink attempting to escape
|
|
sneaky_link = assets / "link_out"
|
|
try:
|
|
sneaky_link.symlink_to(outside)
|
|
except Exception:
|
|
# Some platforms may not allow symlinks in tests; ignore
|
|
pass
|
|
|
|
list_resources = resource_tools["list_resources"]
|
|
# Only .cs under Assets should be listed
|
|
import asyncio
|
|
resp = asyncio.run(
|
|
list_resources(ctx=DummyContext(), pattern="*.cs", under="Assets",
|
|
limit=50, project_root=str(proj))
|
|
)
|
|
assert resp["success"] is True
|
|
uris = resp["data"]["uris"]
|
|
assert any(u.endswith("Assets/Scripts/A.cs") for u in uris)
|
|
assert not any(u.endswith("B.txt") for u in uris)
|
|
assert not any(u.endswith("Outside.cs") for u in uris)
|
|
|
|
|
|
def test_resource_list_rejects_outside_paths(resource_tools, tmp_path):
|
|
proj = tmp_path
|
|
# under points outside Assets
|
|
list_resources = resource_tools["list_resources"]
|
|
import asyncio
|
|
resp = asyncio.run(
|
|
list_resources(ctx=DummyContext(), pattern="*.cs", under="..",
|
|
limit=10, project_root=str(proj))
|
|
)
|
|
assert resp["success"] is False
|
|
assert "Assets" in resp.get(
|
|
"error", "") or "under project root" in resp.get("error", "")
|