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:
cbdmaul
2025-08-17 13:28:47 -04:00
parent 1aec6127e6
commit 4bed20d54d
2 changed files with 68 additions and 28 deletions
+19 -13
View File
@@ -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:
+49 -15
View File
@@ -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 = {