Skip to content

Commit 50aa742

Browse files
committed
test: add unit tests for CallToolHandler, GetPromptHandler, PingHandler, and ReadResourceHandler
1 parent a28dc2a commit 50aa742

File tree

4 files changed

+1130
-0
lines changed

4 files changed

+1130
-0
lines changed
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Tests\Server\RequestHandler;
13+
14+
use Mcp\Capability\Tool\ToolExecutorInterface;
15+
use Mcp\Exception\ToolExecutionException;
16+
use Mcp\Exception\ToolNotFoundException;
17+
use Mcp\Schema\Content\TextContent;
18+
use Mcp\Schema\JsonRpc\Error;
19+
use Mcp\Schema\JsonRpc\Request;
20+
use Mcp\Schema\JsonRpc\Response;
21+
use Mcp\Schema\Request\CallToolRequest;
22+
use Mcp\Schema\Result\CallToolResult;
23+
use Mcp\Server\RequestHandler\CallToolHandler;
24+
use PHPUnit\Framework\MockObject\MockObject;
25+
use PHPUnit\Framework\TestCase;
26+
use Psr\Log\LoggerInterface;
27+
28+
class CallToolHandlerTest extends TestCase
29+
{
30+
private CallToolHandler $handler;
31+
private ToolExecutorInterface|MockObject $toolExecutor;
32+
private LoggerInterface|MockObject $logger;
33+
34+
protected function setUp(): void
35+
{
36+
$this->toolExecutor = $this->createMock(ToolExecutorInterface::class);
37+
$this->logger = $this->createMock(LoggerInterface::class);
38+
39+
$this->handler = new CallToolHandler(
40+
$this->toolExecutor,
41+
$this->logger,
42+
);
43+
}
44+
45+
public function testSupportsCallToolRequest(): void
46+
{
47+
$request = $this->createCallToolRequest('test_tool', ['param' => 'value']);
48+
49+
$this->assertTrue($this->handler->supports($request));
50+
}
51+
52+
public function testHandleSuccessfulToolCall(): void
53+
{
54+
$request = $this->createCallToolRequest('greet_user', ['name' => 'John']);
55+
$expectedResult = new CallToolResult([new TextContent('Hello, John!')]);
56+
57+
$this->toolExecutor
58+
->expects($this->once())
59+
->method('call')
60+
->with($request)
61+
->willReturn($expectedResult);
62+
63+
$this->logger
64+
->expects($this->never())
65+
->method('error');
66+
67+
$response = $this->handler->handle($request);
68+
69+
$this->assertInstanceOf(Response::class, $response);
70+
$this->assertEquals($request->getId(), $response->id);
71+
$this->assertSame($expectedResult, $response->result);
72+
}
73+
74+
public function testHandleToolCallWithEmptyArguments(): void
75+
{
76+
$request = $this->createCallToolRequest('simple_tool', []);
77+
$expectedResult = new CallToolResult([new TextContent('Simple result')]);
78+
79+
$this->toolExecutor
80+
->expects($this->once())
81+
->method('call')
82+
->with($request)
83+
->willReturn($expectedResult);
84+
85+
$response = $this->handler->handle($request);
86+
87+
$this->assertInstanceOf(Response::class, $response);
88+
$this->assertSame($expectedResult, $response->result);
89+
}
90+
91+
public function testHandleToolCallWithComplexArguments(): void
92+
{
93+
$arguments = [
94+
'string_param' => 'value',
95+
'int_param' => 42,
96+
'bool_param' => true,
97+
'array_param' => ['nested' => 'data'],
98+
'null_param' => null,
99+
];
100+
$request = $this->createCallToolRequest('complex_tool', $arguments);
101+
$expectedResult = new CallToolResult([new TextContent('Complex result')]);
102+
103+
$this->toolExecutor
104+
->expects($this->once())
105+
->method('call')
106+
->with($request)
107+
->willReturn($expectedResult);
108+
109+
$response = $this->handler->handle($request);
110+
111+
$this->assertInstanceOf(Response::class, $response);
112+
$this->assertSame($expectedResult, $response->result);
113+
}
114+
115+
public function testHandleToolNotFoundExceptionReturnsError(): void
116+
{
117+
$request = $this->createCallToolRequest('nonexistent_tool', ['param' => 'value']);
118+
$exception = new ToolNotFoundException($request);
119+
120+
$this->toolExecutor
121+
->expects($this->once())
122+
->method('call')
123+
->with($request)
124+
->willThrowException($exception);
125+
126+
$this->logger
127+
->expects($this->once())
128+
->method('error')
129+
->with(
130+
'Error while executing tool "nonexistent_tool": "Tool not found for call: "nonexistent_tool".".',
131+
[
132+
'tool' => 'nonexistent_tool',
133+
'arguments' => ['param' => 'value'],
134+
],
135+
);
136+
137+
$response = $this->handler->handle($request);
138+
139+
$this->assertInstanceOf(Error::class, $response);
140+
$this->assertEquals($request->getId(), $response->id);
141+
$this->assertEquals(Error::INTERNAL_ERROR, $response->code);
142+
$this->assertEquals('Error while executing tool', $response->message);
143+
}
144+
145+
public function testHandleToolExecutionExceptionReturnsError(): void
146+
{
147+
$request = $this->createCallToolRequest('failing_tool', ['param' => 'value']);
148+
$exception = new ToolExecutionException($request, new \RuntimeException('Tool execution failed'));
149+
150+
$this->toolExecutor
151+
->expects($this->once())
152+
->method('call')
153+
->with($request)
154+
->willThrowException($exception);
155+
156+
$this->logger
157+
->expects($this->once())
158+
->method('error')
159+
->with(
160+
'Error while executing tool "failing_tool": "Execution of tool "failing_tool" failed with error: "Tool execution failed".".',
161+
[
162+
'tool' => 'failing_tool',
163+
'arguments' => ['param' => 'value'],
164+
],
165+
);
166+
167+
$response = $this->handler->handle($request);
168+
169+
$this->assertInstanceOf(Error::class, $response);
170+
$this->assertEquals($request->getId(), $response->id);
171+
$this->assertEquals(Error::INTERNAL_ERROR, $response->code);
172+
$this->assertEquals('Error while executing tool', $response->message);
173+
}
174+
175+
public function testHandleWithNullResult(): void
176+
{
177+
$request = $this->createCallToolRequest('null_tool', []);
178+
$expectedResult = new CallToolResult([]);
179+
180+
$this->toolExecutor
181+
->expects($this->once())
182+
->method('call')
183+
->with($request)
184+
->willReturn($expectedResult);
185+
186+
$response = $this->handler->handle($request);
187+
188+
$this->assertInstanceOf(Response::class, $response);
189+
$this->assertSame($expectedResult, $response->result);
190+
}
191+
192+
public function testHandleWithErrorResult(): void
193+
{
194+
$request = $this->createCallToolRequest('error_tool', []);
195+
$expectedResult = CallToolResult::error([new TextContent('Tool error occurred')]);
196+
197+
$this->toolExecutor
198+
->expects($this->once())
199+
->method('call')
200+
->with($request)
201+
->willReturn($expectedResult);
202+
203+
$response = $this->handler->handle($request);
204+
205+
$this->assertInstanceOf(Response::class, $response);
206+
$this->assertSame($expectedResult, $response->result);
207+
$this->assertTrue($response->result->isError);
208+
}
209+
210+
public function testConstructorWithDefaultLogger(): void
211+
{
212+
$handler = new CallToolHandler($this->toolExecutor);
213+
214+
$this->assertInstanceOf(CallToolHandler::class, $handler);
215+
}
216+
217+
public function testHandleLogsErrorWithCorrectParameters(): void
218+
{
219+
$request = $this->createCallToolRequest('test_tool', ['key1' => 'value1', 'key2' => 42]);
220+
$exception = new ToolExecutionException($request, new \RuntimeException('Custom error message'));
221+
222+
$this->toolExecutor
223+
->expects($this->once())
224+
->method('call')
225+
->willThrowException($exception);
226+
227+
$this->logger
228+
->expects($this->once())
229+
->method('error')
230+
->with(
231+
'Error while executing tool "test_tool": "Execution of tool "test_tool" failed with error: "Custom error message".".',
232+
[
233+
'tool' => 'test_tool',
234+
'arguments' => ['key1' => 'value1', 'key2' => 42],
235+
],
236+
);
237+
238+
$this->handler->handle($request);
239+
}
240+
241+
public function testHandleWithSpecialCharactersInToolName(): void
242+
{
243+
$request = $this->createCallToolRequest('tool-with_special.chars', []);
244+
$expectedResult = new CallToolResult([new TextContent('Special tool result')]);
245+
246+
$this->toolExecutor
247+
->expects($this->once())
248+
->method('call')
249+
->with($request)
250+
->willReturn($expectedResult);
251+
252+
$response = $this->handler->handle($request);
253+
254+
$this->assertInstanceOf(Response::class, $response);
255+
$this->assertSame($expectedResult, $response->result);
256+
}
257+
258+
public function testHandleWithSpecialCharactersInArguments(): void
259+
{
260+
$arguments = [
261+
'special_chars' => 'äöü ñ 中文 🚀',
262+
'unicode' => '\\u{1F600}',
263+
'quotes' => 'text with "quotes" and \'single quotes\'',
264+
];
265+
$request = $this->createCallToolRequest('unicode_tool', $arguments);
266+
$expectedResult = new CallToolResult([new TextContent('Unicode handled')]);
267+
268+
$this->toolExecutor
269+
->expects($this->once())
270+
->method('call')
271+
->with($request)
272+
->willReturn($expectedResult);
273+
274+
$response = $this->handler->handle($request);
275+
276+
$this->assertInstanceOf(Response::class, $response);
277+
$this->assertSame($expectedResult, $response->result);
278+
}
279+
280+
private function createCallToolRequest(string $name, array $arguments): Request
281+
{
282+
return CallToolRequest::fromArray([
283+
'jsonrpc' => '2.0',
284+
'method' => CallToolRequest::getMethod(),
285+
'id' => 'test-request-'.uniqid(),
286+
'params' => [
287+
'name' => $name,
288+
'arguments' => $arguments,
289+
],
290+
]);
291+
}
292+
}

0 commit comments

Comments
 (0)