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.
361 lines
14 KiB
Python
361 lines
14 KiB
Python
"""
|
|
Comprehensive unit tests for the new simplified mcmqtt main module.
|
|
|
|
Tests the main entry point orchestration and startup logic.
|
|
"""
|
|
|
|
import pytest
|
|
import sys
|
|
from unittest.mock import patch, Mock, AsyncMock
|
|
from argparse import Namespace
|
|
|
|
from mcmqtt.mcmqtt import main
|
|
|
|
|
|
class TestMain:
|
|
"""Test main entry point functionality."""
|
|
|
|
def test_main_version_flag(self):
|
|
"""Test main with version flag."""
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.get_version', return_value="1.0.0"), \
|
|
patch('sys.exit') as mock_exit, \
|
|
patch('builtins.print') as mock_print:
|
|
|
|
# Mock version argument
|
|
args = Mock()
|
|
args.version = True
|
|
args.log_level = "INFO"
|
|
args.log_file = None
|
|
mock_parse.return_value = args
|
|
|
|
main()
|
|
|
|
mock_print.assert_called_once_with("mcmqtt version 1.0.0")
|
|
mock_exit.assert_called_once_with(0)
|
|
|
|
def test_main_stdio_default(self):
|
|
"""Test main with default STDIO transport."""
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.setup_logging') as mock_setup_log, \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_args', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_env', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.MCMQTTServer') as mock_server_class, \
|
|
patch('asyncio.run') as mock_asyncio_run, \
|
|
patch('structlog.get_logger') as mock_logger:
|
|
|
|
# Mock arguments
|
|
args = Mock()
|
|
args.version = False
|
|
args.log_level = "WARNING"
|
|
args.log_file = None
|
|
args.mqtt_host = None
|
|
args.transport = "stdio"
|
|
args.auto_connect = False
|
|
mock_parse.return_value = args
|
|
|
|
logger = Mock()
|
|
mock_logger.return_value = logger
|
|
mock_server = Mock()
|
|
mock_server_class.return_value = mock_server
|
|
|
|
main()
|
|
|
|
# Verify logging setup
|
|
mock_setup_log.assert_called_once_with("WARNING", None)
|
|
|
|
# Verify server creation
|
|
mock_server_class.assert_called_once_with(None)
|
|
|
|
# Verify asyncio.run called for STDIO
|
|
mock_asyncio_run.assert_called_once()
|
|
|
|
def test_main_http_transport(self):
|
|
"""Test main with HTTP transport."""
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.setup_logging'), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_args', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_env', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.MCMQTTServer') as mock_server_class, \
|
|
patch('asyncio.run') as mock_asyncio_run, \
|
|
patch('structlog.get_logger'):
|
|
|
|
# Mock arguments for HTTP transport
|
|
args = Mock()
|
|
args.version = False
|
|
args.log_level = "INFO"
|
|
args.log_file = "/tmp/test.log"
|
|
args.mqtt_host = None
|
|
args.transport = "http"
|
|
args.host = "127.0.0.1"
|
|
args.port = 8080
|
|
args.auto_connect = True
|
|
mock_parse.return_value = args
|
|
|
|
mock_server = Mock()
|
|
mock_server_class.return_value = mock_server
|
|
|
|
main()
|
|
|
|
# Verify asyncio.run called for HTTP
|
|
mock_asyncio_run.assert_called_once()
|
|
|
|
def test_main_mqtt_command_line_args(self):
|
|
"""Test main with MQTT configuration from command line."""
|
|
mock_config = Mock()
|
|
mock_config.broker_host = 'mqtt.test.com'
|
|
mock_config.broker_port = 8883
|
|
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.setup_logging'), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_args', return_value=mock_config), \
|
|
patch('mcmqtt.mcmqtt.MCMQTTServer') as mock_server_class, \
|
|
patch('asyncio.run'), \
|
|
patch('structlog.get_logger') as mock_logger:
|
|
|
|
# Mock arguments with MQTT settings
|
|
args = Mock()
|
|
args.version = False
|
|
args.log_level = "DEBUG"
|
|
args.log_file = None
|
|
args.mqtt_host = 'mqtt.test.com'
|
|
args.mqtt_port = 8883
|
|
args.transport = "stdio"
|
|
args.auto_connect = False
|
|
mock_parse.return_value = args
|
|
|
|
logger = Mock()
|
|
mock_logger.return_value = logger
|
|
mock_server = Mock()
|
|
mock_server_class.return_value = mock_server
|
|
|
|
main()
|
|
|
|
# Verify server created with MQTT config
|
|
mock_server_class.assert_called_once_with(mock_config)
|
|
|
|
# Verify command line config logging
|
|
logger.info.assert_any_call(
|
|
"MQTT configuration from command line",
|
|
broker="mqtt.test.com:8883"
|
|
)
|
|
|
|
def test_main_mqtt_environment_config(self):
|
|
"""Test main with MQTT configuration from environment."""
|
|
mock_config = Mock()
|
|
mock_config.broker_host = 'env.mqtt.com'
|
|
mock_config.broker_port = 1883
|
|
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.setup_logging'), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_args', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_env', return_value=mock_config), \
|
|
patch('mcmqtt.mcmqtt.MCMQTTServer') as mock_server_class, \
|
|
patch('asyncio.run'), \
|
|
patch('structlog.get_logger') as mock_logger:
|
|
|
|
# Mock arguments with no MQTT settings
|
|
args = Mock()
|
|
args.version = False
|
|
args.log_level = "WARNING"
|
|
args.log_file = None
|
|
args.mqtt_host = None
|
|
args.transport = "stdio"
|
|
args.auto_connect = False
|
|
mock_parse.return_value = args
|
|
|
|
logger = Mock()
|
|
mock_logger.return_value = logger
|
|
mock_server = Mock()
|
|
mock_server_class.return_value = mock_server
|
|
|
|
main()
|
|
|
|
# Verify server created with env config
|
|
mock_server_class.assert_called_once_with(mock_config)
|
|
|
|
# Verify environment config logging
|
|
logger.info.assert_any_call(
|
|
"MQTT configuration from environment",
|
|
broker="env.mqtt.com:1883"
|
|
)
|
|
|
|
def test_main_no_mqtt_config(self):
|
|
"""Test main with no MQTT configuration."""
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.setup_logging'), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_args', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_env', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.MCMQTTServer') as mock_server_class, \
|
|
patch('asyncio.run'), \
|
|
patch('structlog.get_logger') as mock_logger:
|
|
|
|
# Mock arguments with no MQTT settings
|
|
args = Mock()
|
|
args.version = False
|
|
args.log_level = "ERROR"
|
|
args.log_file = None
|
|
args.mqtt_host = None
|
|
args.transport = "stdio"
|
|
args.auto_connect = False
|
|
mock_parse.return_value = args
|
|
|
|
logger = Mock()
|
|
mock_logger.return_value = logger
|
|
mock_server = Mock()
|
|
mock_server_class.return_value = mock_server
|
|
|
|
main()
|
|
|
|
# Verify server created with None config
|
|
mock_server_class.assert_called_once_with(None)
|
|
|
|
# Verify no config logging
|
|
logger.info.assert_any_call(
|
|
"No MQTT configuration provided - use tools to configure at runtime"
|
|
)
|
|
|
|
def test_main_startup_logging(self):
|
|
"""Test main startup information logging."""
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.setup_logging'), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_args', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_env', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.MCMQTTServer'), \
|
|
patch('mcmqtt.mcmqtt.get_version', return_value="2.0.0"), \
|
|
patch('asyncio.run'), \
|
|
patch('structlog.get_logger') as mock_logger:
|
|
|
|
# Mock arguments
|
|
args = Mock()
|
|
args.version = False
|
|
args.log_level = "INFO"
|
|
args.log_file = None
|
|
args.mqtt_host = None
|
|
args.transport = "http"
|
|
args.auto_connect = True
|
|
mock_parse.return_value = args
|
|
|
|
logger = Mock()
|
|
mock_logger.return_value = logger
|
|
|
|
main()
|
|
|
|
# Verify startup logging
|
|
logger.info.assert_any_call(
|
|
"Starting mcmqtt FastMCP server",
|
|
version="2.0.0",
|
|
transport="http",
|
|
auto_connect=True
|
|
)
|
|
|
|
def test_main_keyboard_interrupt(self):
|
|
"""Test main handling KeyboardInterrupt."""
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.setup_logging'), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_args', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_env', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.MCMQTTServer'), \
|
|
patch('asyncio.run', side_effect=KeyboardInterrupt()), \
|
|
patch('sys.exit') as mock_exit, \
|
|
patch('structlog.get_logger') as mock_logger:
|
|
|
|
# Mock arguments
|
|
args = Mock()
|
|
args.version = False
|
|
args.log_level = "WARNING"
|
|
args.log_file = None
|
|
args.mqtt_host = None
|
|
args.transport = "stdio"
|
|
args.auto_connect = False
|
|
mock_parse.return_value = args
|
|
|
|
logger = Mock()
|
|
mock_logger.return_value = logger
|
|
|
|
main()
|
|
|
|
# Verify graceful shutdown
|
|
logger.info.assert_called_with("Server stopped by user")
|
|
mock_exit.assert_called_once_with(0)
|
|
|
|
def test_main_exception(self):
|
|
"""Test main handling general exception."""
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.setup_logging'), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_args', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_env', return_value=None), \
|
|
patch('mcmqtt.mcmqtt.MCMQTTServer'), \
|
|
patch('asyncio.run', side_effect=Exception("Startup failed")), \
|
|
patch('sys.exit') as mock_exit, \
|
|
patch('structlog.get_logger') as mock_logger:
|
|
|
|
# Mock arguments
|
|
args = Mock()
|
|
args.version = False
|
|
args.log_level = "WARNING"
|
|
args.log_file = None
|
|
args.mqtt_host = None
|
|
args.transport = "stdio"
|
|
args.auto_connect = False
|
|
mock_parse.return_value = args
|
|
|
|
logger = Mock()
|
|
mock_logger.return_value = logger
|
|
|
|
main()
|
|
|
|
# Verify error handling
|
|
logger.error.assert_called_with("Failed to start server", error="Startup failed")
|
|
mock_exit.assert_called_once_with(1)
|
|
|
|
def test_main_complex_scenario(self):
|
|
"""Test main with complex real-world scenario."""
|
|
mock_config = Mock()
|
|
mock_config.broker_host = 'production.mqtt.com'
|
|
mock_config.broker_port = 8883
|
|
|
|
with patch('mcmqtt.mcmqtt.parse_arguments') as mock_parse, \
|
|
patch('mcmqtt.mcmqtt.setup_logging') as mock_setup_log, \
|
|
patch('mcmqtt.mcmqtt.create_mqtt_config_from_args', return_value=mock_config), \
|
|
patch('mcmqtt.mcmqtt.MCMQTTServer') as mock_server_class, \
|
|
patch('mcmqtt.mcmqtt.get_version', return_value="1.5.0"), \
|
|
patch('asyncio.run') as mock_asyncio_run, \
|
|
patch('structlog.get_logger') as mock_logger:
|
|
|
|
# Mock complex production-like arguments
|
|
args = Mock()
|
|
args.version = False
|
|
args.log_level = "INFO"
|
|
args.log_file = "/var/log/mcmqtt.log"
|
|
args.mqtt_host = 'production.mqtt.com'
|
|
args.mqtt_port = 8883
|
|
args.transport = "http"
|
|
args.host = "0.0.0.0"
|
|
args.port = 3000
|
|
args.auto_connect = True
|
|
mock_parse.return_value = args
|
|
|
|
logger = Mock()
|
|
mock_logger.return_value = logger
|
|
mock_server = Mock()
|
|
mock_server_class.return_value = mock_server
|
|
|
|
main()
|
|
|
|
# Verify all components called correctly
|
|
mock_setup_log.assert_called_once_with("INFO", "/var/log/mcmqtt.log")
|
|
mock_server_class.assert_called_once_with(mock_config)
|
|
mock_asyncio_run.assert_called_once()
|
|
|
|
# Verify comprehensive logging
|
|
logger.info.assert_any_call(
|
|
"MQTT configuration from command line",
|
|
broker="production.mqtt.com:8883"
|
|
)
|
|
logger.info.assert_any_call(
|
|
"Starting mcmqtt FastMCP server",
|
|
version="1.5.0",
|
|
transport="http",
|
|
auto_connect=True
|
|
) |