From 206aa9da527e56302c2784b0aec127fd7bc08002 Mon Sep 17 00:00:00 2001 From: Dev <94041411+DevkumarPatel@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:41:51 +0800 Subject: [PATCH] [Schema] Fix Implementation::fromArray rejecting falsy-but-valid name/version (#393) * [Schema] Fix Implementation::fromArray rejecting falsy-but-valid name/version `empty()` treats the string "0" as empty, so a valid clientInfo/serverInfo with `"version": "0"` (or `"name": "0"`) was rejected with an InvalidArgumentException. During initialize this aborted request parsing and surfaced as a misleading "A valid session id is REQUIRED" error. Replace the `empty()` guards with explicit isset/is_string/empty-string checks so genuinely missing, non-string, and empty values are still rejected while "0" is accepted. Adds ImplementationTest covering the regression and the existing validation paths. Fixes #392 Co-Authored-By: Claude Opus 4.8 (1M context) * [Schema] Suppress PHPStan argument.type on invalid-input tests --------- Co-authored-by: Claude Opus 4.8 (1M context) --- src/Schema/Implementation.php | 4 +- tests/Unit/Schema/ImplementationTest.php | 177 +++++++++++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Schema/ImplementationTest.php diff --git a/src/Schema/Implementation.php b/src/Schema/Implementation.php index 22d58a6..0216f05 100644 --- a/src/Schema/Implementation.php +++ b/src/Schema/Implementation.php @@ -45,10 +45,10 @@ class Implementation implements \JsonSerializable */ public static function fromArray(array $data): self { - if (empty($data['name']) || !\is_string($data['name'])) { + if (!isset($data['name']) || !\is_string($data['name']) || '' === $data['name']) { throw new InvalidArgumentException('Invalid or missing "name" in Implementation data.'); } - if (empty($data['version']) || !\is_string($data['version'])) { + if (!isset($data['version']) || !\is_string($data['version']) || '' === $data['version']) { throw new InvalidArgumentException('Invalid or missing "version" in Implementation data.'); } diff --git a/tests/Unit/Schema/ImplementationTest.php b/tests/Unit/Schema/ImplementationTest.php new file mode 100644 index 0000000..1e19dea --- /dev/null +++ b/tests/Unit/Schema/ImplementationTest.php @@ -0,0 +1,177 @@ +assertSame('app', $implementation->name); + $this->assertSame('dev', $implementation->version); + $this->assertNull($implementation->description); + $this->assertNull($implementation->icons); + $this->assertNull($implementation->websiteUrl); + } + + public function testFromArrayWithMinimalData(): void + { + $implementation = Implementation::fromArray([ + 'name' => 'my-client', + 'version' => '1.2.3', + ]); + + $this->assertSame('my-client', $implementation->name); + $this->assertSame('1.2.3', $implementation->version); + $this->assertNull($implementation->description); + $this->assertNull($implementation->icons); + $this->assertNull($implementation->websiteUrl); + } + + public function testFromArrayWithAllFields(): void + { + $implementation = Implementation::fromArray([ + 'name' => 'my-client', + 'version' => '1.2.3', + 'description' => 'A test client', + 'icons' => [['src' => 'https://example.com/icon.png']], + 'websiteUrl' => 'https://example.com', + ]); + + $this->assertSame('my-client', $implementation->name); + $this->assertSame('1.2.3', $implementation->version); + $this->assertSame('A test client', $implementation->description); + $this->assertIsArray($implementation->icons); + $this->assertCount(1, $implementation->icons); + $this->assertSame('https://example.com', $implementation->websiteUrl); + } + + /** + * Regression test for #392: falsy-but-valid version strings such as "0" + * were rejected because empty('0') === true. + */ + public function testFromArrayAcceptsZeroStringVersion(): void + { + $implementation = Implementation::fromArray([ + 'name' => 'my-client', + 'version' => '0', + ]); + + $this->assertSame('0', $implementation->version); + } + + /** + * Regression test for #392: a name of "0" is a valid string and must be accepted. + */ + public function testFromArrayAcceptsZeroStringName(): void + { + $implementation = Implementation::fromArray([ + 'name' => '0', + 'version' => '1.0.0', + ]); + + $this->assertSame('0', $implementation->name); + } + + public function testFromArrayThrowsOnMissingName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or missing "name" in Implementation data.'); + + /* @phpstan-ignore argument.type */ + Implementation::fromArray(['version' => '1.0.0']); + } + + public function testFromArrayThrowsOnEmptyName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or missing "name" in Implementation data.'); + + Implementation::fromArray(['name' => '', 'version' => '1.0.0']); + } + + public function testFromArrayThrowsOnNonStringName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or missing "name" in Implementation data.'); + + /* @phpstan-ignore argument.type */ + Implementation::fromArray(['name' => 123, 'version' => '1.0.0']); + } + + public function testFromArrayThrowsOnMissingVersion(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or missing "version" in Implementation data.'); + + /* @phpstan-ignore argument.type */ + Implementation::fromArray(['name' => 'my-client']); + } + + public function testFromArrayThrowsOnEmptyVersion(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or missing "version" in Implementation data.'); + + Implementation::fromArray(['name' => 'my-client', 'version' => '']); + } + + public function testFromArrayThrowsOnNonStringVersion(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or missing "version" in Implementation data.'); + + /* @phpstan-ignore argument.type */ + Implementation::fromArray(['name' => 'my-client', 'version' => 1]); + } + + public function testFromArrayThrowsOnNonArrayIcons(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid "icons" in Implementation data; expected an array.'); + + /* @phpstan-ignore argument.type */ + Implementation::fromArray(['name' => 'my-client', 'version' => '1.0.0', 'icons' => 'nope']); + } + + public function testJsonSerializeRoundTrip(): void + { + $implementation = Implementation::fromArray([ + 'name' => 'my-client', + 'version' => '0', + 'description' => 'A test client', + 'websiteUrl' => 'https://example.com', + ]); + + $this->assertSame([ + 'name' => 'my-client', + 'version' => '0', + 'description' => 'A test client', + 'websiteUrl' => 'https://example.com', + ], $implementation->jsonSerialize()); + } + + public function testJsonSerializeOmitsNullOptionalFields(): void + { + $implementation = new Implementation('my-client', '1.0.0'); + + $this->assertSame([ + 'name' => 'my-client', + 'version' => '1.0.0', + ], $implementation->jsonSerialize()); + } +}