Files
fastmcp-feedback/tests/integration/test_demo_servers.py
T
rsp2k d81fcecb92 feat: Initial release of fastmcp-feedback v2026.01.12
Production-ready feedback collection system for FastMCP servers.

Features:
- One-line integration with add_feedback_tools()
- Modular mixin architecture (SubmissionMixin, RetrievalMixin, ManagementMixin)
- Multi-database support (SQLite, PostgreSQL, MySQL)
- Privacy-compliant analytics with FeedbackInsights
- Server composition with create_feedback_server()
- Full type hints and Pydantic validation
- 97%+ test coverage

Installation:
  uv add fastmcp-feedback

Documentation: https://fastmcp-feedback.l.supported.systems
2026-01-12 11:54:26 -07:00

352 lines
14 KiB
Python

"""
Comprehensive tests for all demo servers to ensure they work correctly
and demonstrate all features properly.
"""
import pytest
import asyncio
import tempfile
import os
import sys
from pathlib import Path
from unittest.mock import patch
# Add the examples directory to the path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "examples"))
from fastmcp import FastMCP
from fastmcp_feedback import FeedbackDatabase, FeedbackInsights
@pytest.mark.asyncio
class TestDemoServerIntegration:
"""Test all demo servers for complete functionality."""
async def test_demo_server_creation_and_tools(self):
"""Test that demo_server.py creates all expected tools."""
# Import here to avoid circular imports
from demo_server import create_demo_server
server = create_demo_server()
# Verify server creation
assert isinstance(server, FastMCP)
assert server.name == "FastMCP Feedback Demo"
assert "demonstration" in server.description.lower()
# Get all tools
tools = await server.get_tools()
# Verify all expected tools are present
expected_tools = {
"submit_feedback",
"list_feedback",
"get_feedback_statistics",
"update_feedback_status",
"delete_feedback",
"get_system_info",
"setup_sample_data"
}
tool_names = set(tools.keys())
assert expected_tools.issubset(tool_names), f"Missing tools: {expected_tools - tool_names}"
# Test tool descriptions
for tool_name, tool in tools.items():
assert hasattr(tool, 'description'), f"Tool {tool_name} missing description"
assert len(tool.description) > 10, f"Tool {tool_name} has insufficient description"
async def test_demo_server_sample_data_setup(self):
"""Test that demo server can setup sample data correctly."""
from demo_server import setup_demo_data, create_temp_database
# Create temporary database
db_url = create_temp_database()
database = FeedbackDatabase(db_url)
try:
# Setup demo data
await setup_demo_data(database)
# Verify data was created
from fastmcp_feedback.models import Feedback
async with database.session() as session:
# Count feedback items
result = session.execute("SELECT COUNT(*) FROM feedback")
count = result.scalar()
assert count >= 5, f"Expected at least 5 sample items, got {count}"
# Verify types are present
result = session.execute("SELECT DISTINCT type FROM feedback")
types = [row[0] for row in result.fetchall()]
expected_types = {"BUG", "FEATURE", "IMPROVEMENT", "QUESTION"}
assert expected_types.issubset(set(types)), f"Missing feedback types: {expected_types - set(types)}"
finally:
await database.close()
async def test_demo_server_tool_execution(self):
"""Test actual execution of demo server tools."""
from demo_server import create_demo_server, create_temp_database, setup_demo_data
# Create server with temporary database
server = create_demo_server()
# Override database URL for testing
db_url = create_temp_database()
database = FeedbackDatabase(db_url)
await setup_demo_data(database)
tools = await server.get_tools()
# Test system info tool
system_info_tool = tools["get_system_info"]
info_result = await system_info_tool.run({})
assert info_result.structured_content["success"] is True
assert "version" in info_result.structured_content
assert "dependencies" in info_result.structured_content
# Test statistics tool (should work with sample data)
stats_tool = tools["get_feedback_statistics"]
stats_result = await stats_tool.run({})
assert stats_result.structured_content["total_count"] >= 5
assert "by_type" in stats_result.structured_content
assert "by_status" in stats_result.structured_content
await database.close()
async def test_simple_integration_server(self):
"""Test that simple_integration.py works correctly."""
# Import simple integration
from simple_integration import FastMCP
from fastmcp_feedback import add_feedback_tools
# Create simple server
app = FastMCP("Test Simple Feedback App")
add_feedback_tools(app, database_url="sqlite:///:memory:")
# Verify tools were added
tools = await app.get_tools()
expected_tools = {
"submit_feedback",
"list_feedback",
"get_feedback_statistics",
"delete_feedback",
"update_feedback_status"
}
tool_names = set(tools.keys())
assert expected_tools == tool_names, f"Tool mismatch. Expected: {expected_tools}, Got: {tool_names}"
# Test basic tool functionality
submit_tool = tools["submit_feedback"]
test_feedback = {
"type": "bug",
"title": "Test simple integration bug",
"description": "Testing simple integration functionality",
"submitter": "simple_test@example.com"
}
result = await submit_tool.run({"request": test_feedback})
assert result.structured_content["success"] is True
assert "feedback_id" in result.structured_content
async def test_mcp_server_environment_configuration(self):
"""Test that mcp_server.py respects environment variables."""
# Mock environment variables
env_vars = {
"DATABASE_URL": "sqlite:///:memory:",
"INSIGHTS_ENABLED": "true",
"LOG_LEVEL": "DEBUG",
"RETENTION_DAYS": "30"
}
with patch.dict(os.environ, env_vars, clear=False):
# Import and test mcp_server
import mcp_server
# Verify environment variables are being read
assert os.getenv("DATABASE_URL") == "sqlite:///:memory:"
assert os.getenv("INSIGHTS_ENABLED") == "true"
assert os.getenv("LOG_LEVEL") == "DEBUG"
assert os.getenv("RETENTION_DAYS") == "30"
async def test_demo_server_error_handling(self):
"""Test that demo servers handle errors gracefully."""
from demo_server import create_demo_server
server = create_demo_server()
tools = await server.get_tools()
# Test submit_feedback with invalid data
submit_tool = tools["submit_feedback"]
# Test missing required fields
with pytest.raises(Exception): # Should raise validation error
await submit_tool.run({"request": {}})
# Test invalid feedback type
with pytest.raises(Exception): # Should raise validation error
await submit_tool.run({"request": {
"type": "invalid_type",
"title": "Test",
"description": "Test",
"submitter": "test@example.com"
}})
async def test_demo_server_comprehensive_workflow(self):
"""Test complete workflow: submit → list → update → delete."""
from demo_server import create_demo_server, create_temp_database
server = create_demo_server()
tools = await server.get_tools()
# Submit feedback
submit_tool = tools["submit_feedback"]
test_feedback = {
"type": "feature",
"title": "Comprehensive workflow test",
"description": "Testing the complete demo server workflow",
"submitter": "workflow_test@example.com",
"contact_info": "workflow@test.com"
}
submit_result = await submit_tool.run({"request": test_feedback})
assert submit_result.structured_content["success"] is True
feedback_id = submit_result.structured_content["feedback_id"]
# List feedback (should include our submission)
list_tool = tools["list_feedback"]
list_result = await list_tool.run({})
feedback_list = list_result.structured_content["feedback"]
# Find our submitted feedback
our_feedback = next((f for f in feedback_list if f["id"] == feedback_id), None)
assert our_feedback is not None, "Submitted feedback not found in list"
assert our_feedback["title"] == test_feedback["title"]
assert our_feedback["type"] == test_feedback["type"]
assert our_feedback["status"] == "OPEN"
# Update feedback status
update_tool = tools["update_feedback_status"]
update_result = await update_tool.run({
"feedback_id": feedback_id,
"new_status": "in_progress"
})
assert update_result.structured_content["success"] is True
# Verify status update
list_result = await list_tool.run({})
feedback_list = list_result.structured_content["feedback"]
updated_feedback = next((f for f in feedback_list if f["id"] == feedback_id), None)
assert updated_feedback["status"] == "IN_PROGRESS"
# Get statistics (should include our feedback)
stats_tool = tools["get_feedback_statistics"]
stats_result = await stats_tool.run({})
stats = stats_result.structured_content
assert stats["total_count"] >= 1
assert "FEATURE" in stats["by_type"]
assert stats["by_type"]["FEATURE"] >= 1
assert "IN_PROGRESS" in stats["by_status"]
# Delete feedback
delete_tool = tools["delete_feedback"]
delete_result = await delete_tool.run({"feedback_id": feedback_id})
assert delete_result.structured_content["success"] is True
# Verify deletion
list_result = await list_tool.run({})
feedback_list = list_result.structured_content["feedback"]
deleted_feedback = next((f for f in feedback_list if f["id"] == feedback_id), None)
assert deleted_feedback is None, "Feedback should be deleted"
@pytest.mark.asyncio
class TestDemoServerDocumentation:
"""Test that demo servers match their documentation."""
async def test_demo_server_matches_documentation(self):
"""Verify demo_server.py matches demo_server.md claims."""
from demo_server import create_demo_server
server = create_demo_server()
tools = await server.get_tools()
# According to demo_server.md, should have exactly 7 tools
assert len(tools) == 7, f"Expected 7 tools as documented, got {len(tools)}"
# Verify documented tool names match actual tools
documented_tools = {
"submit_feedback", "list_feedback", "get_feedback_statistics",
"update_feedback_status", "delete_feedback", "get_system_info",
"setup_sample_data"
}
actual_tools = set(tools.keys())
assert documented_tools == actual_tools, f"Tools mismatch. Documented: {documented_tools}, Actual: {actual_tools}"
async def test_simple_integration_matches_documentation(self):
"""Verify simple_integration.py matches simple_integration.md claims."""
from fastmcp import FastMCP
from fastmcp_feedback import add_feedback_tools
# According to documentation, should add exactly 5 tools
app = FastMCP("Test App")
add_feedback_tools(app, database_url="sqlite:///:memory:")
tools = await app.get_tools()
assert len(tools) == 5, f"Expected 5 tools as documented, got {len(tools)}"
# Verify documented tool names
documented_tools = {
"submit_feedback", "list_feedback", "get_feedback_statistics",
"delete_feedback", "update_feedback_status"
}
actual_tools = set(tools.keys())
assert documented_tools == actual_tools, f"Tools mismatch. Documented: {documented_tools}, Actual: {actual_tools}"
@pytest.mark.asyncio
class TestDemoServerPerformance:
"""Test demo server performance characteristics."""
async def test_demo_server_concurrent_operations(self):
"""Test demo server handles concurrent operations correctly."""
from demo_server import create_demo_server
server = create_demo_server()
tools = await server.get_tools()
submit_tool = tools["submit_feedback"]
# Submit multiple feedback items concurrently
async def submit_feedback(index):
test_feedback = {
"type": "bug",
"title": f"Concurrent test bug {index}",
"description": f"Testing concurrent submission {index}",
"submitter": f"concurrent_test_{index}@example.com"
}
result = await submit_tool.run({"request": test_feedback})
return result.structured_content["success"]
# Submit 10 feedback items concurrently
tasks = [submit_feedback(i) for i in range(10)]
results = await asyncio.gather(*tasks)
# All submissions should succeed
assert all(results), "Some concurrent submissions failed"
# Verify all submissions are in the database
list_tool = tools["list_feedback"]
list_result = await list_tool.run({})
feedback_list = list_result.structured_content["feedback"]
concurrent_submissions = [f for f in feedback_list if "Concurrent test bug" in f["title"]]
assert len(concurrent_submissions) == 10, f"Expected 10 concurrent submissions, found {len(concurrent_submissions)}"
if __name__ == "__main__":
pytest.main([__file__, "-v"])