Skip to content

Commit 3b172f4

Browse files
author
Jianke LIN
committed
test(auth): reuse streamable HTTP session for token refresh
1 parent 29e499d commit 3b172f4

1 file changed

Lines changed: 73 additions & 21 deletions

File tree

‎tests/server/auth/test_get_access_token_streamable_http.py‎

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import time
22

3+
import anyio
34
import httpx2
45
import pytest
56
from mcp_types import (
67
CallToolRequestParams,
78
CallToolResult,
89
ListToolsResult,
910
PaginatedRequestParams,
11+
ProgressNotificationParams,
1012
TextContent,
1113
Tool,
1214
)
@@ -28,7 +30,7 @@ class _EchoTokenVerifier:
2830
"""Accepts any bearer token and echoes it back as the verified AccessToken."""
2931

3032
async def verify_token(self, token: str) -> AccessToken | None:
31-
return AccessToken(token=token, client_id=token, scopes=[], expires_at=int(time.time()) + 3600)
33+
return AccessToken(token=token, client_id="test-client", scopes=[], expires_at=int(time.time()) + 3600)
3234

3335

3436
async def _handle_whoami(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
@@ -51,23 +53,10 @@ def auth_flow(self, request: httpx2.Request):
5153
yield request
5254

5355

54-
async def _call_whoami(asgi_app: Starlette, host: str, token: str | None) -> str:
55-
auth = _MutableBearerAuth(token)
56-
async with (
57-
httpx2.ASGITransport(asgi_app) as transport,
58-
httpx2.AsyncClient(
59-
transport=transport,
60-
base_url=f"http://{host}",
61-
auth=auth,
62-
timeout=httpx2.Timeout(30, read=30),
63-
follow_redirects=True,
64-
) as http_client,
65-
):
66-
transport_ctx = streamable_http_client(f"http://{host}/mcp", http_client=http_client)
67-
async with Client(transport_ctx) as client: # pragma: no branch
68-
result = await client.call_tool("whoami", {})
69-
assert isinstance(result.content[0], TextContent)
70-
return result.content[0].text
56+
async def _call_whoami(client: Client) -> str:
57+
result = await client.call_tool("whoami", {})
58+
assert isinstance(result.content[0], TextContent)
59+
return result.content[0].text
7160

7261

7362
@pytest.mark.anyio
@@ -92,6 +81,69 @@ async def test_get_access_token_reflects_current_request_in_stateful_session() -
9281
)
9382

9483
async with asgi_app.router.lifespan_context(asgi_app):
95-
assert await _call_whoami(asgi_app, host, "token-A") == "token-A"
96-
assert await _call_whoami(asgi_app, host, "token-B") == "token-B"
97-
assert await _call_whoami(asgi_app, host, None) == "<none>"
84+
auth = _MutableBearerAuth("token-A")
85+
async with (
86+
httpx2.ASGITransport(asgi_app) as transport,
87+
httpx2.AsyncClient(
88+
transport=transport,
89+
base_url=f"http://{host}",
90+
auth=auth,
91+
timeout=httpx2.Timeout(30, read=30),
92+
follow_redirects=True,
93+
) as http_client,
94+
Client(streamable_http_client(f"http://{host}/mcp", http_client=http_client), mode="legacy") as client,
95+
):
96+
assert await _call_whoami(client) == "token-A"
97+
98+
auth.token = "token-B"
99+
assert await _call_whoami(client) == "token-B"
100+
101+
102+
@pytest.mark.anyio
103+
async def test_notification_handler_get_access_token_reflects_current_request_in_stateful_session() -> None:
104+
host = "testserver"
105+
send_token, receive_token = anyio.create_memory_object_stream[str](10)
106+
107+
async def handle_progress(ctx: ServerRequestContext, params: ProgressNotificationParams) -> None:
108+
access = get_access_token()
109+
await send_token.send(access.token if access else "<none>")
110+
111+
server = Server(
112+
"auth-test-server",
113+
on_call_tool=_handle_whoami,
114+
on_list_tools=_handle_list_tools,
115+
)
116+
server.add_notification_handler("notifications/progress", ProgressNotificationParams, handle_progress)
117+
118+
session_manager = StreamableHTTPSessionManager(app=server, stateless=False)
119+
120+
asgi_app = Starlette(
121+
routes=[Mount("/mcp", app=session_manager.handle_request)],
122+
middleware=[
123+
Middleware(AuthenticationMiddleware, backend=BearerAuthBackend(_EchoTokenVerifier())),
124+
Middleware(AuthContextMiddleware),
125+
],
126+
lifespan=lambda app: session_manager.run(),
127+
)
128+
129+
async with send_token, receive_token, asgi_app.router.lifespan_context(asgi_app):
130+
auth = _MutableBearerAuth("token-A")
131+
async with (
132+
httpx2.ASGITransport(asgi_app) as transport,
133+
httpx2.AsyncClient(
134+
transport=transport,
135+
base_url=f"http://{host}",
136+
auth=auth,
137+
timeout=httpx2.Timeout(30, read=30),
138+
follow_redirects=True,
139+
) as http_client,
140+
Client(streamable_http_client(f"http://{host}/mcp", http_client=http_client), mode="legacy") as client,
141+
):
142+
await client.send_progress_notification("token-A", 0.1) # pyright: ignore[reportDeprecated]
143+
with anyio.fail_after(5):
144+
assert await receive_token.receive() == "token-A"
145+
146+
auth.token = "token-B"
147+
await client.send_progress_notification("token-B", 0.2) # pyright: ignore[reportDeprecated]
148+
with anyio.fail_after(5):
149+
assert await receive_token.receive() == "token-B"

0 commit comments

Comments
 (0)