Files
fastmcp-feedback/tests/integration/test_demo_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

353 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
from sqlalchemy import text
# 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
from tests._compat import tool_map
@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 tool_map(server)
# 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(text("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(text("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
# Seed a temporary database, then point the server at it
db_url = create_temp_database()
database = FeedbackDatabase(db_url)
await setup_demo_data(database)
server = create_demo_server(database_url=db_url)
tools = await tool_map(server)
# Test system info tool
system_info_tool = tools["get_system_info"]
info_result = await system_info_tool.run({})
assert info_result.structured_content["status"] == "running"
assert "versions" 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 tool_map(app)
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 tool_map(server)
# 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 tool_map(server)
# 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 tool_map(server)
# 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 tool_map(app)
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 tool_map(server)
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"])