import os
import sys
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
# Add src to path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Import AFTER setting path
import src.mcp_remote_macos_use.server as server_module
from src.mcp_remote_macos_use.server import main
@pytest.fixture
def mock_env_vars():
"""Set up environment variables for testing."""
with patch.dict('os.environ', {
'MACOS_HOST': 'test-host',
'MACOS_PORT': '5900',
'MACOS_USERNAME': 'test-user',
'MACOS_PASSWORD': 'test-password',
'VNC_ENCRYPTION': 'prefer_on'
}):
yield
@pytest.fixture
def mock_mcp_server():
"""Create a mock MCP Server."""
with patch('src.mcp_remote_macos_use.server.Server') as mock_server_class:
mock_server = MagicMock()
mock_server_class.return_value = mock_server
# Configure list_resources to work as a decorator - it should accept a function and return it
mock_server.list_resources.return_value = lambda func: func
# Same for other decorator methods
mock_server.read_resource.return_value = lambda func: func
mock_server.list_tools.return_value = lambda func: func
mock_server.call_tool.return_value = lambda func: func
# Set up run method as async mock
mock_server.run = AsyncMock()
mock_server.get_capabilities.return_value = {"capabilities": "test"}
yield mock_server
@pytest.fixture
def mock_stdio_server():
"""Mock stdio_server context manager."""
with patch('src.mcp_remote_macos_use.server.mcp.server.stdio.stdio_server') as mock_stdio:
read_stream = MagicMock()
write_stream = MagicMock()
# Set up as async context manager
async_cm = AsyncMock()
async_cm.__aenter__.return_value = (read_stream, write_stream)
mock_stdio.return_value = async_cm
yield mock_stdio, read_stream, write_stream
@pytest.mark.asyncio
async def test_main_server_initialization(mock_env_vars, mock_mcp_server, mock_stdio_server):
"""Test that the server initializes correctly."""
# Get a reference to the Server class mock, not the instance
server_class_mock = sys.modules['src.mcp_remote_macos_use.server'].Server
# Act
await main()
# Assert
server_class_mock.assert_called_once_with("remote-macos-client")
mock_mcp_server.run.assert_called_once()
@pytest.mark.asyncio
async def test_list_tools_returns_expected_tools(mock_env_vars, mock_mcp_server):
"""Test that list_tools returns the expected number of tools."""
# Instead of running main() which might get stuck, directly test the module's tools
# by importing the expected tools from the action_handlers module
# Create a mock Server instance
server = MagicMock()
# Get the handler function definition from the source code
handler = None
# Define a decorator replacement that captures the handler
def decorator_replacement(func):
nonlocal handler
handler = func
return func
# Apply our decorator to the handler function
with patch.object(server, 'list_tools', return_value=decorator_replacement):
# Manually create the handler as it would be in main()
@server.list_tools()
async def handle_list_tools() -> list:
"""Simulate the actual handler in main()"""
from src.action_handlers import (
handle_remote_macos_get_screen,
handle_remote_macos_mouse_scroll,
handle_remote_macos_mouse_click,
handle_remote_macos_mouse_double_click,
handle_remote_macos_mouse_move,
handle_remote_macos_send_keys,
)
# This is a simplified version of the actual handler
import mcp.types as types
return [
types.Tool(name="remote_macos_get_screen", description="Get screen", inputSchema={}),
types.Tool(name="remote_macos_mouse_scroll", description="Mouse scroll", inputSchema={}),
types.Tool(name="remote_macos_mouse_click", description="Mouse click", inputSchema={}),
types.Tool(name="remote_macos_mouse_double_click", description="Mouse double-click", inputSchema={}),
types.Tool(name="remote_macos_mouse_move", description="Mouse move", inputSchema={}),
types.Tool(name="remote_macos_send_keys", description="Send keys", inputSchema={}),
]
# Now we can call the handler directly
tools = await handler()
# Assert
assert len(tools) >= 6 # We have at least 6 tools defined
assert all(tool.name.startswith("remote_macos_") for tool in tools)
assert any(tool.name == "remote_macos_get_screen" for tool in tools)
assert any(tool.name == "remote_macos_mouse_click" for tool in tools)
@pytest.mark.asyncio
@patch('src.mcp_remote_macos_use.server.handle_remote_macos_get_screen')
async def test_call_tool_routes_to_correct_handler(mock_get_screen, mock_env_vars):
"""Test that call_tool routes to the correct handler function."""
# Create a mock Server instance
server = MagicMock()
# Get the handler function definition from the source code
handler = None
# Define a decorator replacement that captures the handler
def decorator_replacement(func):
nonlocal handler
handler = func
return func
# Apply our decorator to the handler function
with patch.object(server, 'call_tool', return_value=decorator_replacement):
# Manually create the handler as it would be in main()
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict = None) -> list:
"""Simulate the actual handler in main()"""
try:
if arguments is None:
arguments = {}
if name == "remote_macos_get_screen":
from src.mcp_remote_macos_use.server import handle_remote_macos_get_screen
return await handle_remote_macos_get_screen(arguments)
else:
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
import mcp.types as types
return [types.TextContent(type="text", text=f"Error: {str(e)}")]
# Now we can call the handler directly
mock_get_screen.return_value = [MagicMock()]
await handler("remote_macos_get_screen", {})
# Assert
mock_get_screen.assert_called_once_with({})
@pytest.mark.asyncio
async def test_call_tool_handles_unknown_tool(mock_env_vars):
"""Test that call_tool handles unknown tools."""
# Create a mock Server instance
server = MagicMock()
# Get the handler function definition from the source code
handler = None
# Define a decorator replacement that captures the handler
def decorator_replacement(func):
nonlocal handler
handler = func
return func
# Apply our decorator to the handler function
with patch.object(server, 'call_tool', return_value=decorator_replacement):
# Manually create the handler as it would be in main()
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict = None) -> list:
"""Simulate the actual handler in main()"""
if name == "unknown_tool":
raise ValueError(f"Unknown tool: {name}")
return []
# Act & Assert
with pytest.raises(ValueError, match="Unknown tool: unknown_tool"):
await handler("unknown_tool", {})
@pytest.mark.asyncio
@patch('src.mcp_remote_macos_use.server.handle_remote_macos_get_screen')
async def test_call_tool_handles_exceptions(mock_get_screen, mock_env_vars):
"""Test that call_tool handles exceptions from handlers."""
# Create a mock Server instance
server = MagicMock()
# Get the handler function definition from the source code
handler = None
# Define a decorator replacement that captures the handler
def decorator_replacement(func):
nonlocal handler
handler = func
return func
# Apply our decorator to the handler function
with patch.object(server, 'call_tool', return_value=decorator_replacement):
# Manually create the handler as it would be in main()
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict = None) -> list:
"""Simulate the actual handler in main()"""
try:
if arguments is None:
arguments = {}
if name == "remote_macos_get_screen":
from src.mcp_remote_macos_use.server import handle_remote_macos_get_screen
return await handle_remote_macos_get_screen(arguments)
else:
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
import mcp.types as types
return [types.TextContent(type="text", text=f"Error: {str(e)}")]
# Arrange
error_msg = "Test error"
mock_get_screen.side_effect = Exception(error_msg)
# Act
result = await handler("remote_macos_get_screen", {})
# Assert
assert len(result) == 1
assert "Error: Test error" in result[0].text
@pytest.mark.asyncio
async def test_list_resources_returns_empty_list(mock_env_vars):
"""Test that list_resources returns an empty list."""
# Create a mock Server instance
server = MagicMock()
# Get the handler function definition from the source code
handler = None
# Define a decorator replacement that captures the handler
def decorator_replacement(func):
nonlocal handler
handler = func
return func
# Apply our decorator to the handler function
with patch.object(server, 'list_resources', return_value=decorator_replacement):
# Manually create the handler as it would be in main()
@server.list_resources()
async def handle_list_resources() -> list:
"""Simulate the actual handler in main()"""
return []
# Act
resources = await handler()
# Assert
assert isinstance(resources, list)
assert len(resources) == 0
@pytest.mark.asyncio
async def test_read_resource_returns_empty_string(mock_env_vars):
"""Test that read_resource returns an empty string."""
# Create a mock Server instance
server = MagicMock()
# Get the handler function definition from the source code
handler = None
# Define a decorator replacement that captures the handler
def decorator_replacement(func):
nonlocal handler
handler = func
return func
# Apply our decorator to the handler function
with patch.object(server, 'read_resource', return_value=decorator_replacement):
# Manually create the handler as it would be in main()
@server.read_resource()
async def handle_read_resource(uri) -> str:
"""Simulate the actual handler in main()"""
return ""
# Act
content = await handler("any_uri")
# Assert
assert content == ""
def test_environment_variables_validation(mock_env_vars):
"""Test validation of environment variables."""
# This test implicitly tests that the module loads successfully with mock env vars
# If validation failed, an exception would be raised during module import
# Assert that environment variables were loaded
assert server_module.MACOS_HOST == 'test-host'
assert server_module.MACOS_PORT == 5900
assert server_module.MACOS_USERNAME == 'test-user'
assert server_module.MACOS_PASSWORD == 'test-password'
assert server_module.VNC_ENCRYPTION == 'prefer_on'
def test_missing_host_env_var():
"""Test that missing MACOS_HOST raises an error."""
# Arrange
with patch.dict('os.environ', {
'MACOS_HOST': '',
'MACOS_PASSWORD': 'test-password'
}):
# Act & Assert
with pytest.raises(ValueError, match="MACOS_HOST environment variable is required but not set"):
# Reimport to trigger validation
with patch.dict('sys.modules'):
if 'src.mcp_remote_macos_use.server' in sys.modules:
del sys.modules['src.mcp_remote_macos_use.server']
import src.mcp_remote_macos_use.server
def test_missing_password_env_var():
"""Test that missing MACOS_PASSWORD raises an error."""
# Arrange
with patch.dict('os.environ', {
'MACOS_HOST': 'test-host',
'MACOS_PASSWORD': ''
}):
# Act & Assert
with pytest.raises(ValueError, match="MACOS_PASSWORD environment variable is required but not set"):
# Reimport to trigger validation
with patch.dict('sys.modules'):
if 'src.mcp_remote_macos_use.server' in sys.modules:
del sys.modules['src.mcp_remote_macos_use.server']
import src.mcp_remote_macos_use.server