项目文件夹

文件
wehub-resource-sync 60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:59 +08:00

227 行
8.1 KiB
Python

"""Tests for ping middleware."""
from unittest.mock import AsyncMock, MagicMock
import anyio
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.middleware.ping import PingMiddleware
class TestPingMiddlewareInit:
"""Test PingMiddleware initialization."""
def test_init_default(self):
"""Test default initialization."""
middleware = PingMiddleware()
assert middleware.interval_ms == 30000
assert middleware._active_sessions == set()
def test_init_custom(self):
"""Test custom interval initialization."""
middleware = PingMiddleware(interval_ms=5000)
assert middleware.interval_ms == 5000
def test_init_invalid_interval_zero(self):
"""Test that zero interval raises ValueError."""
with pytest.raises(ValueError, match="interval_ms must be positive"):
PingMiddleware(interval_ms=0)
def test_init_invalid_interval_negative(self):
"""Test that negative interval raises ValueError."""
with pytest.raises(ValueError, match="interval_ms must be positive"):
PingMiddleware(interval_ms=-1000)
class TestPingMiddlewareOnMessage:
"""Test on_message hook behavior."""
def _mock_session(self):
"""Build a mock session with a stable per-connection Connection.
SDK v2 constructs a ServerSession per request; PingMiddleware keys the
keepalive loop off the underlying Connection (and registers cleanup on
its exit stack), so tests supply a connection with an exit_stack.
"""
connection = MagicMock()
connection.exit_stack = MagicMock()
connection.exit_stack.push_async_callback = MagicMock()
session = MagicMock()
session._connection = connection
session.send_ping = AsyncMock()
return session, connection
async def test_starts_ping_task_on_first_message(self):
"""Test that a ping task is started on first message from a connection."""
middleware = PingMiddleware(interval_ms=1000)
mock_session, connection = self._mock_session()
mock_context = MagicMock()
mock_context.fastmcp_context.session = mock_session
mock_call_next = AsyncMock(return_value="result")
result = await middleware.on_message(mock_context, mock_call_next)
assert result == "result"
assert id(connection) in middleware._active_sessions
connection.exit_stack.push_async_callback.assert_called_once()
async def test_does_not_start_duplicate_task(self):
"""Test that duplicate messages from same connection don't spawn duplicates."""
middleware = PingMiddleware(interval_ms=1000)
mock_session, connection = self._mock_session()
mock_context = MagicMock()
mock_context.fastmcp_context.session = mock_session
mock_call_next = AsyncMock(return_value="result")
# Three messages from the same connection
await middleware.on_message(mock_context, mock_call_next)
await middleware.on_message(mock_context, mock_call_next)
await middleware.on_message(mock_context, mock_call_next)
# Cleanup registered only once
assert connection.exit_stack.push_async_callback.call_count == 1
assert len(middleware._active_sessions) == 1
async def test_starts_separate_task_per_session(self):
"""Test that different connections get separate ping tasks."""
middleware = PingMiddleware(interval_ms=1000)
mock_session1, connection1 = self._mock_session()
mock_session2, connection2 = self._mock_session()
mock_context1 = MagicMock()
mock_context1.fastmcp_context.session = mock_session1
mock_context2 = MagicMock()
mock_context2.fastmcp_context.session = mock_session2
mock_call_next = AsyncMock(return_value="result")
await middleware.on_message(mock_context1, mock_call_next)
await middleware.on_message(mock_context2, mock_call_next)
connection1.exit_stack.push_async_callback.assert_called_once()
connection2.exit_stack.push_async_callback.assert_called_once()
assert len(middleware._active_sessions) == 2
async def test_skips_when_fastmcp_context_is_none(self):
"""Test that middleware passes through when fastmcp_context is None."""
middleware = PingMiddleware(interval_ms=1000)
mock_context = MagicMock()
mock_context.fastmcp_context = None
mock_call_next = AsyncMock(return_value="result")
result = await middleware.on_message(mock_context, mock_call_next)
assert result == "result"
assert len(middleware._active_sessions) == 0
async def test_skips_when_request_context_is_none(self):
"""Test that middleware passes through when request_context is None."""
middleware = PingMiddleware(interval_ms=1000)
mock_context = MagicMock()
mock_context.fastmcp_context = MagicMock()
mock_context.fastmcp_context.request_context = None
mock_call_next = AsyncMock(return_value="result")
result = await middleware.on_message(mock_context, mock_call_next)
assert result == "result"
assert len(middleware._active_sessions) == 0
class TestPingLoop:
"""Test the ping loop behavior."""
async def test_ping_loop_sends_pings_at_interval(self):
"""Test that ping loop sends pings at configured interval."""
middleware = PingMiddleware(interval_ms=50)
mock_session = MagicMock()
mock_session.send_ping = AsyncMock()
session_id = id(mock_session)
middleware._active_sessions.add(session_id)
# Run ping loop for a short time then cancel
with anyio.move_on_after(0.35):
await middleware._ping_loop(mock_session, session_id)
# Should have sent at least 2 pings in 350ms with 50ms interval
assert mock_session.send_ping.call_count >= 2
async def test_ping_loop_cleans_up_on_cancellation(self):
"""Test that session is removed from active sessions on cancellation."""
middleware = PingMiddleware(interval_ms=50)
mock_session = MagicMock()
mock_session.send_ping = AsyncMock()
session_id = 12345
middleware._active_sessions.add(session_id)
# Run and cancel the ping loop
with anyio.move_on_after(0.1):
await middleware._ping_loop(mock_session, session_id)
# Session should be cleaned up after cancellation
assert session_id not in middleware._active_sessions
class TestPingMiddlewareIntegration:
"""Integration tests for PingMiddleware with real FastMCP server."""
async def test_ping_middleware_registers_session(self):
"""Test that PingMiddleware registers sessions on first request."""
mcp = FastMCP("PingTestServer")
middleware = PingMiddleware(interval_ms=50)
mcp.add_middleware(middleware)
@mcp.tool
def hello() -> str:
return "Hello!"
assert len(middleware._active_sessions) == 0
async with Client(mcp) as client:
result = await client.call_tool("hello")
assert result.content[0].text == "Hello!"
# Should have registered the session
assert len(middleware._active_sessions) == 1
# Make another request - should not add duplicate
await client.call_tool("hello")
assert len(middleware._active_sessions) == 1
async def test_ping_task_cancelled_on_disconnect(self):
"""Test that ping task is properly cancelled when client disconnects."""
mcp = FastMCP("PingTestServer")
middleware = PingMiddleware(interval_ms=50)
mcp.add_middleware(middleware)
@mcp.tool
def hello() -> str:
return "Hello!"
async with Client(mcp) as client:
await client.call_tool("hello")
# Should have one active session
assert len(middleware._active_sessions) == 1
# After disconnect, give a moment for cleanup
await anyio.sleep(0.01)
# Session should be cleaned up
assert len(middleware._active_sessions) == 0