rea/tests/test_server.py

299 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Tests for HopperProxyMCP server with mocked subprocess.
"""
import asyncio
import json
import os
import sys
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
class MockHopperClient:
def __init__(self, server_path=None):
self._server_info = {"name": "HopperMCPServer", "version": "0.1.0"}
self._started = False
self._call_history = []
def start(self):
self._started = True
return self._server_info
def stop(self):
self._started = False
def call_tool(self, tool_name, arguments=None):
arguments = arguments or {}
self._call_history.append({"tool": tool_name, "args": arguments})
if tool_name == "list_documents":
return ["test_binary"]
elif tool_name == "list_segments":
return [{"name": "__TEXT", "start": "0x100000000", "end": "0x1000e0000"}]
elif tool_name == "list_procedures":
return {
"0x1040f4124": "-[_TtC6Signal29AccountSettingsViewController viewDidLoad]",
"0x1040f4150": "_TtC6Signal29AccountSettingsViewController"
}
elif tool_name == "procedure_pseudo_code":
return "func viewDidLoad() { self.setup() }"
elif tool_name == "search_procedures":
return {"0x1040f4124": "-[_TtC6Signal29AccountSettingsViewController viewDidLoad]"}
elif tool_name == "procedure_callees":
return ["0x1040f4160", "0x1040f4170"]
elif tool_name == "procedure_callers":
return ["0x1040f4100"]
elif tool_name == "list_names":
return [
{"address": "0x1000a0000", "name": "_OBJC_CLASS_$_SignalAppDelegate"},
{"address": "0x1000a0010", "name": "_TtC6Signal11AppDelegate"}
]
elif tool_name == "address_name":
return {"address": "0x1040f4124", "name": "viewDidLoad"}
elif tool_name == "xrefs":
return ["0x1040f4100", "0x1040f4110"]
elif tool_name == "list_strings":
return [{"address": "0x100020000", "value": "Hello World"}]
elif tool_name == "set_comment":
return "OK"
elif tool_name == "set_address_name":
return "OK"
else:
return {"result": "ok"}
@property
def server_info(self):
return self._server_info
@pytest.fixture
def mock_client():
return MockHopperClient()
@pytest.fixture
def server_module(mock_client):
with patch("HopperProxyMCP.hopper_client.HopperMCPSubprocessClient") as mock_class:
mock_instance = MagicMock()
mock_instance.start.return_value = {"name": "HopperMCPServer", "version": "0.1.0"}
mock_instance.stop.return_value = None
mock_instance.call_tool.side_effect = mock_client.call_tool
mock_class.return_value = mock_instance
from fastmcp import Client
import importlib
import HopperProxyMCP.server as server_mod
importlib.reload(server_mod)
yield server_mod
class TestProxyTools:
@pytest.mark.asyncio
async def test_binary_overview(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("binary_overview", {})
assert "test_binary" in result.data
assert "__TEXT" in result.data
@pytest.mark.asyncio
async def test_swift_classes(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("swift_classes", {"pattern": ""})
data = json.loads(result.data)
assert data["count"] >= 1
assert any("_TtC" in c["name"] for c in data["classes"])
@pytest.mark.asyncio
async def test_get_objc_classes(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("get_objc_classes", {"pattern": ""})
data = json.loads(result.data)
assert data["count"] >= 1
@pytest.mark.asyncio
async def test_get_objc_protocols(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("get_objc_protocols", {})
data = json.loads(result.data)
assert "count" in data
@pytest.mark.asyncio
async def test_batch_decompile(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("batch_decompile", {
"addresses": ["0x1040f4124", "0x1040f4150"]
})
data = json.loads(result.data)
assert "0x1040f4124" in data
@pytest.mark.asyncio
async def test_get_call_graph_forward(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("get_call_graph", {
"address": "0x1040f4124",
"direction": "forward",
"depth": 2
})
data = json.loads(result.data)
assert "0" in data or 0 in data
@pytest.mark.asyncio
async def test_get_call_graph_backward(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("get_call_graph", {
"address": "0x1040f4124",
"direction": "backward",
"depth": 2
})
data = json.loads(result.data)
assert "0" in data or 0 in data
@pytest.mark.asyncio
async def test_analyze_swift_types(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("analyze_swift_types", {})
data = json.loads(result.data)
assert "total" in data
assert "categories" in data
@pytest.mark.asyncio
async def test_find_xrefs_to_name(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("find_xrefs_to_name", {
"name": "viewDidLoad"
})
data = json.loads(result.data)
class TestOfficialToolProxying:
@pytest.mark.asyncio
async def test_list_documents(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("list_documents", {})
assert "test_binary" in result.data
@pytest.mark.asyncio
async def test_list_segments(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("list_segments", {})
assert "__TEXT" in result.data
@pytest.mark.asyncio
async def test_list_procedures(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("list_procedures", {})
assert "_TtC" in result.data
@pytest.mark.asyncio
async def test_procedure_pseudo_code(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("procedure_pseudo_code", {
"procedure": "0x1040f4124"
})
assert "viewDidLoad" in result.data
@pytest.mark.asyncio
async def test_search_procedures(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("search_procedures", {
"pattern": "viewDidLoad",
"case_sensitive": False
})
assert "viewDidLoad" in result.data
@pytest.mark.asyncio
async def test_procedure_callees(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("procedure_callees", {
"procedure": "0x1040f4124"
})
assert "0x1040f4160" in result.data or "0x1040f4170" in result.data
@pytest.mark.asyncio
async def test_procedure_callers(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("procedure_callers", {
"procedure": "0x1040f4124"
})
assert "0x1040f4100" in result.data
@pytest.mark.asyncio
async def test_list_names(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("list_names", {})
assert "_OBJC_CLASS_" in result.data or "_TtC" in result.data
@pytest.mark.asyncio
async def test_address_name(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("address_name", {
"address": "0x1040f4124"
})
assert "viewDidLoad" in result.data
@pytest.mark.asyncio
async def test_xrefs(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("xrefs", {
"address": "0x1040f4124"
})
assert "0x1040f4100" in result.data
@pytest.mark.asyncio
async def test_set_comment(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("set_comment", {
"address": "0x1040f4124",
"comment": "Test comment",
"document": "test_binary"
})
assert result.data in ["OK", '{"result": "ok"}']
@pytest.mark.asyncio
async def test_set_address_name(self, server_module, mock_client):
from fastmcp import Client
async with Client(server_module.mcp) as client:
result = await client.call_tool("set_address_name", {
"address": "0x1040f4124",
"name": "myFunction",
"document": "test_binary"
})
assert result.data in ["OK", '{"result": "ok"}']
class TestMockClient:
def test_call_history(self, mock_client):
mock_client.call_tool("list_documents")
mock_client.call_tool("list_segments")
assert len(mock_client._call_history) == 2
assert mock_client._call_history[0]["tool"] == "list_documents"
assert mock_client._call_history[1]["tool"] == "list_segments"
if __name__ == "__main__":
pytest.main([__file__, "-v"])