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