66
77import java .time .Duration ;
88import java .util .Map ;
9+ import java .util .function .Function ;
910
1011import io .modelcontextprotocol .MockMcpClientTransport ;
1112import io .modelcontextprotocol .json .TypeRef ;
12- import org .junit .jupiter .api .AfterEach ;
13- import org .junit .jupiter .api .BeforeEach ;
1413import org .junit .jupiter .api .Test ;
1514import org .slf4j .Logger ;
1615import org .slf4j .LoggerFactory ;
1918import reactor .test .StepVerifier ;
2019
2120import static org .assertj .core .api .Assertions .assertThat ;
22- import static org .assertj .core .api .Assertions .assertThatThrownBy ;
2321
2422/**
2523 * Test suite for {@link McpClientSession} that verifies its JSON-RPC message handling,
@@ -39,35 +37,6 @@ class McpClientSessionTests {
3937
4038 private static final String ECHO_METHOD = "echo" ;
4139
42- private McpClientSession session ;
43-
44- private MockMcpClientTransport transport ;
45-
46- @ BeforeEach
47- void setUp () {
48- transport = new MockMcpClientTransport ();
49- session = new McpClientSession (TIMEOUT , transport , Map .of (),
50- Map .of (TEST_NOTIFICATION , params -> Mono .fromRunnable (() -> logger .info ("Status update: {}" , params ))));
51- }
52-
53- @ AfterEach
54- void tearDown () {
55- if (session != null ) {
56- session .close ();
57- }
58- }
59-
60- @ Test
61- void testConstructorWithInvalidArguments () {
62- assertThatThrownBy (() -> new McpClientSession (null , transport , Map .of (), Map .of ()))
63- .isInstanceOf (IllegalArgumentException .class )
64- .hasMessageContaining ("The requestTimeout can not be null" );
65-
66- assertThatThrownBy (() -> new McpClientSession (TIMEOUT , null , Map .of (), Map .of ()))
67- .isInstanceOf (IllegalArgumentException .class )
68- .hasMessageContaining ("transport can not be null" );
69- }
70-
7140 TypeRef <String > responseType = new TypeRef <>() {
7241 };
7342
@@ -76,6 +45,11 @@ void testSendRequest() {
7645 String testParam = "test parameter" ;
7746 String responseData = "test response" ;
7847
48+ var transport = new MockMcpClientTransport ();
49+ var session = new McpClientSession (TIMEOUT , transport , Map .of (),
50+ Map .of (TEST_NOTIFICATION , params -> Mono .fromRunnable (() -> logger .info ("Status update: {}" , params ))),
51+ Function .identity ());
52+
7953 // Create a Mono that will emit the response after the request is sent
8054 Mono <String > responseMono = session .sendRequest (TEST_METHOD , testParam , responseType );
8155 // Verify response handling
@@ -92,10 +66,17 @@ void testSendRequest() {
9266 assertThat (request .params ()).isEqualTo (testParam );
9367 assertThat (response ).isEqualTo (responseData );
9468 }).verifyComplete ();
69+
70+ session .close ();
9571 }
9672
9773 @ Test
9874 void testSendRequestWithError () {
75+ var transport = new MockMcpClientTransport ();
76+ var session = new McpClientSession (TIMEOUT , transport , Map .of (),
77+ Map .of (TEST_NOTIFICATION , params -> Mono .fromRunnable (() -> logger .info ("Status update: {}" , params ))),
78+ Function .identity ());
79+
9980 Mono <String > responseMono = session .sendRequest (TEST_METHOD , "test" , responseType );
10081
10182 // Verify error handling
@@ -107,20 +88,34 @@ void testSendRequestWithError() {
10788 transport .simulateIncomingMessage (
10889 new McpSchema .JSONRPCResponse (McpSchema .JSONRPC_VERSION , request .id (), null , error ));
10990 }).expectError (McpError .class ).verify ();
91+
92+ session .close ();
11093 }
11194
11295 @ Test
11396 void testRequestTimeout () {
97+ var transport = new MockMcpClientTransport ();
98+ var session = new McpClientSession (TIMEOUT , transport , Map .of (),
99+ Map .of (TEST_NOTIFICATION , params -> Mono .fromRunnable (() -> logger .info ("Status update: {}" , params ))),
100+ Function .identity ());
101+
114102 Mono <String > responseMono = session .sendRequest (TEST_METHOD , "test" , responseType );
115103
116104 // Verify timeout
117105 StepVerifier .create (responseMono )
118106 .expectError (java .util .concurrent .TimeoutException .class )
119107 .verify (TIMEOUT .plusSeconds (1 ));
108+
109+ session .close ();
120110 }
121111
122112 @ Test
123113 void testSendNotification () {
114+ var transport = new MockMcpClientTransport ();
115+ var session = new McpClientSession (TIMEOUT , transport , Map .of (),
116+ Map .of (TEST_NOTIFICATION , params -> Mono .fromRunnable (() -> logger .info ("Status update: {}" , params ))),
117+ Function .identity ());
118+
124119 Map <String , Object > params = Map .of ("key" , "value" );
125120 Mono <Void > notificationMono = session .sendNotification (TEST_NOTIFICATION , params );
126121
@@ -132,15 +127,17 @@ void testSendNotification() {
132127 assertThat (notification .method ()).isEqualTo (TEST_NOTIFICATION );
133128 assertThat (notification .params ()).isEqualTo (params );
134129 }).verifyComplete ();
130+
131+ session .close ();
135132 }
136133
137134 @ Test
138135 void testRequestHandling () {
139136 String echoMessage = "Hello MCP!" ;
140137 Map <String , McpClientSession .RequestHandler <?>> requestHandlers = Map .of (ECHO_METHOD ,
141138 params -> Mono .just (params ));
142- transport = new MockMcpClientTransport ();
143- session = new McpClientSession (TIMEOUT , transport , requestHandlers , Map .of ());
139+ var transport = new MockMcpClientTransport ();
140+ var session = new McpClientSession (TIMEOUT , transport , requestHandlers , Map .of (), Function . identity ());
144141
145142 // Simulate incoming request
146143 McpSchema .JSONRPCRequest request = new McpSchema .JSONRPCRequest (McpSchema .JSONRPC_VERSION , ECHO_METHOD ,
@@ -153,15 +150,18 @@ void testRequestHandling() {
153150 McpSchema .JSONRPCResponse response = (McpSchema .JSONRPCResponse ) sentMessage ;
154151 assertThat (response .result ()).isEqualTo (echoMessage );
155152 assertThat (response .error ()).isNull ();
153+
154+ session .close ();
156155 }
157156
158157 @ Test
159158 void testNotificationHandling () {
160159 Sinks .One <Object > receivedParams = Sinks .one ();
161160
162- transport = new MockMcpClientTransport ();
163- session = new McpClientSession (TIMEOUT , transport , Map .of (),
164- Map .of (TEST_NOTIFICATION , params -> Mono .fromRunnable (() -> receivedParams .tryEmitValue (params ))));
161+ var transport = new MockMcpClientTransport ();
162+ var session = new McpClientSession (TIMEOUT , transport , Map .of (),
163+ Map .of (TEST_NOTIFICATION , params -> Mono .fromRunnable (() -> receivedParams .tryEmitValue (params ))),
164+ Function .identity ());
165165
166166 // Simulate incoming notification from the server
167167 Map <String , Object > notificationParams = Map .of ("status" , "ready" );
@@ -173,10 +173,18 @@ void testNotificationHandling() {
173173
174174 // Verify handler was called
175175 assertThat (receivedParams .asMono ().block (Duration .ofSeconds (1 ))).isEqualTo (notificationParams );
176+
177+ session .close ();
176178 }
177179
178180 @ Test
179181 void testUnknownMethodHandling () {
182+
183+ var transport = new MockMcpClientTransport ();
184+ var session = new McpClientSession (TIMEOUT , transport , Map .of (),
185+ Map .of (TEST_NOTIFICATION , params -> Mono .fromRunnable (() -> logger .info ("Status update: {}" , params ))),
186+ Function .identity ());
187+
180188 // Simulate incoming request for unknown method
181189 McpSchema .JSONRPCRequest request = new McpSchema .JSONRPCRequest (McpSchema .JSONRPC_VERSION , "unknown.method" ,
182190 "test-id" , null );
@@ -188,10 +196,117 @@ void testUnknownMethodHandling() {
188196 McpSchema .JSONRPCResponse response = (McpSchema .JSONRPCResponse ) sentMessage ;
189197 assertThat (response .error ()).isNotNull ();
190198 assertThat (response .error ().code ()).isEqualTo (McpSchema .ErrorCodes .METHOD_NOT_FOUND );
199+
200+ session .close ();
201+ }
202+
203+ @ Test
204+ void testRequestHandlerThrowsMcpErrorWithJsonRpcError () {
205+ // Setup: Create a request handler that throws McpError with custom error code and
206+ // data
207+ String testMethod = "test.customError" ;
208+ Map <String , Object > errorData = Map .of ("customField" , "customValue" );
209+ McpClientSession .RequestHandler <?> failingHandler = params -> Mono
210+ .error (McpError .builder (123 ).message ("Custom error message" ).data (errorData ).build ());
211+
212+ var transport = new MockMcpClientTransport ();
213+ var session = new McpClientSession (TIMEOUT , transport , Map .of (testMethod , failingHandler ), Map .of (),
214+ Function .identity ());
215+
216+ // Simulate incoming request that will trigger the error
217+ McpSchema .JSONRPCRequest request = new McpSchema .JSONRPCRequest (McpSchema .JSONRPC_VERSION , testMethod ,
218+ "test-id" , null );
219+ transport .simulateIncomingMessage (request );
220+
221+ // Verify: The response should contain the custom error from McpError
222+ McpSchema .JSONRPCMessage sentMessage = transport .getLastSentMessage ();
223+ assertThat (sentMessage ).isInstanceOf (McpSchema .JSONRPCResponse .class );
224+ McpSchema .JSONRPCResponse response = (McpSchema .JSONRPCResponse ) sentMessage ;
225+ assertThat (response .error ()).isNotNull ();
226+ assertThat (response .error ().code ()).isEqualTo (123 );
227+ assertThat (response .error ().message ()).isEqualTo ("Custom error message" );
228+ assertThat (response .error ().data ()).isEqualTo (errorData );
229+
230+ session .close ();
231+ }
232+
233+ @ Test
234+ void testRequestHandlerThrowsGenericException () {
235+ // Setup: Create a request handler that throws a generic RuntimeException
236+ String testMethod = "test.genericError" ;
237+ RuntimeException exception = new RuntimeException ("Something went wrong" );
238+ McpClientSession .RequestHandler <?> failingHandler = params -> Mono .error (exception );
239+
240+ var transport = new MockMcpClientTransport ();
241+ var session = new McpClientSession (TIMEOUT , transport , Map .of (testMethod , failingHandler ), Map .of (),
242+ Function .identity ());
243+
244+ // Simulate incoming request that will trigger the error
245+ McpSchema .JSONRPCRequest request = new McpSchema .JSONRPCRequest (McpSchema .JSONRPC_VERSION , testMethod ,
246+ "test-id" , null );
247+ transport .simulateIncomingMessage (request );
248+
249+ // Verify: The response should contain INTERNAL_ERROR with aggregated exception
250+ // messages in data field
251+ McpSchema .JSONRPCMessage sentMessage = transport .getLastSentMessage ();
252+ assertThat (sentMessage ).isInstanceOf (McpSchema .JSONRPCResponse .class );
253+ McpSchema .JSONRPCResponse response = (McpSchema .JSONRPCResponse ) sentMessage ;
254+ assertThat (response .error ()).isNotNull ();
255+ assertThat (response .error ().code ()).isEqualTo (McpSchema .ErrorCodes .INTERNAL_ERROR );
256+ assertThat (response .error ().message ()).isEqualTo ("Something went wrong" );
257+ // Verify data field contains aggregated exception messages
258+ assertThat (response .error ().data ()).isNotNull ();
259+ assertThat (response .error ().data ().toString ()).contains ("RuntimeException" );
260+ assertThat (response .error ().data ().toString ()).contains ("Something went wrong" );
261+
262+ session .close ();
263+ }
264+
265+ @ Test
266+ void testRequestHandlerThrowsExceptionWithCause () {
267+ // Setup: Create a request handler that throws an exception with a cause chain
268+ String testMethod = "test.chainedError" ;
269+ RuntimeException rootCause = new IllegalArgumentException ("Root cause message" );
270+ RuntimeException middleCause = new IllegalStateException ("Middle cause message" , rootCause );
271+ RuntimeException topException = new RuntimeException ("Top level message" , middleCause );
272+ McpClientSession .RequestHandler <?> failingHandler = params -> Mono .error (topException );
273+
274+ var transport = new MockMcpClientTransport ();
275+ var session = new McpClientSession (TIMEOUT , transport , Map .of (testMethod , failingHandler ), Map .of (),
276+ Function .identity ());
277+
278+ // Simulate incoming request that will trigger the error
279+ McpSchema .JSONRPCRequest request = new McpSchema .JSONRPCRequest (McpSchema .JSONRPC_VERSION , testMethod ,
280+ "test-id" , null );
281+ transport .simulateIncomingMessage (request );
282+
283+ // Verify: The response should contain INTERNAL_ERROR with full exception chain
284+ // in data field
285+ McpSchema .JSONRPCMessage sentMessage = transport .getLastSentMessage ();
286+ assertThat (sentMessage ).isInstanceOf (McpSchema .JSONRPCResponse .class );
287+ McpSchema .JSONRPCResponse response = (McpSchema .JSONRPCResponse ) sentMessage ;
288+ assertThat (response .error ()).isNotNull ();
289+ assertThat (response .error ().code ()).isEqualTo (McpSchema .ErrorCodes .INTERNAL_ERROR );
290+ assertThat (response .error ().message ()).isEqualTo ("Top level message" );
291+ // Verify data field contains the full exception chain
292+ String dataString = response .error ().data ().toString ();
293+ assertThat (dataString ).contains ("RuntimeException" );
294+ assertThat (dataString ).contains ("Top level message" );
295+ assertThat (dataString ).contains ("IllegalStateException" );
296+ assertThat (dataString ).contains ("Middle cause message" );
297+ assertThat (dataString ).contains ("IllegalArgumentException" );
298+ assertThat (dataString ).contains ("Root cause message" );
299+
300+ session .close ();
191301 }
192302
193303 @ Test
194304 void testGracefulShutdown () {
305+ var transport = new MockMcpClientTransport ();
306+ var session = new McpClientSession (TIMEOUT , transport , Map .of (),
307+ Map .of (TEST_NOTIFICATION , params -> Mono .fromRunnable (() -> logger .info ("Status update: {}" , params ))),
308+ Function .identity ());
309+
195310 StepVerifier .create (session .closeGracefully ()).verifyComplete ();
196311 }
197312
0 commit comments