Skip to content

Commit e33515e

Browse files
committed
test: add transport layer tests with ASGI mocking
- Add tests/test_transport.py to verify /mcp and /sse endpoints - Add httpx to requirements.txt (required for Starlette TestClient) - Implement ASGI side_effects in mocks to simulate real SDK network responses
1 parent eb2855a commit e33515e

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ requests
44
uvicorn
55
starlette
66
pytest
7+
httpx

tests/test_transport.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import os
2+
import sys
3+
import pytest
4+
from unittest.mock import AsyncMock, patch, MagicMock
5+
from starlette.testclient import TestClient
6+
from contextlib import asynccontextmanager
7+
8+
# --- Setup Environment ---
9+
os.environ["AGILEDAY_TENANT_ID"] = "test-tenant"
10+
os.environ["AGILEDAY_API_TOKEN"] = "test-token"
11+
12+
# Add src to path
13+
sys.path.append(os.path.join(os.path.dirname(__file__), '../src'))
14+
15+
from agileday_server import starlette_app
16+
17+
# --- Fixtures ---
18+
19+
@pytest.fixture
20+
def client():
21+
"""Returns a Starlette TestClient for making HTTP requests."""
22+
return TestClient(starlette_app)
23+
24+
@pytest.fixture
25+
def mock_server_run():
26+
"""
27+
Patches the internal MCP server.run method to prevent it from blocking.
28+
"""
29+
with patch("agileday_server.server.run", new_callable=AsyncMock) as mock_run:
30+
yield mock_run
31+
32+
@pytest.fixture
33+
def mock_sse_transport(mock_server_run):
34+
"""
35+
Mocks the internal SSE Transport methods to simulate ASGI responses.
36+
"""
37+
with patch("agileday_server.sse_transport") as mock:
38+
39+
# 1. Mock handle_post_message
40+
# The real SDK writes a 202 Accepted response to the 'send' channel.
41+
# We must replicate that side effect here.
42+
async def side_effect_post(scope, receive, send):
43+
await send({
44+
'type': 'http.response.start',
45+
'status': 202,
46+
'headers': [[b'content-type', b'text/plain']]
47+
})
48+
await send({
49+
'type': 'http.response.body',
50+
'body': b'Accepted'
51+
})
52+
53+
mock.handle_post_message = AsyncMock(side_effect=side_effect_post)
54+
55+
# 2. Mock connect_sse
56+
# The real SDK writes 200 OK headers for the Event Stream.
57+
# We use asynccontextmanager to replicate the 'async with' behavior.
58+
@asynccontextmanager
59+
async def side_effect_connect(scope, receive, send):
60+
# Simulate starting the stream
61+
await send({
62+
'type': 'http.response.start',
63+
'status': 200,
64+
'headers': [[b'content-type', b'text/event-stream']]
65+
})
66+
67+
# Yield mock streams (reader, writer) required by the server
68+
yield (AsyncMock(), AsyncMock())
69+
70+
mock.connect_sse.side_effect = side_effect_connect
71+
72+
yield mock
73+
74+
# --- Tests ---
75+
76+
def test_health_check(client):
77+
"""Ensure the health endpoint returns 200 OK."""
78+
response = client.get("/health")
79+
assert response.status_code == 200
80+
assert response.text == "OK"
81+
82+
def test_mcp_post_message_accepted(client, mock_sse_transport):
83+
"""
84+
Verifies that a POST to /mcp returns 202 Accepted.
85+
"""
86+
response = client.post("/mcp", json={"jsonrpc": "2.0", "method": "ping"})
87+
88+
# We expect the mock to have been called with the raw ASGI arguments
89+
assert mock_sse_transport.handle_post_message.call_count == 1
90+
91+
# Check the response
92+
assert response.status_code == 202
93+
assert response.text == "Accepted"
94+
95+
def test_mcp_get_connects_sse(client, mock_sse_transport):
96+
"""
97+
Verifies that GET /mcp attempts to establish an SSE stream.
98+
"""
99+
with client.stream("GET", "/mcp") as response:
100+
# Check we got the 200 OK headers simulated by our mock
101+
assert response.status_code == 200
102+
103+
# Verify the context manager was entered
104+
assert mock_sse_transport.connect_sse.called
105+
106+
def test_legacy_sse_endpoint_redirects(client, mock_sse_transport):
107+
"""
108+
Verifies that the deprecated /sse endpoint still works.
109+
"""
110+
with client.stream("GET", "/sse") as response:
111+
assert response.status_code == 200
112+
113+
assert mock_sse_transport.connect_sse.called
114+
115+
def test_method_not_allowed(client):
116+
"""Ensure we enforce allowed methods (PUT should fail)."""
117+
response = client.put("/mcp")
118+
assert response.status_code == 405

0 commit comments

Comments
 (0)