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