Skip to content

Commit f3ed099

Browse files
committed
test(auth): add E2E tests for tenant_id binding in request/notification handling
Add two E2E tests using Client(server) that exercise the session.tenant_id set-once binding in lowlevel/server.py, covering the previously uncovered branches in _handle_request (line 456) and _handle_notification (line 504).
1 parent 161f123 commit f3ed099

1 file changed

Lines changed: 85 additions & 1 deletion

File tree

tests/server/test_multi_tenancy_session.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import anyio
66
import pytest
77

8+
from mcp import Client
9+
from mcp.server import Server
810
from mcp.server.auth.middleware.auth_context import auth_context_var, get_tenant_id
911
from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser
1012
from mcp.server.auth.provider import AccessToken
@@ -15,7 +17,7 @@
1517
from mcp.shared._context import RequestContext
1618
from mcp.shared.message import SessionMessage
1719
from mcp.shared.session import BaseSession
18-
from mcp.types import ServerCapabilities
20+
from mcp.types import ListToolsResult, NotificationParams, PaginatedRequestParams, ServerCapabilities
1921

2022

2123
def _simulate_tenant_binding(session: ServerSession, tenant_id_value: str) -> None:
@@ -321,3 +323,85 @@ async def test_server_session_isolation_between_instances(init_options: Initiali
321323
# Verify the other session is unaffected
322324
assert session1.tenant_id == "tenant-gamma"
323325
assert session2.tenant_id == "tenant-beta" # Still beta, not gamma
326+
327+
328+
@pytest.mark.anyio
329+
async def test_handle_request_populates_session_tenant_id():
330+
"""E2E: session.tenant_id is set from auth context during request handling.
331+
332+
This exercises the set-once tenant binding in lowlevel/server.py
333+
_handle_request, covering the branch where get_tenant_id() returns
334+
a non-None value.
335+
"""
336+
captured_ctx_tenant: str | None = None
337+
captured_session_tenant: str | None = None
338+
339+
async def handle_list_tools(
340+
ctx: ServerRequestContext, params: PaginatedRequestParams | None
341+
) -> ListToolsResult:
342+
nonlocal captured_ctx_tenant, captured_session_tenant
343+
captured_ctx_tenant = ctx.tenant_id
344+
captured_session_tenant = ctx.session.tenant_id
345+
return ListToolsResult(tools=[])
346+
347+
server = Server("test", on_list_tools=handle_list_tools)
348+
349+
# Set auth context with tenant before entering the Client —
350+
# contextvars are inherited by child tasks, so the server will see it
351+
access_token = AccessToken(
352+
token="test-token",
353+
client_id="test-client",
354+
scopes=["read"],
355+
expires_at=int(time.time()) + 3600,
356+
tenant_id="tenant-e2e",
357+
)
358+
user = AuthenticatedUser(access_token)
359+
token = auth_context_var.set(user)
360+
try:
361+
async with Client(server) as client:
362+
await client.list_tools()
363+
finally:
364+
auth_context_var.reset(token)
365+
366+
assert captured_ctx_tenant == "tenant-e2e"
367+
assert captured_session_tenant == "tenant-e2e"
368+
369+
370+
@pytest.mark.anyio
371+
async def test_handle_notification_populates_session_tenant_id():
372+
"""E2E: session.tenant_id is set from auth context during notification handling.
373+
374+
This exercises the set-once tenant binding in lowlevel/server.py
375+
_handle_notification, covering the branch where get_tenant_id() returns
376+
a non-None value.
377+
"""
378+
notification_tenant: str | None = None
379+
notification_received = anyio.Event()
380+
381+
async def handle_roots_list_changed(
382+
ctx: ServerRequestContext, params: NotificationParams | None
383+
) -> None:
384+
nonlocal notification_tenant
385+
notification_tenant = ctx.tenant_id
386+
notification_received.set()
387+
388+
server = Server("test", on_roots_list_changed=handle_roots_list_changed)
389+
390+
access_token = AccessToken(
391+
token="test-token",
392+
client_id="test-client",
393+
scopes=["read"],
394+
expires_at=int(time.time()) + 3600,
395+
tenant_id="tenant-notify",
396+
)
397+
user = AuthenticatedUser(access_token)
398+
token = auth_context_var.set(user)
399+
try:
400+
async with Client(server) as client:
401+
await client.session.send_roots_list_changed()
402+
with anyio.fail_after(5):
403+
await notification_received.wait()
404+
finally:
405+
auth_context_var.reset(token)
406+
407+
assert notification_tenant == "tenant-notify"

0 commit comments

Comments
 (0)