mcmqtt/tests/unit/test_server_runners_comprehensive.py
Ryan Malloy 8ab61eb1df 🚀 Initial release: mcmqtt FastMCP MQTT Server v2025.09.17
Complete FastMCP MQTT integration server featuring:

 Core Features:
- FastMCP native Model Context Protocol server with MQTT tools
- Embedded MQTT broker support with zero-configuration spawning
- Modular architecture: CLI, config, logging, server, MQTT, MCP, broker
- Comprehensive testing: 70+ tests with 96%+ coverage
- Cross-platform support: Linux, macOS, Windows

🏗️ Architecture:
- Clean separation of concerns across 7 modules
- Async/await patterns throughout for maximum performance
- Pydantic models with validation and configuration management
- AMQTT pure Python embedded brokers
- Typer CLI framework with rich output formatting

🧪 Quality Assurance:
- pytest-cov with HTML reporting
- AsyncMock comprehensive unit testing
- Edge case coverage for production reliability
- Pre-commit hooks with black, ruff, mypy

📦 Production Ready:
- PyPI package with proper metadata
- MIT License
- Professional documentation
- uvx installation support
- MCP client integration examples

Perfect for AI agent coordination, IoT data collection, and
microservice communication with MQTT messaging patterns.
2025-09-17 05:46:08 -06:00

363 lines
14 KiB
Python

"""
Comprehensive unit tests for server runner modules.
Tests STDIO and HTTP server execution functionality.
"""
import pytest
import sys
from unittest.mock import Mock, AsyncMock, patch
from mcmqtt.server.runners import run_stdio_server, run_http_server
class TestRunStdioServer:
"""Test STDIO server runner functionality."""
@pytest.fixture
def mock_server(self):
"""Create a mock MQTT server."""
server = Mock()
server.mqtt_config = None
server._last_error = None
server.initialize_mqtt_client = AsyncMock(return_value=True)
server.connect_mqtt = AsyncMock()
server.disconnect_mqtt = AsyncMock()
server.get_mcp_server = Mock()
# Mock the FastMCP instance
mock_mcp = Mock()
mock_mcp.run_stdio_async = AsyncMock()
server.get_mcp_server.return_value = mock_mcp
return server
@pytest.mark.asyncio
async def test_run_stdio_server_no_auto_connect(self, mock_server):
"""Test STDIO server without auto-connect."""
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_stdio_server(mock_server, auto_connect=False)
# Verify no MQTT operations
mock_server.initialize_mqtt_client.assert_not_called()
mock_server.connect_mqtt.assert_not_called()
# Verify MCP server started
mock_server.get_mcp_server.assert_called_once()
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_stdio_async.assert_called_once()
@pytest.mark.asyncio
async def test_run_stdio_server_auto_connect_success(self, mock_server):
"""Test STDIO server with successful auto-connect."""
mock_config = Mock()
mock_config.broker_host = 'localhost'
mock_config.broker_port = 1883
mock_server.mqtt_config = mock_config
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_stdio_server(mock_server, auto_connect=True)
# Verify MQTT operations
mock_server.initialize_mqtt_client.assert_called_once_with(mock_config)
mock_server.connect_mqtt.assert_called_once()
# Verify logging
logger.info.assert_any_call(
"Auto-connecting to MQTT broker",
broker="localhost:1883"
)
logger.info.assert_any_call("Connected to MQTT broker")
@pytest.mark.asyncio
async def test_run_stdio_server_auto_connect_failure(self, mock_server):
"""Test STDIO server with failed auto-connect."""
mock_config = Mock()
mock_config.broker_host = 'localhost'
mock_config.broker_port = 1883
mock_server.mqtt_config = mock_config
mock_server.initialize_mqtt_client = AsyncMock(return_value=False)
mock_server._last_error = "Connection failed"
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_stdio_server(mock_server, auto_connect=True)
# Verify MQTT initialization attempted but connect not called
mock_server.initialize_mqtt_client.assert_called_once()
mock_server.connect_mqtt.assert_not_called()
# Verify warning logged
logger.warning.assert_called_once_with(
"Failed to connect to MQTT broker",
error="Connection failed"
)
@pytest.mark.asyncio
async def test_run_stdio_server_no_mqtt_config(self, mock_server):
"""Test STDIO server with no MQTT config and auto-connect."""
mock_server.mqtt_config = None
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_stdio_server(mock_server, auto_connect=True)
# Verify no MQTT operations when no config
mock_server.initialize_mqtt_client.assert_not_called()
mock_server.connect_mqtt.assert_not_called()
@pytest.mark.asyncio
async def test_run_stdio_server_keyboard_interrupt(self, mock_server):
"""Test STDIO server handling KeyboardInterrupt."""
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_stdio_async.side_effect = KeyboardInterrupt()
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_stdio_server(mock_server)
# Verify cleanup
mock_server.disconnect_mqtt.assert_called_once()
logger.info.assert_called_with("Server shutting down...")
@pytest.mark.asyncio
async def test_run_stdio_server_exception(self, mock_server):
"""Test STDIO server handling general exception."""
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_stdio_async.side_effect = Exception("Server error")
with patch('structlog.get_logger') as mock_logger, \
patch('sys.exit') as mock_exit:
logger = Mock()
mock_logger.return_value = logger
await run_stdio_server(mock_server)
# Verify cleanup and exit
mock_server.disconnect_mqtt.assert_called_once()
logger.error.assert_called_with("Server error", error="Server error")
mock_exit.assert_called_once_with(1)
@pytest.mark.asyncio
async def test_run_stdio_server_with_log_file(self, mock_server):
"""Test STDIO server with log file parameter."""
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_stdio_server(mock_server, log_file="/tmp/test.log")
# Should still run normally (log_file is passed but not used in runner)
mock_server.get_mcp_server.assert_called_once()
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_stdio_async.assert_called_once()
class TestRunHttpServer:
"""Test HTTP server runner functionality."""
@pytest.fixture
def mock_server(self):
"""Create a mock MQTT server."""
server = Mock()
server.mqtt_config = None
server._last_error = None
server.initialize_mqtt_client = AsyncMock(return_value=True)
server.connect_mqtt = AsyncMock()
server.disconnect_mqtt = AsyncMock()
server.get_mcp_server = Mock()
# Mock the FastMCP instance
mock_mcp = Mock()
mock_mcp.run_http_async = AsyncMock()
server.get_mcp_server.return_value = mock_mcp
return server
@pytest.mark.asyncio
async def test_run_http_server_default_params(self, mock_server):
"""Test HTTP server with default parameters."""
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_http_server(mock_server)
# Verify MCP server started with defaults
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_http_async.assert_called_once_with(host="0.0.0.0", port=3000)
@pytest.mark.asyncio
async def test_run_http_server_custom_params(self, mock_server):
"""Test HTTP server with custom parameters."""
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_http_server(mock_server, host="127.0.0.1", port=8080)
# Verify MCP server started with custom params
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_http_async.assert_called_once_with(host="127.0.0.1", port=8080)
@pytest.mark.asyncio
async def test_run_http_server_auto_connect_success(self, mock_server):
"""Test HTTP server with successful auto-connect."""
mock_config = Mock()
mock_config.broker_host = 'mqtt.example.com'
mock_config.broker_port = 8883
mock_server.mqtt_config = mock_config
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_http_server(mock_server, auto_connect=True)
# Verify MQTT connection
mock_server.initialize_mqtt_client.assert_called_once_with(mock_config)
mock_server.connect_mqtt.assert_called_once()
# Verify logging
logger.info.assert_any_call(
"Auto-connecting to MQTT broker",
broker="mqtt.example.com:8883"
)
logger.info.assert_any_call("Connected to MQTT broker")
@pytest.mark.asyncio
async def test_run_http_server_auto_connect_failure(self, mock_server):
"""Test HTTP server with failed auto-connect."""
mock_config = Mock()
mock_config.broker_host = 'mqtt.example.com'
mock_config.broker_port = 8883
mock_server.mqtt_config = mock_config
mock_server.initialize_mqtt_client = AsyncMock(return_value=False)
mock_server._last_error = "Connection failed"
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_http_server(mock_server, auto_connect=True)
# Verify MQTT initialization attempted but connect not called
mock_server.initialize_mqtt_client.assert_called_once()
mock_server.connect_mqtt.assert_not_called()
# Verify warning logged
logger.warning.assert_called_once_with(
"Failed to connect to MQTT broker",
error="Connection failed"
)
@pytest.mark.asyncio
async def test_run_http_server_no_mqtt_config(self, mock_server):
"""Test HTTP server with no MQTT config and auto-connect."""
mock_server.mqtt_config = None
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_http_server(mock_server, auto_connect=True)
# Verify no MQTT operations when no config
mock_server.initialize_mqtt_client.assert_not_called()
mock_server.connect_mqtt.assert_not_called()
@pytest.mark.asyncio
async def test_run_http_server_keyboard_interrupt(self, mock_server):
"""Test HTTP server handling KeyboardInterrupt."""
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_http_async.side_effect = KeyboardInterrupt()
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_http_server(mock_server)
# Verify cleanup
mock_server.disconnect_mqtt.assert_called_once()
logger.info.assert_called_with("Server shutting down...")
@pytest.mark.asyncio
async def test_run_http_server_exception(self, mock_server):
"""Test HTTP server handling general exception."""
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_http_async.side_effect = Exception("HTTP error")
with patch('structlog.get_logger') as mock_logger, \
patch('sys.exit') as mock_exit:
logger = Mock()
mock_logger.return_value = logger
await run_http_server(mock_server)
# Verify cleanup and exit
mock_server.disconnect_mqtt.assert_called_once()
logger.error.assert_called_with("Server error", error="HTTP error")
mock_exit.assert_called_once_with(1)
@pytest.mark.asyncio
async def test_run_http_server_extreme_ports(self, mock_server):
"""Test HTTP server with extreme port values."""
test_cases = [
(1, "0.0.0.0"), # Minimum port
(65535, "0.0.0.0"), # Maximum port
(8080, "127.0.0.1"), # Common development port
(443, "0.0.0.0"), # HTTPS port
(80, "0.0.0.0") # HTTP port
]
for port, host in test_cases:
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_http_server(mock_server, host=host, port=port)
# Verify MCP server called with correct parameters
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_http_async.assert_called_with(host=host, port=port)
# Reset for next test
mock_server.reset_mock()
@pytest.mark.asyncio
async def test_run_http_server_various_hosts(self, mock_server):
"""Test HTTP server with various host configurations."""
test_hosts = [
"0.0.0.0", # All interfaces
"127.0.0.1", # Localhost
"localhost", # Localhost name
"192.168.1.1", # Private IP
"::" # IPv6 all interfaces
]
for host in test_hosts:
with patch('structlog.get_logger') as mock_logger:
logger = Mock()
mock_logger.return_value = logger
await run_http_server(mock_server, host=host, port=3000)
# Verify MCP server called with correct host
mock_mcp = mock_server.get_mcp_server.return_value
mock_mcp.run_http_async.assert_called_with(host=host, port=3000)
# Reset for next test
mock_server.reset_mock()