diff --git a/src/Server/Session/FileSessionStore.php b/src/Server/Session/FileSessionStore.php index aa99aba8..9716e3d6 100644 --- a/src/Server/Session/FileSessionStore.php +++ b/src/Server/Session/FileSessionStore.php @@ -11,6 +11,7 @@ namespace Mcp\Server\Session; +use Mcp\Exception\RuntimeException; use Mcp\Server\NativeClock; use Psr\Clock\ClockInterface; use Symfony\Component\Uid\Uuid; @@ -31,7 +32,7 @@ public function __construct( } if (!is_dir($this->directory) || !is_writable($this->directory)) { - throw new \RuntimeException(\sprintf('Session directory "%s" is not writable.', $this->directory)); + throw new RuntimeException(\sprintf('Session directory "%s" is not writable.', $this->directory)); } } diff --git a/tests/Unit/Server/Session/FileSessionStoreTest.php b/tests/Unit/Server/Session/FileSessionStoreTest.php new file mode 100644 index 00000000..1e0caa75 --- /dev/null +++ b/tests/Unit/Server/Session/FileSessionStoreTest.php @@ -0,0 +1,90 @@ +directory = sys_get_temp_dir().'/mcp-file-session-store-'.bin2hex(random_bytes(6)); + } + + protected function tearDown(): void + { + if (!is_dir($this->directory)) { + return; + } + + @chmod($this->directory, 0775); + + foreach (glob($this->directory.'/*') ?: [] as $file) { + @unlink($file); + } + + @rmdir($this->directory); + } + + #[TestDox('creates the session directory when it does not exist yet')] + public function testCreatesMissingDirectory(): void + { + new FileSessionStore($this->directory); + + $this->assertDirectoryExists($this->directory); + } + + #[TestDox('round-trips a session payload through the filesystem')] + public function testWriteThenRead(): void + { + $store = new FileSessionStore($this->directory); + $id = new UuidV4(); + + $store->write($id, 'payload'); + + $this->assertTrue($store->exists($id)); + $this->assertSame('payload', $store->read($id)); + } + + #[TestDox('rejects an unwritable directory with the SDK\'s own exception')] + public function testUnwritableDirectoryThrowsPackageException(): void + { + mkdir($this->directory, 0775, true); + chmod($this->directory, 0555); + clearstatcache(true, $this->directory); + + if (is_writable($this->directory)) { + $this->markTestSkipped('Permission bits do not restrict writes here (running as root, or a filesystem that ignores them).'); + } + + // The store's only throw must stay inside the package hierarchy, so a + // consumer catching ExceptionInterface sees it rather than a bare SPL + // RuntimeException escaping the SDK. + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(\sprintf('Session directory "%s" is not writable.', $this->directory)); + + try { + new FileSessionStore($this->directory); + } catch (RuntimeException $e) { + $this->assertInstanceOf(ExceptionInterface::class, $e); + + throw $e; + } + } +}