Files
fastmcp-feedback/tests/unit/test_feedback_servers.py
T
rsp2k ddf23d10e4
Tests / test (==2.12.2) (push) Canceled after 0s
Tests / test (==2.14.3) (push) Canceled after 0s
Tests / test (>=4,<5) (push) Canceled after 0s
Tests / canary (push) Canceled after 0s
Support FastMCP 4 and fix silent composition failure
compose_feedback_services() called import_server() without awaiting it.
That method became async in FastMCP 2.14, so on the version this package
was built against it composed nothing while logging success. FastMCP 4
removed it outright. Use mount(), which is sync on 2.12 through 4.x.

The test suite could not start (pytest-html hook, plugin not installed),
so nothing caught this. Add pytest-html, bring examples/ over from the docs
repo since the integration tests import them, and fix the tests that had
drifted from the code. 407 tests pass on FastMCP 2.12.2, 2.14.3 and 4.0.10.

- Declare fastmcp>=2.12.2,<5; CI matrix covers each family plus a
  non-blocking canary on the newest FastMCP
- Add a composition test with real servers; the mocked ones could not
  see a missing await
- Rewrite the README against the actual API and execute its examples
- Ignore ruff UP042: StrEnum would change stored enum formatting
- Version 2026.09.27
2026-09-27 06:09:30 -06:00

830 lines
32 KiB
Python

"""Comprehensive tests for feedback.py server factory functions.
This module tests all server creation functions with comprehensive error handling,
configuration validation, and edge case testing to boost coverage to 90%+.
"""
import pytest
import pytest_asyncio
from unittest.mock import Mock, AsyncMock, patch, MagicMock, PropertyMock
from fastmcp import FastMCP
from fastmcp_feedback.feedback import (
create_feedback_server,
create_submission_server,
create_management_server,
create_analytics_server,
compose_feedback_services,
create_distributed_feedback_system,
get_server_health,
monitor_feedback_servers,
get_server_templates,
validate_server_config,
_add_analytics_tools
)
from fastmcp_feedback.insights import FeedbackInsights
@pytest.mark.unit
class TestFeedbackServerFactories:
"""Test server creation factory functions."""
@pytest.mark.asyncio
async def test_create_feedback_server_success(self, memory_db_url, test_insights):
"""Test successful feedback server creation with all parameters."""
with patch('fastmcp_feedback.feedback.add_feedback_tools') as mock_add_tools:
server = create_feedback_server(
name="Test Feedback Server",
database_url=memory_db_url,
insights=test_insights,
description="Custom test description"
)
# Verify server creation
assert isinstance(server, FastMCP)
assert server.name == "Test Feedback Server"
assert server.description == "Custom test description"
# Verify add_feedback_tools was called with correct parameters
mock_add_tools.assert_called_once_with(
server,
database_url=memory_db_url,
insights=test_insights
)
@pytest.mark.asyncio
async def test_create_feedback_server_defaults(self):
"""Test feedback server creation with default parameters."""
with patch('fastmcp_feedback.feedback.add_feedback_tools') as mock_add_tools:
server = create_feedback_server()
# Verify defaults are applied
assert server.name == "FastMCP Feedback Server"
assert server.description == "Comprehensive feedback collection and management system"
# Verify insights were created as default
mock_add_tools.assert_called_once()
call_args = mock_add_tools.call_args
assert call_args[0][0] == server # First arg is server
assert call_args[1]['database_url'] is None
assert isinstance(call_args[1]['insights'], FeedbackInsights)
@pytest.mark.asyncio
async def test_create_feedback_server_error_handling(self):
"""Test error handling in feedback server creation."""
with patch('fastmcp_feedback.feedback.add_feedback_tools', side_effect=Exception("Database connection failed")):
with pytest.raises(Exception, match="Database connection failed"):
create_feedback_server("Test Server")
@pytest.mark.asyncio
async def test_create_submission_server_success(self, memory_db_url):
"""Test successful submission server creation."""
with patch('fastmcp_feedback.tools.add_submission_tools') as mock_add_submission:
server = create_submission_server(
name="Submission Server",
database_url=memory_db_url,
insights=None
)
assert isinstance(server, FastMCP)
assert server.name == "Submission Server"
assert server.description == "Feedback submission service"
# Verify add_submission_tools was called
mock_add_submission.assert_called_once()
call_args = mock_add_submission.call_args
assert call_args[0][0] == server
assert call_args[0][1] == memory_db_url
assert isinstance(call_args[0][2], FeedbackInsights)
@pytest.mark.asyncio
async def test_create_submission_server_default_name(self):
"""Test submission server creation with default name."""
with patch('fastmcp_feedback.tools.add_submission_tools'):
server = create_submission_server()
assert server.name == "Feedback Submission Server"
@pytest.mark.asyncio
async def test_create_submission_server_error_handling(self):
"""Test error handling in submission server creation."""
with patch('fastmcp_feedback.tools.add_submission_tools', side_effect=RuntimeError("Tool registration failed")):
with pytest.raises(RuntimeError, match="Tool registration failed"):
create_submission_server("Test Server")
@pytest.mark.asyncio
async def test_create_management_server_success(self, memory_db_url):
"""Test successful management server creation."""
with patch('fastmcp_feedback.tools.add_retrieval_tools') as mock_retrieval, \
patch('fastmcp_feedback.tools.add_management_tools') as mock_management:
server = create_management_server(
name="Management Server",
database_url=memory_db_url
)
assert isinstance(server, FastMCP)
assert server.name == "Management Server"
assert server.description == "Feedback management and analytics service"
# Verify both tool sets were added
mock_retrieval.assert_called_once()
mock_management.assert_called_once()
@pytest.mark.asyncio
async def test_create_management_server_defaults(self):
"""Test management server creation with defaults."""
with patch('fastmcp_feedback.tools.add_retrieval_tools'), \
patch('fastmcp_feedback.tools.add_management_tools'):
server = create_management_server()
assert server.name == "Feedback Management Server"
@pytest.mark.asyncio
async def test_create_management_server_error_handling(self):
"""Test error handling in management server creation."""
with patch('fastmcp_feedback.tools.add_retrieval_tools', side_effect=ValueError("Invalid configuration")):
with pytest.raises(ValueError, match="Invalid configuration"):
create_management_server("Test Server")
@pytest.mark.asyncio
async def test_create_analytics_server_success(self, memory_db_url):
"""Test successful analytics server creation."""
with patch('fastmcp_feedback.tools.add_retrieval_tools') as mock_retrieval, \
patch('fastmcp_feedback.feedback._add_analytics_tools') as mock_analytics:
server = create_analytics_server(
name="Analytics Server",
database_url=memory_db_url,
enable_insights=True,
retention_days=60
)
assert isinstance(server, FastMCP)
assert server.name == "Analytics Server"
assert server.description == "Feedback analytics and reporting service"
# Verify insights configuration
mock_retrieval.assert_called_once()
mock_analytics.assert_called_once()
# Check insights configuration from call
retrieval_call_args = mock_retrieval.call_args
insights = retrieval_call_args[0][2] # Third argument is insights
assert insights.enabled is True
assert insights.retention_days == 60
@pytest.mark.asyncio
async def test_create_analytics_server_defaults(self):
"""Test analytics server creation with default parameters."""
with patch('fastmcp_feedback.tools.add_retrieval_tools'), \
patch('fastmcp_feedback.feedback._add_analytics_tools'):
server = create_analytics_server()
assert server.name == "Feedback Analytics Server"
@pytest.mark.asyncio
async def test_create_analytics_server_error_handling(self):
"""Test error handling in analytics server creation."""
with patch('fastmcp_feedback.tools.add_retrieval_tools'), \
patch('fastmcp_feedback.feedback._add_analytics_tools', side_effect=ConnectionError("Analytics service unavailable")):
with pytest.raises(ConnectionError, match="Analytics service unavailable"):
create_analytics_server("Test Server")
@pytest.mark.unit
class TestAnalyticsTools:
"""Test analytics-specific tool creation."""
@pytest.mark.asyncio
async def test_add_analytics_tools_success(self, memory_db_url):
"""Test successful analytics tools addition."""
server = Mock()
server.add_tool = Mock()
insights = Mock()
insights.generate_report.return_value = {"total_feedback": 10}
insights.export_data.return_value = {"data": "test"}
# Test the internal function
_add_analytics_tools(server, memory_db_url, insights)
# Verify add_tool was called twice (for both analytics tools)
assert server.add_tool.call_count == 2
@pytest.mark.asyncio
async def test_analytics_report_tool_success(self):
"""Test analytics report tool with date parameters."""
# Test the analytics report function directly
insights = Mock()
insights.generate_report.return_value = {"total_feedback": 5, "period": "test"}
# Create a mock server to capture the tool functions
captured_tools = []
def mock_add_tool(func):
captured_tools.append(func)
server = Mock()
server.add_tool = mock_add_tool
_add_analytics_tools(server, None, insights)
# Find the analytics report tool
report_tool = None
for tool in captured_tools:
if hasattr(tool, '__name__') and 'analytics_report' in tool.__name__:
report_tool = tool
break
assert report_tool is not None
# Test the tool function directly
result = await report_tool("2024-01-01", "2024-01-31")
insights.generate_report.assert_called_once()
assert result == {"total_feedback": 5, "period": "test"}
@pytest.mark.asyncio
async def test_analytics_report_tool_error_handling(self):
"""Test analytics report tool error handling."""
insights = Mock()
insights.generate_report.side_effect = ValueError("Invalid date format")
# Create a mock server to capture the tool functions
captured_tools = []
def mock_add_tool(func):
captured_tools.append(func)
server = Mock()
server.add_tool = mock_add_tool
_add_analytics_tools(server, None, insights)
# Find the analytics report tool
report_tool = None
for tool in captured_tools:
if hasattr(tool, '__name__') and 'analytics_report' in tool.__name__:
report_tool = tool
break
assert report_tool is not None
# Test error handling
result = await report_tool("invalid-date", "2024-01-31")
assert "error" in result
assert "Invalid" in result["error"] # More flexible assertion to handle different error messages
@pytest.mark.asyncio
async def test_export_analytics_tool_success(self):
"""Test export analytics data tool."""
insights = Mock()
insights.export_data.return_value = {"exported_data": "success"}
# Create a mock server to capture the tool functions
captured_tools = []
def mock_add_tool(func):
captured_tools.append(func)
server = Mock()
server.add_tool = mock_add_tool
_add_analytics_tools(server, None, insights)
# Find the export tool
export_tool = None
for tool in captured_tools:
if hasattr(tool, '__name__') and 'export_analytics_data' in tool.__name__:
export_tool = tool
break
assert export_tool is not None
# Test the tool function
result = await export_tool(include_metrics=True)
insights.export_data.assert_called_once_with(True)
assert result == {"exported_data": "success"}
@pytest.mark.asyncio
async def test_export_analytics_tool_error_handling(self):
"""Test export analytics tool error handling."""
insights = Mock()
insights.export_data.side_effect = PermissionError("Access denied")
# Create a mock server to capture the tool functions
captured_tools = []
def mock_add_tool(func):
captured_tools.append(func)
server = Mock()
server.add_tool = mock_add_tool
_add_analytics_tools(server, None, insights)
# Find the export tool
export_tool = None
for tool in captured_tools:
if hasattr(tool, '__name__') and 'export_analytics_data' in tool.__name__:
export_tool = tool
break
assert export_tool is not None
# Test error handling
result = await export_tool(include_metrics=False)
assert "error" in result
assert "Access denied" in result["error"]
@pytest.mark.unit
class TestServerComposition:
"""Test server composition and distributed systems."""
@pytest.mark.asyncio
async def test_compose_feedback_services_success(self):
"""Test successful service composition."""
main_server = Mock()
main_server.mount = Mock()
services = {
"service1": {
"database_url": "sqlite:///service1.db",
"type": "submission"
},
"service2": {
"database_url": "sqlite:///service2.db",
"type": "management"
},
"service3": {
"database_url": "sqlite:///service3.db",
"type": "full"
}
}
with patch('fastmcp_feedback.feedback.create_submission_server') as mock_submission, \
patch('fastmcp_feedback.feedback.create_management_server') as mock_management, \
patch('fastmcp_feedback.feedback.create_feedback_server') as mock_feedback:
mock_submission.return_value = Mock(name="SubmissionServer")
mock_management.return_value = Mock(name="ManagementServer")
mock_feedback.return_value = Mock(name="FeedbackServer")
compose_feedback_services(main_server, services)
# Verify all servers were created and imported
mock_submission.assert_called_once()
mock_management.assert_called_once()
mock_feedback.assert_called_once()
assert main_server.mount.call_count == 3
@pytest.mark.asyncio
async def test_compose_feedback_services_real_servers(self, tmp_path):
"""Composed tools must actually appear under their prefix.
The mocked tests above can't catch a missing await: a sync Mock
records the call either way. This one uses real FastMCP servers.
"""
from fastmcp import FastMCP
from tests._compat import tool_map
main_server = FastMCP("Main")
compose_feedback_services(
main_server,
{
"public": {"database_url": f"sqlite:///{tmp_path}/p.db", "type": "submission"},
"admin": {"database_url": f"sqlite:///{tmp_path}/a.db", "type": "full"},
},
)
names = set(await tool_map(main_server))
assert "public_submit_feedback" in names
assert "admin_list_feedback" in names
assert "admin_update_feedback_status" in names
@pytest.mark.asyncio
async def test_compose_feedback_services_with_shared_insights(self):
"""Test service composition with shared insights."""
main_server = Mock()
main_server.mount = Mock()
shared_insights = Mock()
services = {
"test_service": {
"database_url": "sqlite:///test.db",
"type": "submission"
}
}
with patch('fastmcp_feedback.feedback.create_submission_server') as mock_submission:
mock_submission.return_value = Mock()
compose_feedback_services(main_server, services, shared_insights)
# Verify shared insights was passed
mock_submission.assert_called_once()
call_args = mock_submission.call_args[0]
assert call_args[2] == shared_insights # Third argument is insights
@pytest.mark.asyncio
async def test_compose_feedback_services_error_handling(self):
"""Test error handling in service composition."""
main_server = Mock()
services = {"bad_service": {"database_url": "invalid://url"}}
with patch('fastmcp_feedback.feedback.create_feedback_server', side_effect=RuntimeError("Server creation failed")):
with pytest.raises(RuntimeError, match="Server creation failed"):
compose_feedback_services(main_server, services)
@pytest.mark.asyncio
async def test_create_distributed_feedback_system_success(self):
"""Test successful distributed system creation."""
services = {
"submission": {
"database_url": "sqlite:///submission.db",
"type": "submission"
},
"analytics": {
"database_url": "sqlite:///analytics.db",
"type": "analytics",
"enable_insights": False,
"retention_days": 30
},
"management": {
"database_url": "sqlite:///management.db",
"type": "management"
},
"full": {
"database_url": "sqlite:///full.db",
"type": "full"
}
}
with patch('fastmcp_feedback.feedback.create_submission_server') as mock_submission, \
patch('fastmcp_feedback.feedback.create_analytics_server') as mock_analytics, \
patch('fastmcp_feedback.feedback.create_management_server') as mock_management, \
patch('fastmcp_feedback.feedback.create_feedback_server') as mock_feedback:
mock_submission.return_value = Mock(name="SubmissionServer")
mock_analytics.return_value = Mock(name="AnalyticsServer")
mock_management.return_value = Mock(name="ManagementServer")
mock_feedback.return_value = Mock(name="FeedbackServer")
result = create_distributed_feedback_system(services)
# Verify all servers were created
assert len(result) == 4
assert "submission" in result
assert "analytics" in result
assert "management" in result
assert "full" in result
# Verify analytics server was called with custom config
mock_analytics.assert_called_once_with(
"analytics Analytics Service",
"sqlite:///analytics.db",
False, # enable_insights
30 # retention_days
)
@pytest.mark.asyncio
async def test_create_distributed_feedback_system_with_shared_insights(self):
"""Test distributed system creation with shared insights."""
services = {"test": {"database_url": "sqlite:///test.db"}}
shared_insights = Mock()
with patch('fastmcp_feedback.feedback.create_feedback_server') as mock_feedback:
mock_feedback.return_value = Mock()
result = create_distributed_feedback_system(services, shared_insights)
# Verify shared insights was used
mock_feedback.assert_called_once()
call_args = mock_feedback.call_args[0]
assert call_args[2] == shared_insights
@pytest.mark.asyncio
async def test_create_distributed_feedback_system_error_handling(self):
"""Test error handling in distributed system creation."""
services = {"failing_service": {"database_url": "invalid://url"}}
with patch('fastmcp_feedback.feedback.create_feedback_server', side_effect=ConnectionError("Connection failed")):
with pytest.raises(ConnectionError, match="Connection failed"):
create_distributed_feedback_system(services)
@pytest.mark.unit
class TestServerHealthMonitoring:
"""Test server health monitoring functions."""
def test_get_server_health_success(self):
"""Test successful server health check."""
server = Mock()
server.name = "Test Server"
result = get_server_health(server)
assert result["server_name"] == "Test Server"
assert result["status"] == "healthy"
assert result["message"] == "Feedback server is healthy"
def test_get_server_health_error_handling(self):
"""Test server health check with errors."""
# Create a server that will trigger an exception in the try block
# but allow getattr to work in the except block
class ProblematicServer:
def __init__(self):
self._name = "Crashed Server"
self._access_count = 0
@property
def name(self):
self._access_count += 1
if self._access_count == 1: # First access in try block
raise RuntimeError("Server crashed")
return self._name # Second access via getattr works
server = ProblematicServer()
result = get_server_health(server)
# The function should catch the exception and return error status
assert result["status"] == "unhealthy"
assert "error" in result
assert "Server crashed" in result["error"]
assert result["server_name"] == "Crashed Server"
def test_get_server_health_unknown_server(self):
"""Test health check for server without name."""
server = Mock(spec=[]) # Empty spec means no attributes
result = get_server_health(server)
# Should handle missing name gracefully
assert result["server_name"] == "unknown"
assert result["status"] == "unhealthy"
def test_monitor_feedback_servers_all_healthy(self):
"""Test monitoring multiple healthy servers."""
servers = {
"server1": Mock(name="Server1"),
"server2": Mock(name="Server2")
}
with patch('fastmcp_feedback.feedback.get_server_health') as mock_health:
mock_health.side_effect = [
{"server_name": "server1", "status": "healthy", "total_tools": 5},
{"server_name": "server2", "status": "healthy", "total_tools": 3}
]
result = monitor_feedback_servers(servers)
assert result["overall_status"] == "healthy"
assert result["summary"]["total_servers"] == 2
assert result["summary"]["healthy_servers"] == 2
assert result["summary"]["unhealthy_servers"] == 0
assert result["summary"]["total_tools"] == 8
def test_monitor_feedback_servers_mixed_health(self):
"""Test monitoring servers with mixed health status."""
servers = {
"healthy_server": Mock(),
"unhealthy_server": Mock()
}
with patch('fastmcp_feedback.feedback.get_server_health') as mock_health:
mock_health.side_effect = [
{"server_name": "healthy_server", "status": "healthy", "total_tools": 4},
{"server_name": "unhealthy_server", "status": "unhealthy", "error": "Connection lost"}
]
result = monitor_feedback_servers(servers)
assert result["overall_status"] == "degraded"
assert result["summary"]["total_servers"] == 2
assert result["summary"]["healthy_servers"] == 1
assert result["summary"]["unhealthy_servers"] == 1
assert result["summary"]["total_tools"] == 4
def test_monitor_feedback_servers_empty(self):
"""Test monitoring with no servers."""
result = monitor_feedback_servers({})
assert result["overall_status"] == "healthy"
assert result["summary"]["total_servers"] == 0
assert result["summary"]["healthy_servers"] == 0
assert result["summary"]["unhealthy_servers"] == 0
assert result["summary"]["total_tools"] == 0
@pytest.mark.unit
class TestConfigurationHelpers:
"""Test configuration and template helper functions."""
def test_get_server_templates(self):
"""Test server template definitions."""
templates = get_server_templates()
# Verify all expected templates exist
assert "basic" in templates
assert "submission_only" in templates
assert "management" in templates
assert "analytics" in templates
# Verify template structure
basic_template = templates["basic"]
assert "description" in basic_template
assert "factory" in basic_template
assert "features" in basic_template
assert basic_template["factory"] == create_feedback_server
# Verify analytics template
analytics_template = templates["analytics"]
assert analytics_template["factory"] == create_analytics_server
assert "analytics" in analytics_template["features"]
def test_validate_server_config_valid(self):
"""Test validation of valid server configuration."""
config = {
"name": "Test Server",
"database_url": "sqlite:///test.db",
"insights": {
"enabled": True,
"retention_days": 30
}
}
result = validate_server_config(config)
assert result["valid"] is True
assert len(result["errors"]) == 0
assert len(result["warnings"]) == 0
def test_validate_server_config_missing_name(self):
"""Test validation with missing server name."""
config = {}
result = validate_server_config(config)
assert result["valid"] is False
assert "Server name is required" in result["errors"]
def test_validate_server_config_memory_database_warning(self):
"""Test validation with in-memory database warning."""
config = {
"name": "Test Server",
"database_url": "sqlite:///:memory:"
}
result = validate_server_config(config)
assert result["valid"] is True
assert any("in-memory database" in warning.lower() for warning in result["warnings"])
def test_validate_server_config_no_database_url(self):
"""Test validation without database URL."""
config = {
"name": "Test Server"
}
result = validate_server_config(config)
assert result["valid"] is True
assert any("no database url specified" in warning.lower() for warning in result["warnings"])
def test_validate_server_config_invalid_insights(self):
"""Test validation with invalid insights configuration."""
config = {
"name": "Test Server",
"insights": {
"enabled": True,
"retention_days": 0 # Invalid: must be at least 1
}
}
result = validate_server_config(config)
assert result["valid"] is False
assert any("retention days must be at least 1" in error.lower() for error in result["errors"])
def test_validate_server_config_negative_retention_days(self):
"""Test validation with negative retention days."""
config = {
"name": "Test Server",
"insights": {
"enabled": True,
"retention_days": -5
}
}
result = validate_server_config(config)
assert result["valid"] is False
assert any("retention days must be at least 1" in error.lower() for error in result["errors"])
def test_validate_server_config_disabled_insights(self):
"""Test validation with disabled insights (should be valid)."""
config = {
"name": "Test Server",
"insights": {
"enabled": False,
"retention_days": 0 # Should be ignored when disabled
}
}
result = validate_server_config(config)
assert result["valid"] is True
@pytest.mark.unit
@pytest.mark.integration
class TestEndToEndServerCreation:
"""Integration tests for complete server creation workflows."""
@pytest.mark.asyncio
async def test_full_server_creation_workflow(self, memory_db_url):
"""Test complete server creation and validation workflow."""
# Test configuration validation
config = {
"name": "Integration Test Server",
"database_url": memory_db_url,
"insights": {
"enabled": True,
"retention_days": 7
}
}
validation_result = validate_server_config(config)
assert validation_result["valid"]
# Create server using validated config
with patch('fastmcp_feedback.feedback.add_feedback_tools'):
server = create_feedback_server(
name=config["name"],
database_url=config["database_url"]
)
# Verify server health
health = get_server_health(server)
assert health["status"] == "healthy"
@pytest.mark.asyncio
async def test_distributed_system_with_health_monitoring(self):
"""Test distributed system creation with health monitoring."""
services_config = {
"frontend": {"database_url": "sqlite:///frontend.db", "type": "submission"},
"backend": {"database_url": "sqlite:///backend.db", "type": "management"},
"analytics": {"database_url": "sqlite:///analytics.db", "type": "analytics"}
}
with patch('fastmcp_feedback.feedback.create_submission_server') as mock_submission, \
patch('fastmcp_feedback.feedback.create_management_server') as mock_management, \
patch('fastmcp_feedback.feedback.create_analytics_server') as mock_analytics:
mock_submission.return_value = Mock(name="FrontendServer")
mock_management.return_value = Mock(name="BackendServer")
mock_analytics.return_value = Mock(name="AnalyticsServer")
# Create distributed system
servers = create_distributed_feedback_system(services_config)
# Monitor health of all servers
health_status = monitor_feedback_servers(servers)
assert len(servers) == 3
assert health_status["summary"]["total_servers"] == 3
assert health_status["overall_status"] == "healthy"
def test_template_based_server_creation(self):
"""Test server creation using templates."""
templates = get_server_templates()
# Test creating servers from each template
for template_name, template_config in templates.items():
factory_function = template_config["factory"]
# Create appropriate patches based on the factory function
patches = ['fastmcp_feedback.feedback.add_feedback_tools']
if factory_function in [create_submission_server, create_management_server, create_analytics_server]:
patches.extend([
'fastmcp_feedback.tools.add_submission_tools',
'fastmcp_feedback.tools.add_retrieval_tools',
'fastmcp_feedback.tools.add_management_tools',
'fastmcp_feedback.feedback._add_analytics_tools'
])
# Apply all patches
with patch('fastmcp_feedback.feedback.add_feedback_tools'), \
patch('fastmcp_feedback.tools.add_submission_tools'), \
patch('fastmcp_feedback.tools.add_retrieval_tools'), \
patch('fastmcp_feedback.tools.add_management_tools'), \
patch('fastmcp_feedback.feedback._add_analytics_tools'):
# Create server using template factory
server = factory_function(f"Template {template_name.title()} Server")
# Verify server was created successfully
assert isinstance(server, FastMCP)
assert template_name.title() in server.name or "Server" in server.name