Skip to content

Commit 820452d

Browse files
mowens3claude
andcommitted
Add tests for PdoStorage edge cases for 100% coverage
- Test getAll() with non-array rows (defensive code path) - Test getAll() with non-string key_id/data values - Test get() with non-string data column Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e4fdbda commit 820452d

File tree

2 files changed

+100
-1
lines changed

2 files changed

+100
-1
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.4] - 2026-01-09
9+
10+
### Added
11+
- Tests for edge cases in PdoStorage for 100% code coverage
12+
- Non-array rows from PDOStatement::fetch
13+
- Non-string key_id/data values
14+
- Non-string data column in get()
15+
816
## [Unreleased]
917

1018
### Added
1119
- PHPStan level 9 strict type checking
1220
- Infection PHP mutation testing
1321
- PHPBench performance benchmarks
1422
- Example integrations (Slim 4, standalone, CLI key manager)
15-
- Comprehensive test coverage (167 tests)
1623

1724
### Changed
1825
- Changed `readonly class` to `readonly` properties for PHP 8.1 compatibility

tests/ApiKey/Storage/PdoStorageTest.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)