-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy pathtest_non2xx_status.py
More file actions
133 lines (103 loc) · 5.32 KB
/
Copy pathtest_non2xx_status.py
File metadata and controls
133 lines (103 loc) · 5.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""Tests for non-2xx HTTP status handling in StreamableHTTPTransport.
Verifies that when the server returns 401/403/5xx, the caller receives
a proper JSONRPCError (not a timeout).
Closes #3091
"""
import json
from unittest.mock import AsyncMock, MagicMock
import pytest
from mcp_types import JSONRPCError, JSONRPCRequest
from mcp.client.streamable_http import RequestContext, StreamableHTTPTransport
from mcp.shared.message import SessionMessage
class TestNon2xxStatusHandling:
"""Test that non-2xx status codes produce proper error responses."""
def _make_request_context(self, request_id: str = "test-123") -> MagicMock:
"""Create a mock RequestContext."""
ctx = MagicMock(spec=RequestContext)
ctx.session_message = MagicMock()
ctx.session_message.message = MagicMock(spec=JSONRPCRequest)
ctx.session_message.message.id = request_id
ctx.read_stream_writer = AsyncMock()
ctx.client = MagicMock() # Use MagicMock for client (not AsyncMock)
ctx.metadata = None
return ctx
def _make_transport(self) -> StreamableHTTPTransport:
"""Create a StreamableHTTPTransport for testing."""
return StreamableHTTPTransport("http://test/mcp")
@pytest.mark.anyio
async def test_401_produces_error_response(self):
"""401 Unauthorized should produce a JSONRPCError with the request's ID."""
transport = self._make_transport()
ctx = self._make_request_context()
mock_response = MagicMock()
mock_response.status_code = 401
mock_response.headers = {"content-type": "text/plain"}
mock_response.aread = AsyncMock(return_value=b"Unauthorized")
# Use MagicMock with async __aenter__/__aexit__
mock_stream_ctx = MagicMock()
mock_stream_ctx.__aenter__ = AsyncMock(return_value=mock_response)
mock_stream_ctx.__aexit__ = AsyncMock(return_value=False)
ctx.client.stream.return_value = mock_stream_ctx
await transport._handle_post_request(ctx)
ctx.read_stream_writer.send.assert_called_once()
sent_message = ctx.read_stream_writer.send.call_args[0][0]
assert isinstance(sent_message, SessionMessage)
assert isinstance(sent_message.message, JSONRPCError)
assert sent_message.message.id == "test-123"
assert sent_message.message.error.code == -32603
@pytest.mark.anyio
async def test_403_produces_error_response(self):
"""403 Forbidden should produce a JSONRPCError with the request's ID."""
transport = self._make_transport()
ctx = self._make_request_context()
mock_response = MagicMock()
mock_response.status_code = 403
mock_response.headers = {"content-type": "text/plain"}
mock_response.aread = AsyncMock(return_value=b"Forbidden")
mock_stream_ctx = MagicMock()
mock_stream_ctx.__aenter__ = AsyncMock(return_value=mock_response)
mock_stream_ctx.__aexit__ = AsyncMock(return_value=False)
ctx.client.stream.return_value = mock_stream_ctx
await transport._handle_post_request(ctx)
ctx.read_stream_writer.send.assert_called_once()
sent_message = ctx.read_stream_writer.send.call_args[0][0]
assert isinstance(sent_message.message, JSONRPCError)
assert sent_message.message.id == "test-123"
@pytest.mark.anyio
async def test_500_produces_error_response(self):
"""500 Internal Server Error should produce a JSONRPCError."""
transport = self._make_transport()
ctx = self._make_request_context()
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.headers = {"content-type": "text/plain"}
mock_response.aread = AsyncMock(return_value=b"Internal Server Error")
mock_stream_ctx = MagicMock()
mock_stream_ctx.__aenter__ = AsyncMock(return_value=mock_response)
mock_stream_ctx.__aexit__ = AsyncMock(return_value=False)
ctx.client.stream.return_value = mock_stream_ctx
await transport._handle_post_request(ctx)
ctx.read_stream_writer.send.assert_called_once()
sent_message = ctx.read_stream_writer.send.call_args[0][0]
assert isinstance(sent_message.message, JSONRPCError)
@pytest.mark.anyio
async def test_json_error_body_is_parsed(self):
"""When server returns JSON-RPC error body, it should be used directly."""
transport = self._make_transport()
ctx = self._make_request_context()
error_body = json.dumps(
{"jsonrpc": "2.0", "id": "test-123", "error": {"code": -32600, "message": "Invalid Request"}}
).encode()
mock_response = MagicMock()
mock_response.status_code = 400
mock_response.headers = {"content-type": "application/json"}
mock_response.aread = AsyncMock(return_value=error_body)
mock_stream_ctx = MagicMock()
mock_stream_ctx.__aenter__ = AsyncMock(return_value=mock_response)
mock_stream_ctx.__aexit__ = AsyncMock(return_value=False)
ctx.client.stream.return_value = mock_stream_ctx
await transport._handle_post_request(ctx)
ctx.read_stream_writer.send.assert_called_once()
sent_message = ctx.read_stream_writer.send.call_args[0][0]
assert isinstance(sent_message.message, JSONRPCError)
assert sent_message.message.error.code == -32600