feat(server): add server lifecycle events (#175)
Signed-off-by: Edouard Courty <edouard.courty2@gmail.com>
This commit is contained in:
parent
04a9a16be2
commit
158e64627f
1 changed files with 1008 additions and 0 deletions
|
|
@ -11,9 +11,16 @@
|
|||
|
||||
namespace Mcp\Tests\Unit\Server;
|
||||
|
||||
use Mcp\Event\ErrorEvent;
|
||||
use Mcp\Event\NotificationEvent;
|
||||
use Mcp\Event\RequestEvent;
|
||||
use Mcp\Event\ResponseEvent;
|
||||
use Mcp\JsonRpc\MessageFactory;
|
||||
use Mcp\Schema\Enum\LoggingLevel;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Notification\LoggingMessageNotification;
|
||||
use Mcp\Schema\Request\CallToolRequest;
|
||||
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
|
||||
use Mcp\Server\Handler\Request\RequestHandlerInterface;
|
||||
use Mcp\Server\Protocol;
|
||||
|
|
@ -24,6 +31,7 @@ use Mcp\Server\Transport\TransportInterface;
|
|||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
final class ProtocolTest extends TestCase
|
||||
|
|
@ -719,4 +727,620 @@ final class ProtocolTest extends TestCase
|
|||
|
||||
$protocol->destroySession($sessionId);
|
||||
}
|
||||
|
||||
#[TestDox('RequestEvent is dispatched when a request is received')]
|
||||
public function testRequestEventIsDispatched(): void
|
||||
{
|
||||
$capturedEvents = [];
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function ($event) use (&$capturedEvents) {
|
||||
$capturedEvents[] = $event;
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$handler = $this->createMock(RequestHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler->method('handle')->willReturn(new Response(1, ['result' => 'success']));
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getId')->willReturn(Uuid::v4());
|
||||
$session->method('get')->willReturn([]);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [$handler],
|
||||
notificationHandlers: [],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "ping", "id": 1}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
// Should have RequestEvent (and ResponseEvent)
|
||||
$this->assertGreaterThanOrEqual(1, \count($capturedEvents));
|
||||
$this->assertInstanceOf(RequestEvent::class, $capturedEvents[0]);
|
||||
$this->assertSame($session, $capturedEvents[0]->getSession());
|
||||
$this->assertSame('ping', $capturedEvents[0]->getMethod());
|
||||
}
|
||||
|
||||
#[TestDox('RequestEvent modification is used by handler')]
|
||||
public function testRequestEventModificationIsUsed(): void
|
||||
{
|
||||
$handlerReceivedRequest = null;
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function ($event) {
|
||||
if ($event instanceof RequestEvent) {
|
||||
// Simulate a listener modifying the request
|
||||
$originalRequest = $event->getRequest();
|
||||
|
||||
// Create a modified CallToolRequest with different name but same ID
|
||||
$modifiedRequest = CallToolRequest::fromArray([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $originalRequest->getId(),
|
||||
'method' => 'tools/call',
|
||||
'params' => [
|
||||
'name' => 'modified_tool',
|
||||
'arguments' => ['modified' => true],
|
||||
],
|
||||
]);
|
||||
|
||||
$event->setRequest($modifiedRequest);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$handler = $this->createMock(RequestHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler
|
||||
->method('handle')
|
||||
->willReturnCallback(static function ($request) use (&$handlerReceivedRequest) {
|
||||
$handlerReceivedRequest = $request;
|
||||
|
||||
return new Response(1, ['result' => 'success']);
|
||||
});
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getId')->willReturn(Uuid::v4());
|
||||
$session->method('get')->willReturn([]);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [$handler],
|
||||
notificationHandlers: [],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "tools/call", "id": 1, "params": {"name": "original_tool", "arguments": {}}}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
// Verify the handler received the modified request
|
||||
$this->assertInstanceOf(CallToolRequest::class, $handlerReceivedRequest);
|
||||
|
||||
$this->assertSame('modified_tool', $handlerReceivedRequest->name);
|
||||
$this->assertSame(['modified' => true], $handlerReceivedRequest->arguments);
|
||||
}
|
||||
|
||||
#[TestDox('RequestEvent works with null EventDispatcher')]
|
||||
public function testRequestEventWithNullDispatcher(): void
|
||||
{
|
||||
$handler = $this->createMock(RequestHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler->method('handle')->willReturn(new Response(1, ['result' => 'success']));
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getId')->willReturn(Uuid::v4());
|
||||
$session->method('get')->willReturn([]);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [$handler],
|
||||
notificationHandlers: [],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: null,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "ping", "id": 1}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
// Should not crash - success
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
#[TestDox('ResponseEvent is dispatched when handler returns Response')]
|
||||
public function testResponseEventIsDispatched(): void
|
||||
{
|
||||
$capturedEvents = [];
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function ($event) use (&$capturedEvents) {
|
||||
$capturedEvents[] = $event;
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$handler = $this->createMock(RequestHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler->method('handle')->willReturn(new Response(1, ['result' => 'success']));
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getId')->willReturn(Uuid::v4());
|
||||
$session->method('get')->willReturn([]);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [$handler],
|
||||
notificationHandlers: [],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "ping", "id": 1}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
// Should have RequestEvent and ResponseEvent
|
||||
$this->assertCount(2, $capturedEvents);
|
||||
$this->assertInstanceOf(RequestEvent::class, $capturedEvents[0]);
|
||||
$this->assertInstanceOf(ResponseEvent::class, $capturedEvents[1]);
|
||||
|
||||
/** @var ResponseEvent $responseEvent */
|
||||
$responseEvent = $capturedEvents[1];
|
||||
$this->assertSame($session, $responseEvent->getSession());
|
||||
$this->assertSame('ping', $responseEvent->getMethod());
|
||||
$this->assertInstanceOf(Response::class, $responseEvent->getResponse());
|
||||
}
|
||||
|
||||
#[TestDox('ResponseEvent modification is used when sending')]
|
||||
public function testResponseEventModificationIsUsed(): void
|
||||
{
|
||||
$outgoingQueue = [];
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function ($event) {
|
||||
if ($event instanceof ResponseEvent) {
|
||||
// Simulate a listener modifying the response
|
||||
$modifiedResponse = new Response(
|
||||
$event->getResponse()->getId(),
|
||||
['result' => 'modified', 'original' => false]
|
||||
);
|
||||
$event->setResponse($modifiedResponse);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$handler = $this->createMock(RequestHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler->method('handle')->willReturn(new Response(1, ['result' => 'original']));
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getId')->willReturn(Uuid::v4());
|
||||
$session->method('get')->willReturn([]);
|
||||
$session
|
||||
->method('set')
|
||||
->willReturnCallback(static function ($key, $value) use (&$outgoingQueue) {
|
||||
if ('_mcp.outgoing_queue' === $key) {
|
||||
$outgoingQueue = $value;
|
||||
}
|
||||
});
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [$handler],
|
||||
notificationHandlers: [],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "ping", "id": 1}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
// Verify the MODIFIED response was queued
|
||||
$this->assertNotEmpty($outgoingQueue);
|
||||
$lastQueued = end($outgoingQueue);
|
||||
$this->assertIsArray($lastQueued);
|
||||
$this->assertArrayHasKey('message', $lastQueued);
|
||||
|
||||
$decoded = json_decode($lastQueued['message'], true);
|
||||
$this->assertSame('modified', $decoded['result']['result']);
|
||||
$this->assertFalse($decoded['result']['original']);
|
||||
}
|
||||
|
||||
#[TestDox('ErrorEvent is dispatched when handler returns Error')]
|
||||
public function testErrorEventIsDispatchedForErrorResult(): void
|
||||
{
|
||||
$capturedEvents = [];
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function ($event) use (&$capturedEvents) {
|
||||
$capturedEvents[] = $event;
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$handler = $this->createMock(RequestHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler->method('handle')->willReturn(Error::forInternalError('test error', 1));
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getId')->willReturn(Uuid::v4());
|
||||
$session->method('get')->willReturn([]);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [$handler],
|
||||
notificationHandlers: [],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "ping", "id": 1}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
// Should have RequestEvent and ErrorEvent
|
||||
$this->assertCount(2, $capturedEvents);
|
||||
$this->assertInstanceOf(RequestEvent::class, $capturedEvents[0]);
|
||||
$this->assertInstanceOf(ErrorEvent::class, $capturedEvents[1]);
|
||||
|
||||
/** @var ErrorEvent $errorEvent */
|
||||
$errorEvent = $capturedEvents[1];
|
||||
$this->assertSame($session, $errorEvent->getSession());
|
||||
$this->assertNull($errorEvent->getThrowable());
|
||||
$this->assertInstanceOf(Error::class, $errorEvent->getError());
|
||||
}
|
||||
|
||||
#[TestDox('ErrorEvent is dispatched on InvalidArgumentException')]
|
||||
public function testErrorEventIsDispatchedForInvalidArgument(): void
|
||||
{
|
||||
$capturedEvents = [];
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function ($event) use (&$capturedEvents) {
|
||||
$capturedEvents[] = $event;
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$handler = $this->createMock(RequestHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler->method('handle')->willThrowException(new \InvalidArgumentException('Invalid param'));
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getId')->willReturn(Uuid::v4());
|
||||
$session->method('get')->willReturn([]);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [$handler],
|
||||
notificationHandlers: [],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "ping", "id": 1}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
// Should have RequestEvent and ErrorEvent
|
||||
$this->assertCount(2, $capturedEvents);
|
||||
$this->assertInstanceOf(RequestEvent::class, $capturedEvents[0]);
|
||||
$this->assertInstanceOf(ErrorEvent::class, $capturedEvents[1]);
|
||||
|
||||
/** @var ErrorEvent $errorEvent */
|
||||
$errorEvent = $capturedEvents[1];
|
||||
$this->assertInstanceOf(\InvalidArgumentException::class, $errorEvent->getThrowable());
|
||||
$this->assertSame('Invalid param', $errorEvent->getThrowable()->getMessage());
|
||||
}
|
||||
|
||||
#[TestDox('ErrorEvent is dispatched on generic Throwable')]
|
||||
public function testErrorEventIsDispatchedForGenericException(): void
|
||||
{
|
||||
$capturedEvents = [];
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function ($event) use (&$capturedEvents) {
|
||||
$capturedEvents[] = $event;
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$handler = $this->createMock(RequestHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler->method('handle')->willThrowException(new \RuntimeException('Runtime error'));
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getId')->willReturn(Uuid::v4());
|
||||
$session->method('get')->willReturn([]);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [$handler],
|
||||
notificationHandlers: [],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "ping", "id": 1}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
// Should have RequestEvent and ErrorEvent
|
||||
$this->assertCount(2, $capturedEvents);
|
||||
$this->assertInstanceOf(RequestEvent::class, $capturedEvents[0]);
|
||||
$this->assertInstanceOf(ErrorEvent::class, $capturedEvents[1]);
|
||||
|
||||
/** @var ErrorEvent $errorEvent */
|
||||
$errorEvent = $capturedEvents[1];
|
||||
$this->assertInstanceOf(\RuntimeException::class, $errorEvent->getThrowable());
|
||||
$this->assertSame('Runtime error', $errorEvent->getThrowable()->getMessage());
|
||||
}
|
||||
|
||||
#[TestDox('ErrorEvent is dispatched when no handler found')]
|
||||
public function testErrorEventIsDispatchedForMethodNotFound(): void
|
||||
{
|
||||
$capturedEvents = [];
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function ($event) use (&$capturedEvents) {
|
||||
$capturedEvents[] = $event;
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getId')->willReturn(Uuid::v4());
|
||||
$session->method('get')->willReturn([]);
|
||||
$session->expects($this->once())->method('save');
|
||||
$session->expects($this->atLeastOnce())->method('set');
|
||||
|
||||
$this->sessionFactory->method('create')->willReturn($session); // create() for initialize
|
||||
$this->sessionStore->method('exists')->willReturn(false); // No existing session
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [], // No handlers
|
||||
notificationHandlers: [],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0"}}}',
|
||||
null // Initialize must not have sessionId
|
||||
);
|
||||
|
||||
// Should have RequestEvent and ErrorEvent
|
||||
$this->assertCount(2, $capturedEvents);
|
||||
$this->assertInstanceOf(RequestEvent::class, $capturedEvents[0]);
|
||||
$this->assertInstanceOf(ErrorEvent::class, $capturedEvents[1]);
|
||||
|
||||
/** @var ErrorEvent $errorEvent */
|
||||
$errorEvent = $capturedEvents[1];
|
||||
$this->assertNull($errorEvent->getThrowable());
|
||||
$this->assertInstanceOf(Error::class, $errorEvent->getError());
|
||||
}
|
||||
|
||||
#[TestDox('NotificationEvent is dispatched when notification received')]
|
||||
public function testNotificationEventIsDispatched(): void
|
||||
{
|
||||
$capturedEvent = null;
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->expects($this->once())
|
||||
->method('dispatch')
|
||||
->with($this->callback(static function ($event) use (&$capturedEvent) {
|
||||
$capturedEvent = $event;
|
||||
|
||||
return $event instanceof NotificationEvent;
|
||||
}))
|
||||
->willReturnArgument(0);
|
||||
|
||||
$handler = $this->createMock(NotificationHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler->expects($this->once())->method('handle');
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [],
|
||||
notificationHandlers: [$handler],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "notifications/initialized"}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
$this->assertNotNull($capturedEvent);
|
||||
$this->assertInstanceOf(NotificationEvent::class, $capturedEvent);
|
||||
$this->assertSame($session, $capturedEvent->getSession());
|
||||
$this->assertSame('notifications/initialized', $capturedEvent->getMethod());
|
||||
}
|
||||
|
||||
#[TestDox('NotificationEvent modification is used by handlers')]
|
||||
public function testNotificationEventModificationIsUsed(): void
|
||||
{
|
||||
$handlerReceivedNotification = null;
|
||||
|
||||
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$eventDispatcher
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function ($event) {
|
||||
if ($event instanceof NotificationEvent) {
|
||||
// Simulate a listener modifying the notification
|
||||
$modifiedNotification = LoggingMessageNotification::fromArray([
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => 'notifications/message',
|
||||
'params' => [
|
||||
'level' => 'error',
|
||||
'data' => 'modified message',
|
||||
'logger' => 'modified_logger',
|
||||
],
|
||||
]);
|
||||
|
||||
$event->setNotification($modifiedNotification);
|
||||
}
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
$handler = $this->createMock(NotificationHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler
|
||||
->method('handle')
|
||||
->willReturnCallback(static function ($notification) use (&$handlerReceivedNotification) {
|
||||
$handlerReceivedNotification = $notification;
|
||||
});
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [],
|
||||
notificationHandlers: [$handler],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: $eventDispatcher,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "notifications/message", "params": {"level": "info", "data": "original message"}}',
|
||||
$sessionId
|
||||
);
|
||||
|
||||
// Verify the handler received the MODIFIED notification
|
||||
$this->assertInstanceOf(LoggingMessageNotification::class, $handlerReceivedNotification);
|
||||
$this->assertSame(LoggingLevel::Error, $handlerReceivedNotification->level);
|
||||
$this->assertSame('modified message', $handlerReceivedNotification->data);
|
||||
$this->assertSame('modified_logger', $handlerReceivedNotification->logger);
|
||||
}
|
||||
|
||||
#[TestDox('NotificationEvent works with null EventDispatcher')]
|
||||
public function testNotificationEventWithNullDispatcher(): void
|
||||
{
|
||||
$handler = $this->createMock(NotificationHandlerInterface::class);
|
||||
$handler->method('supports')->willReturn(true);
|
||||
$handler->expects($this->once())->method('handle');
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
|
||||
$this->sessionFactory->method('createWithId')->willReturn($session);
|
||||
$this->sessionStore->method('exists')->willReturn(true);
|
||||
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: [],
|
||||
notificationHandlers: [$handler],
|
||||
messageFactory: MessageFactory::make(),
|
||||
sessionFactory: $this->sessionFactory,
|
||||
sessionStore: $this->sessionStore,
|
||||
eventDispatcher: null,
|
||||
);
|
||||
|
||||
$sessionId = Uuid::v4();
|
||||
$protocol->processInput(
|
||||
$this->transport,
|
||||
'{"jsonrpc": "2.0", "method": "notifications/initialized"}',
|
||||
$sessionId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue