@@ -176,6 +176,24 @@ public function testGetReturnsNullForInvalidJson(): void
176176 $ this ->assertNull ($ result );
177177 }
178178
179+ public function testGetReturnsNullForNonStringData (): void
180+ {
181+ // Mock PDOStatement to return a row with non-string data
182+ $ mockStmt = $ this ->createMock (\PDOStatement::class);
183+ $ mockStmt ->method ('execute ' )->willReturn (true );
184+ $ mockStmt ->method ('fetch ' )
185+ ->willReturn (['data ' => 123 ]); // Integer instead of string
186+
187+ $ mockPdo = $ this ->createMock (PDO ::class);
188+ $ mockPdo ->method ('getAttribute ' )->willReturn ('sqlite ' );
189+ $ mockPdo ->method ('prepare ' )->willReturn ($ mockStmt );
190+
191+ $ storage = new PdoStorage ($ mockPdo , 'test_keys ' );
192+ $ result = $ storage ->get ('test ' );
193+
194+ $ this ->assertNull ($ result );
195+ }
196+
179197 public function testGetAllSkipsInvalidJsonRows (): void
180198 {
181199 // Insert one valid and one invalid row
@@ -261,4 +279,78 @@ public function testSetRethrowsNonConflictException(): void
261279 $ this ->expectExceptionMessage ('Connection lost ' );
262280 $ storage ->set ('key1 ' , ['label ' => 'Test ' ]);
263281 }
282+
283+ public function testGetAllSkipsNonArrayRows (): void
284+ {
285+ // Mock PDOStatement to return a non-array truthy value (edge case)
286+ $ callCount = 0 ;
287+ $ mockStmt = $ this ->createMock (\PDOStatement::class);
288+ $ mockStmt ->method ('execute ' )->willReturn (true );
289+ $ mockStmt ->method ('fetch ' )
290+ ->willReturnCallback (function () use (&$ callCount ) {
291+ $ callCount ++;
292+ if ($ callCount === 1 ) {
293+ // Return a stdClass instead of array (truthy but not array)
294+ return (object ) ['key_id ' => 'test ' , 'data ' => '{} ' ];
295+ }
296+ if ($ callCount === 2 ) {
297+ // Return a valid array row
298+ return ['key_id ' => 'valid ' , 'data ' => '{"label": "Valid"} ' ];
299+ }
300+ return false ;
301+ });
302+
303+ $ mockPdo = $ this ->createMock (PDO ::class);
304+ $ mockPdo ->method ('getAttribute ' )->willReturn ('sqlite ' );
305+ $ mockPdo ->method ('prepare ' )->willReturn ($ mockStmt );
306+
307+ $ storage = new PdoStorage ($ mockPdo , 'test_keys ' );
308+ $ keys = $ storage ->getAll ();
309+
310+ // Should skip the object row and only include the valid array row
311+ $ this ->assertCount (1 , $ keys );
312+ $ this ->assertArrayHasKey ('valid ' , $ keys );
313+ }
314+
315+ public function testGetAllSkipsRowsWithNonStringKeyId (): void
316+ {
317+ // Insert a row with null key_id (edge case)
318+ $ stmt = $ this ->pdo ->prepare (
319+ "INSERT INTO mcp_api_keys (key_id, data) VALUES (:key_id, :data) "
320+ );
321+ $ stmt ->execute (['key_id ' => 'valid ' , 'data ' => '{"label": "Valid"} ' ]);
322+
323+ // Create a mock that returns a row with integer key_id
324+ $ callCount = 0 ;
325+ $ mockStmt = $ this ->createMock (\PDOStatement::class);
326+ $ mockStmt ->method ('execute ' )->willReturn (true );
327+ $ mockStmt ->method ('fetch ' )
328+ ->willReturnCallback (function () use (&$ callCount ) {
329+ $ callCount ++;
330+ if ($ callCount === 1 ) {
331+ // Return row with integer key_id
332+ return ['key_id ' => 123 , 'data ' => '{"label": "IntKey"} ' ];
333+ }
334+ if ($ callCount === 2 ) {
335+ // Return row with null data
336+ return ['key_id ' => 'nulldata ' , 'data ' => null ];
337+ }
338+ if ($ callCount === 3 ) {
339+ // Return valid row
340+ return ['key_id ' => 'valid ' , 'data ' => '{"label": "Valid"} ' ];
341+ }
342+ return false ;
343+ });
344+
345+ $ mockPdo = $ this ->createMock (PDO ::class);
346+ $ mockPdo ->method ('getAttribute ' )->willReturn ('sqlite ' );
347+ $ mockPdo ->method ('prepare ' )->willReturn ($ mockStmt );
348+
349+ $ storage = new PdoStorage ($ mockPdo , 'test_keys ' );
350+ $ keys = $ storage ->getAll ();
351+
352+ // Should skip rows with non-string key_id or null data
353+ $ this ->assertCount (1 , $ keys );
354+ $ this ->assertArrayHasKey ('valid ' , $ keys );
355+ }
264356}
0 commit comments