php-sdk/tests/Unit/Schema/ImplementationTest.php
Dev 206aa9da52
[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) <noreply@anthropic.com>

* [Schema] Suppress PHPStan argument.type on invalid-input tests

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 23:41:51 +02:00

177 lines
5.9 KiB
PHP

<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mcp\Tests\Unit\Schema;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\Implementation;
use PHPUnit\Framework\TestCase;
final class ImplementationTest extends TestCase
{
public function testConstructorDefaults(): void
{
$implementation = new Implementation();
$this->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());
}
}