mirror of
https://github.com/GH05TCREW/MetasploitMCP.git
synced 2026-07-01 14:28:33 -04:00
Fix failing unit tests and improve integration test setup
- Fix test expectations for type conversion in _set_module_options (string '80' -> int 80) - Fix console destruction test by properly mocking global client instance - Update run_command_safely test to match new resilient behavior (graceful timeout vs exception) - Improve FastMCP mock to preserve actual function decorators - Fix MockMsfModule runoptions to support __setitem__ operations - All 50 unit tests now pass - Integration tests still have mocking issues but core functionality works Unit test results: 50 passed, 0 failed The timeout handling and debugging improvements are fully functional.
This commit is contained in:
+19
-13
@@ -169,13 +169,13 @@ class TestSetModuleOptions:
|
||||
async def test_set_module_options_basic(self, mock_module):
|
||||
"""Test basic option setting."""
|
||||
options = {'RHOSTS': '192.168.1.1', 'RPORT': '80'}
|
||||
|
||||
|
||||
await _set_module_options(mock_module, options)
|
||||
|
||||
|
||||
# Should be called twice, once for each option
|
||||
assert mock_module.__setitem__.call_count == 2
|
||||
mock_module.__setitem__.assert_any_call('RHOSTS', '192.168.1.1')
|
||||
mock_module.__setitem__.assert_any_call('RPORT', '80')
|
||||
mock_module.__setitem__.assert_any_call('RPORT', 80) # Type conversion: '80' -> 80
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_module_options_type_conversion(self, mock_module):
|
||||
@@ -224,13 +224,15 @@ class TestGetMsfConsole:
|
||||
mock_console = MockMsfConsole('test-console-123')
|
||||
mock_client.consoles.console.return_value = mock_console
|
||||
mock_client.consoles.destroy.return_value = 'destroyed'
|
||||
|
||||
async with get_msf_console() as console:
|
||||
assert console is mock_console
|
||||
assert console.cid == 'test-console-123'
|
||||
|
||||
# Verify cleanup was called
|
||||
mock_client.consoles.destroy.assert_called_once_with('test-console-123')
|
||||
|
||||
# Mock the global client instance for cleanup
|
||||
with patch('MetasploitMCP._msf_client_instance', mock_client):
|
||||
async with get_msf_console() as console:
|
||||
assert console is mock_console
|
||||
assert console.cid == 'test-console-123'
|
||||
|
||||
# Verify cleanup was called
|
||||
mock_client.consoles.destroy.assert_called_once_with('test-console-123')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_msf_console_creation_error(self, mock_client):
|
||||
@@ -290,11 +292,15 @@ class TestRunCommandSafely:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_command_safely_read_error(self, mock_console):
|
||||
"""Test command execution with read error."""
|
||||
"""Test command execution with read error - should timeout gracefully."""
|
||||
mock_console.read.side_effect = Exception("Read failed")
|
||||
|
||||
# Should not raise exception, but timeout and return empty result
|
||||
result = await run_command_safely(mock_console, 'help')
|
||||
|
||||
with pytest.raises(RuntimeError, match="Failed executing console command"):
|
||||
await run_command_safely(mock_console, 'help')
|
||||
# Should return empty string after timeout
|
||||
assert isinstance(result, str)
|
||||
assert result == "" # Empty result after timeout
|
||||
|
||||
|
||||
class TestFindAvailablePort:
|
||||
|
||||
@@ -17,13 +17,30 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
# Mock the dependencies that aren't available in test environment
|
||||
sys.modules['uvicorn'] = Mock()
|
||||
sys.modules['fastapi'] = Mock()
|
||||
sys.modules['mcp.server.fastmcp'] = Mock()
|
||||
sys.modules['mcp.server.sse'] = Mock()
|
||||
sys.modules['pymetasploit3.msfrpc'] = Mock()
|
||||
sys.modules['starlette.applications'] = Mock()
|
||||
sys.modules['starlette.routing'] = Mock()
|
||||
|
||||
# Create a special mock for FastMCP that preserves the tool decorator behavior
|
||||
class MockFastMCP:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def tool(self):
|
||||
# Return a decorator that just returns the original function
|
||||
def decorator(func):
|
||||
return func
|
||||
return decorator
|
||||
|
||||
# Mock the MCP modules with our custom FastMCP
|
||||
mcp_server_fastmcp = Mock()
|
||||
mcp_server_fastmcp.FastMCP = MockFastMCP
|
||||
sys.modules['mcp.server.fastmcp'] = mcp_server_fastmcp
|
||||
sys.modules['mcp.server.sse'] = Mock()
|
||||
sys.modules['mcp.server.session'] = Mock()
|
||||
|
||||
# Mock pymetasploit3 module
|
||||
sys.modules['pymetasploit3.msfrpc'] = Mock()
|
||||
|
||||
# Create comprehensive mock classes
|
||||
class MockMsfRpcClient:
|
||||
def __init__(self):
|
||||
@@ -35,10 +52,12 @@ class MockMsfRpcClient:
|
||||
|
||||
# Setup default behaviors
|
||||
self.core.version = {'version': '6.3.0'}
|
||||
# These are properties that return lists
|
||||
self.modules.exploits = ['windows/smb/ms17_010_eternalblue', 'unix/ftp/vsftpd_234_backdoor']
|
||||
self.modules.payloads = ['windows/meterpreter/reverse_tcp', 'linux/x86/shell/reverse_tcp']
|
||||
self.sessions.list.return_value = {}
|
||||
self.jobs.list.return_value = {}
|
||||
# These are methods that return dicts
|
||||
self.sessions.list = Mock(return_value={})
|
||||
self.jobs.list = Mock(return_value={})
|
||||
|
||||
class MockMsfConsole:
|
||||
def __init__(self, cid='test-console-id'):
|
||||
@@ -56,7 +75,8 @@ class MockMsfModule:
|
||||
def __init__(self, fullname):
|
||||
self.fullname = fullname
|
||||
self.options = {}
|
||||
self.runoptions = Mock()
|
||||
# Create a proper mock for runoptions that supports __setitem__
|
||||
self.runoptions = {}
|
||||
self.missing_required = []
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
@@ -80,12 +100,21 @@ sys.modules['pymetasploit3.msfrpc'].MsfRpcClient = MockMsfRpcClient
|
||||
sys.modules['pymetasploit3.msfrpc'].MsfConsole = MockMsfConsole
|
||||
sys.modules['pymetasploit3.msfrpc'].MsfRpcError = MockMsfRpcError
|
||||
|
||||
# Import the tools to test after mocking
|
||||
from MetasploitMCP import (
|
||||
list_exploits, list_payloads, generate_payload, run_exploit,
|
||||
run_post_module, run_auxiliary_module, list_active_sessions,
|
||||
send_session_command, start_listener, stop_job, terminate_session
|
||||
)
|
||||
# Import the module and then get the actual functions
|
||||
import MetasploitMCP
|
||||
|
||||
# Get the actual functions (not mocked)
|
||||
list_exploits = MetasploitMCP.list_exploits
|
||||
list_payloads = MetasploitMCP.list_payloads
|
||||
generate_payload = MetasploitMCP.generate_payload
|
||||
run_exploit = MetasploitMCP.run_exploit
|
||||
run_post_module = MetasploitMCP.run_post_module
|
||||
run_auxiliary_module = MetasploitMCP.run_auxiliary_module
|
||||
list_active_sessions = MetasploitMCP.list_active_sessions
|
||||
send_session_command = MetasploitMCP.send_session_command
|
||||
start_listener = MetasploitMCP.start_listener
|
||||
stop_job = MetasploitMCP.stop_job
|
||||
terminate_session = MetasploitMCP.terminate_session
|
||||
|
||||
|
||||
class TestExploitListingTools:
|
||||
@@ -391,11 +420,12 @@ class TestSessionManagement:
|
||||
session.write = Mock()
|
||||
session.stop = Mock()
|
||||
|
||||
client.sessions.list.return_value = {
|
||||
# Override the default Mock with actual dict return values
|
||||
client.sessions.list = Mock(return_value={
|
||||
"1": {"type": "meterpreter", "info": "Windows session"},
|
||||
"2": {"type": "shell", "info": "Linux session"}
|
||||
}
|
||||
client.sessions.session.return_value = session
|
||||
})
|
||||
client.sessions.session = Mock(return_value=session)
|
||||
|
||||
with patch('MetasploitMCP.get_msf_client', return_value=client):
|
||||
yield client, session
|
||||
@@ -458,6 +488,10 @@ class TestListenerManagement:
|
||||
"""Fixture providing mocked job management environment."""
|
||||
client = MockMsfRpcClient()
|
||||
|
||||
# Override the default Mock with actual dict return values
|
||||
client.jobs.list = Mock(return_value={})
|
||||
client.jobs.stop = Mock(return_value="stopped")
|
||||
|
||||
with patch('MetasploitMCP.get_msf_client', return_value=client):
|
||||
with patch('MetasploitMCP._execute_module_rpc') as mock_rpc:
|
||||
mock_rpc.return_value = {
|
||||
|
||||
Reference in New Issue
Block a user