FastMCP Todo Server
by DanEdens
- fastmcp-todo-server
- tests
import asyncio
import json
import os
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
import sys
from starlette.testclient import TestClient
import logging
# Add src to path so we can import server
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from fastmcp_todo_server.server import Omnispindle
class TestOmnispindle:
"""Test cases for the Omnispindle class"""
@pytest.fixture
def server(self):
"""Creates a test server instance"""
return Omnispindle(name="test-server", server_type="sse")
@pytest.fixture
def mock_publish_status(self):
"""Creates a mock publish_mqtt_status function"""
async def _mock_publish(topic, message, retain=False):
return True
return _mock_publish
@pytest.mark.asyncio
async def test_run_server_handles_none_return(self, server, mock_publish_status):
"""Test that run_server properly handles when run_sse_async returns None"""
# Mock the run_sse_async method to return None
with patch.object(server, 'run_sse_async', new_callable=AsyncMock) as mock_run_sse:
mock_run_sse.return_value = None
# Call run_server
app = await server.run_server(mock_publish_status)
# Verify run_sse_async was called
mock_run_sse.assert_called_once()
# Verify app is not None
assert app is not None
# Test that the dummy app is callable with ASGI signature
async def receive():
return {"type": "http.request"}
async def send(message):
# Verify message structure for response
if message["type"] == "http.response.start":
assert message["status"] == 503
assert any(h[0] == b"content-type" for h in message["headers"])
assert any(h[0] == b"x-fallback-app" for h in message["headers"])
# Simulate HTTP request to dummy app
scope = {"type": "http", "path": "/test"}
await app(scope, receive, send)
@pytest.mark.asyncio
async def test_dummy_app_lifespan_protocol(self, server, mock_publish_status):
"""Test that the dummy app properly handles lifespan protocol messages"""
# Mock the run_sse_async method to return None
with patch.object(server, 'run_sse_async', new_callable=AsyncMock) as mock_run_sse:
mock_run_sse.return_value = None
# Call run_server to get the dummy app
app = await server.run_server(mock_publish_status)
# Prepare mock functions for ASGI interface
messages_received = []
async def receive():
# First return startup, then shutdown
if not messages_received:
messages_received.append("startup")
return {"type": "lifespan.startup"}
else:
return {"type": "lifespan.shutdown"}
messages_sent = []
async def send(message):
messages_sent.append(message)
# Test lifespan protocol
scope = {"type": "lifespan"}
await app(scope, receive, send)
# Verify the correct responses were sent
assert len(messages_sent) == 2
assert messages_sent[0]["type"] == "lifespan.startup.complete"
assert messages_sent[1]["type"] == "lifespan.shutdown.complete"
@pytest.mark.asyncio
async def test_dummy_app_websocket_protocol(self, server, mock_publish_status):
"""Test that the dummy app properly handles websocket protocol messages"""
# Mock the run_sse_async method to return None
with patch.object(server, 'run_sse_async', new_callable=AsyncMock) as mock_run_sse:
mock_run_sse.return_value = None
# Call run_server to get the dummy app
app = await server.run_server(mock_publish_status)
# Prepare mock functions for ASGI interface
async def receive():
return {"type": "websocket.connect"}
messages_sent = []
async def send(message):
messages_sent.append(message)
# Test websocket protocol
scope = {"type": "websocket", "path": "/ws"}
await app(scope, receive, send)
# Verify the close message was sent with correct code
assert len(messages_sent) == 1
assert messages_sent[0]["type"] == "websocket.close"
assert messages_sent[0]["code"] == 1013 # Try again later
@pytest.mark.asyncio
async def test_run_server_returns_app_when_run_sse_async_succeeds(self, server, mock_publish_status):
"""Test that run_server returns the app from run_sse_async when it's not None"""
# Create a mock ASGI app
async def mock_asgi_app(scope, receive, send):
await send({"type": "http.response.start", "status": 200})
await send({"type": "http.response.body", "body": b"success"})
# Mock the run_sse_async method to return the mock app
with patch.object(server, 'run_sse_async', new_callable=AsyncMock) as mock_run_sse:
mock_run_sse.return_value = mock_asgi_app
# Call run_server
app = await server.run_server(mock_publish_status)
# Verify run_sse_async was called
mock_run_sse.assert_called_once()
# Verify app is the same as the mock app
assert app is mock_asgi_app