Skip to content

Commit 67a5c2a

Browse files
author
larry.sulebalogun
committed
10 - Added the list resources handler with test class
1 parent f3bea09 commit 67a5c2a

File tree

3 files changed

+234
-10
lines changed

3 files changed

+234
-10
lines changed

src/Server/RequestHandler/ListResourcesHandler.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ public function handle(ListResourcesRequest|HasMethodInterface $message): Respon
3838
{
3939
\assert($message instanceof ListResourcesRequest);
4040

41-
$cursor = null;
4241
$resources = $this->registry->getResources($this->pageSize, $message->cursor);
43-
$nextCursor = (null !== $cursor && \count($resources) === $this->pageSize) ? $cursor : null;
42+
43+
$nextCursor = $this->registry->calculateNextCursor(
44+
$this->registry->getResourcesCount(),
45+
$message->cursor,
46+
\count($resources)
47+
);
4448

4549
return new Response(
4650
$message->getId(),

tests/Server/RequestHandler/ListPromptsHandlerTest.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
use Mcp\Server\RequestHandler\ListPromptsHandler;
2020
use PHPUnit\Framework\Attributes\TestDox;
2121
use PHPUnit\Framework\TestCase;
22-
use ReflectionClass;
2322

2423
class ListPromptsHandlerTest extends TestCase
2524
{
@@ -166,7 +165,7 @@ public function testHandlesCursorAtExactBoundary(): void
166165
{
167166
// Arrange
168167
$this->addPromptsToRegistry(6);
169-
$exactBoundaryCursor = base64_encode('6');
168+
$exactBoundaryCursor = base64_encode('6'); // Exactly at the end
170169
$request = $this->createListPromptsRequest(cursor: $exactBoundaryCursor);
171170

172171
// Act
@@ -208,12 +207,13 @@ private function addPromptsToRegistry(int $count): void
208207

209208
private function createListPromptsRequest(?string $cursor = null): ListPromptsRequest
210209
{
211-
$listPromptsRequest = new ListPromptsRequest(cursor: $cursor);
212-
213-
$reflection = new ReflectionClass($listPromptsRequest);
214-
$idProperty = $reflection->getProperty('id');
215-
$idProperty->setValue($listPromptsRequest, 'test-request-id');
210+
$mock = $this->getMockBuilder(ListPromptsRequest::class)
211+
->setConstructorArgs([$cursor])
212+
->onlyMethods(['getId'])
213+
->getMock();
214+
215+
$mock->method('getId')->willReturn('test-request-id');
216216

217-
return $listPromptsRequest;
217+
return $mock;
218218
}
219219
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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\Registry;
15+
use Mcp\Exception\InvalidCursorException;
16+
use Mcp\Schema\Resource;
17+
use Mcp\Schema\Request\ListResourcesRequest;
18+
use Mcp\Schema\Result\ListResourcesResult;
19+
use Mcp\Server\RequestHandler\ListResourcesHandler;
20+
use PHPUnit\Framework\Attributes\TestDox;
21+
use PHPUnit\Framework\TestCase;
22+
23+
class ListResourcesHandlerTest extends TestCase
24+
{
25+
private Registry $registry;
26+
private ListResourcesHandler $handler;
27+
28+
protected function setUp(): void
29+
{
30+
$this->registry = new Registry();
31+
$this->handler = new ListResourcesHandler($this->registry, pageSize: 3); // Use small page size for testing
32+
}
33+
34+
#[TestDox('Returns first page when no cursor provided')]
35+
public function testReturnsFirstPageWhenNoCursorProvided(): void
36+
{
37+
// Arrange
38+
$this->addResourcesToRegistry(5);
39+
$request = $this->createListResourcesRequest();
40+
41+
// Act
42+
$response = $this->handler->handle($request);
43+
44+
// Assert
45+
$this->assertInstanceOf(ListResourcesResult::class, $response->result);
46+
$this->assertCount(3, $response->result->resources);
47+
$this->assertNotNull($response->result->nextCursor);
48+
49+
$this->assertEquals('resource://test/resource_0', $response->result->resources[0]->uri);
50+
$this->assertEquals('resource://test/resource_1', $response->result->resources[1]->uri);
51+
$this->assertEquals('resource://test/resource_2', $response->result->resources[2]->uri);
52+
}
53+
54+
#[TestDox('Returns paginated resources with cursor')]
55+
public function testReturnsPaginatedResourcesWithCursor(): void
56+
{
57+
// Arrange
58+
$this->addResourcesToRegistry(10);
59+
$request = $this->createListResourcesRequest(cursor: null);
60+
61+
// Act
62+
$response = $this->handler->handle($request);
63+
64+
// Assert
65+
$this->assertInstanceOf(ListResourcesResult::class, $response->result);
66+
$this->assertCount(3, $response->result->resources);
67+
$this->assertNotNull($response->result->nextCursor);
68+
69+
$this->assertEquals('resource://test/resource_0', $response->result->resources[0]->uri);
70+
$this->assertEquals('resource://test/resource_1', $response->result->resources[1]->uri);
71+
$this->assertEquals('resource://test/resource_2', $response->result->resources[2]->uri);
72+
}
73+
74+
#[TestDox('Returns second page with cursor')]
75+
public function testReturnsSecondPageWithCursor(): void
76+
{
77+
// Arrange
78+
$this->addResourcesToRegistry(10);
79+
$firstPageRequest = $this->createListResourcesRequest();
80+
$firstPageResponse = $this->handler->handle($firstPageRequest);
81+
82+
$secondPageRequest = $this->createListResourcesRequest(cursor: $firstPageResponse->result->nextCursor);
83+
84+
// Act
85+
$response = $this->handler->handle($secondPageRequest);
86+
87+
// Assert
88+
$this->assertInstanceOf(ListResourcesResult::class, $response->result);
89+
$this->assertCount(3, $response->result->resources);
90+
$this->assertNotNull($response->result->nextCursor);
91+
92+
$this->assertEquals('resource://test/resource_3', $response->result->resources[0]->uri);
93+
$this->assertEquals('resource://test/resource_4', $response->result->resources[1]->uri);
94+
$this->assertEquals('resource://test/resource_5', $response->result->resources[2]->uri);
95+
}
96+
97+
#[TestDox('Returns last page with null cursor')]
98+
public function testReturnsLastPageWithNullCursor(): void
99+
{
100+
// Arrange
101+
$this->addResourcesToRegistry(5);
102+
$firstPageRequest = $this->createListResourcesRequest();
103+
$firstPageResponse = $this->handler->handle($firstPageRequest);
104+
105+
$secondPageRequest = $this->createListResourcesRequest(cursor: $firstPageResponse->result->nextCursor);
106+
107+
// Act
108+
$response = $this->handler->handle($secondPageRequest);
109+
110+
// Assert
111+
$this->assertInstanceOf(ListResourcesResult::class, $response->result);
112+
$this->assertCount(2, $response->result->resources);
113+
$this->assertNull($response->result->nextCursor);
114+
115+
$this->assertEquals('resource://test/resource_3', $response->result->resources[0]->uri);
116+
$this->assertEquals('resource://test/resource_4', $response->result->resources[1]->uri);
117+
}
118+
119+
#[TestDox('Handles empty registry')]
120+
public function testHandlesEmptyRegistry(): void
121+
{
122+
// Arrange
123+
$request = $this->createListResourcesRequest();
124+
125+
// Act
126+
$response = $this->handler->handle($request);
127+
128+
// Assert
129+
$this->assertInstanceOf(ListResourcesResult::class, $response->result);
130+
$this->assertCount(0, $response->result->resources);
131+
$this->assertNull($response->result->nextCursor);
132+
}
133+
134+
#[TestDox('Throws exception for invalid cursor')]
135+
public function testThrowsExceptionForInvalidCursor(): void
136+
{
137+
// Arrange
138+
$this->addResourcesToRegistry(5);
139+
$request = $this->createListResourcesRequest(cursor: 'invalid-cursor');
140+
141+
// Assert
142+
$this->expectException(InvalidCursorException::class);
143+
144+
// Act
145+
$this->handler->handle($request);
146+
}
147+
148+
#[TestDox('Throws exception for cursor beyond bounds')]
149+
public function testThrowsExceptionForCursorBeyondBounds(): void
150+
{
151+
// Arrange
152+
$this->addResourcesToRegistry(5);
153+
$outOfBoundsCursor = base64_encode('100');
154+
$request = $this->createListResourcesRequest(cursor: $outOfBoundsCursor);
155+
156+
// Assert
157+
$this->expectException(InvalidCursorException::class);
158+
159+
// Act
160+
$this->handler->handle($request);
161+
}
162+
163+
#[TestDox('Handles cursor at exact boundary')]
164+
public function testHandlesCursorAtExactBoundary(): void
165+
{
166+
// Arrange
167+
$this->addResourcesToRegistry(6);
168+
$exactBoundaryCursor = base64_encode('6');
169+
$request = $this->createListResourcesRequest(cursor: $exactBoundaryCursor);
170+
171+
// Act
172+
$response = $this->handler->handle($request);
173+
174+
// Assert
175+
$this->assertInstanceOf(ListResourcesResult::class, $response->result);
176+
$this->assertCount(0, $response->result->resources);
177+
$this->assertNull($response->result->nextCursor);
178+
}
179+
180+
#[TestDox('Maintains stable cursors across calls')]
181+
public function testMaintainsStableCursorsAcrossCalls(): void
182+
{
183+
// Arrange
184+
$this->addResourcesToRegistry(10);
185+
186+
// Act
187+
$request = $this->createListResourcesRequest();
188+
$response1 = $this->handler->handle($request);
189+
$response2 = $this->handler->handle($request);
190+
191+
// Assert
192+
$this->assertEquals($response1->result->nextCursor, $response2->result->nextCursor);
193+
$this->assertEquals($response1->result->resources, $response2->result->resources);
194+
}
195+
196+
private function addResourcesToRegistry(int $count): void
197+
{
198+
for ($i = 0; $i < $count; $i++) {
199+
$resource = new Resource(
200+
uri: "resource://test/resource_$i",
201+
name: "resource_$i",
202+
description: "Test resource $i"
203+
);
204+
// Use a simple callable as handler
205+
$this->registry->registerResource($resource, fn() => null);
206+
}
207+
}
208+
209+
private function createListResourcesRequest(?string $cursor = null): ListResourcesRequest
210+
{
211+
$mock = $this->getMockBuilder(ListResourcesRequest::class)
212+
->setConstructorArgs([$cursor])
213+
->onlyMethods(['getId'])
214+
->getMock();
215+
216+
$mock->method('getId')->willReturn('test-request-id');
217+
218+
return $mock;
219+
}
220+
}

0 commit comments

Comments
 (0)