Skip to content

Commit e98dad8

Browse files
author
klapaudius
committed
Add comprehensive tests for SseTransport's message handling
This commit introduces various tests for the `SseTransport` class, covering `receive`, `pushMessage`, `processMessage`, and error handling behavior. It ensures proper validation of adapter interactions, message flow, and logging of exceptions to enhance test coverage and reliability.
1 parent cb2a5b3 commit e98dad8

File tree

1 file changed

+305
-16
lines changed

1 file changed

+305
-16
lines changed

tests/Transports/SseTransportTest.php

Lines changed: 305 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use KLP\KlpMcpServer\Transports\SseAdapters\SseAdapterInterface;
66
use KLP\KlpMcpServer\Transports\SseTransport;
7+
use KLP\KlpMcpServer\Transports\SseTransportException;
8+
use PHPUnit\Framework\Attributes\CoversClass;
79
use PHPUnit\Framework\Attributes\Small;
810
use PHPUnit\Framework\MockObject\MockObject;
911
use PHPUnit\Framework\TestCase;
@@ -74,7 +76,7 @@ public function test_initialize_generates_client_id_and_sends_endpoint(): void
7476
/**
7577
* Test that initialize does not overwrite an existing client ID.
7678
*/
77-
public function test_initialize_d_does_not_overwrite_existing_client_id(): void
79+
public function test_initialize_does_not_overwrite_existing_client_id(): void
7880
{
7981
// Arrange
8082
$existingClientId = 'predefined-client-id';
@@ -125,21 +127,6 @@ public function test_send_array_message(): void
125127
);
126128
}
127129

128-
private function getProtectedProperty(SseTransport $instance, string $propertyName)
129-
{
130-
$reflection = new \ReflectionClass($instance);
131-
$prop = $reflection->getProperty($propertyName);
132-
133-
return $prop->getValue($instance);
134-
}
135-
136-
private function setProtectedProperty(SseTransport $instance, string $propertyName, string $propertyValue): void
137-
{
138-
$reflection = new \ReflectionClass($instance);
139-
$prop = $reflection->getProperty($propertyName);
140-
$prop->setValue($instance, $propertyValue);
141-
}
142-
143130
/**
144131
* Test that the start() method sets the connected flag to true and initializes the transport.
145132
*/
@@ -308,4 +295,306 @@ public function test_close_logs_exceptions_in_handlers_or_cleanup(): void
308295
ob_end_clean();
309296
}
310297
}
298+
299+
/**
300+
* Test that `receive` returns an empty array when no adapter is configured.
301+
*/
302+
public function test_receive_returns_empty_array_without_adapter(): void
303+
{
304+
// Act
305+
$result = $this->instance->receive();
306+
307+
// Assert
308+
$this->assertEquals([], $result, 'Expected receive to return an empty array when no adapter is set.');
309+
}
310+
311+
/**
312+
* Test that `receive` logs an info message when no adapter is configured.
313+
*/
314+
public function test_receive_logs_info_when_no_adapter(): void
315+
{
316+
// Arrange
317+
$this->loggerMock->expects($this->once())
318+
->method('info')
319+
->with('SSE Transport::receive called but no adapter is configured.');
320+
321+
// Act
322+
$this->instance->receive();
323+
}
324+
325+
/**
326+
* Test that `receive` returns received messages when an adapter and client ID are set.
327+
*/
328+
public function test_receive_returns_adapter_messages(): void
329+
{
330+
// Arrange
331+
$this->setProtectedProperty($this->instance, 'connected', true);
332+
$this->setProtectedProperty($this->instance, 'clientId', 'test-client-id');
333+
$adapterMock = $this->createMock(SseAdapterInterface::class);
334+
$this->instance->setAdapter($adapterMock);
335+
336+
$messages = [['type' => 'test', 'data' => 'value']];
337+
$adapterMock->expects($this->once())
338+
->method('receiveMessages')
339+
->with('test-client-id')
340+
->willReturn($messages);
341+
342+
// Act
343+
$result = $this->instance->receive();
344+
345+
// Assert
346+
$this->assertEquals($messages, $result, 'Expected received messages to match the adapter output.');
347+
}
348+
349+
/**
350+
* Test that `receive` calls the adapter's `receiveMessages` method with the correct client ID.
351+
*/
352+
public function test_receive_calls_adapter_receiveMessages_with_correct_client_id(): void
353+
{
354+
// Arrange
355+
$this->setProtectedProperty($this->instance, 'connected', true);
356+
$this->setProtectedProperty($this->instance, 'clientId', 'test-client-id');
357+
$adapterMock = $this->createMock(SseAdapterInterface::class);
358+
$this->instance->setAdapter($adapterMock);
359+
360+
$adapterMock->expects($this->once())
361+
->method('receiveMessages')
362+
->with('test-client-id');
363+
364+
// Act
365+
$this->instance->receive();
366+
}
367+
368+
/**
369+
* Test that `receive` triggers error handlers if the adapter throws an exception.
370+
*/
371+
public function test_receive_triggers_error_handlers_on_adapter_exception(): void
372+
{
373+
// Arrange
374+
$this->setProtectedProperty($this->instance, 'connected', true);
375+
$this->setProtectedProperty($this->instance, 'clientId', 'test-client-id');
376+
$adapterMock = $this->createMock(SseAdapterInterface::class);
377+
$this->instance->setAdapter($adapterMock);
378+
379+
$adapterMock->expects($this->once())
380+
->method('receiveMessages')
381+
->willThrowException(new \Exception('Adapter Exception'));
382+
383+
$errorTriggered = false;
384+
$this->instance->onError(function (string $error) use (&$errorTriggered) {
385+
$errorTriggered = true;
386+
$this->assertStringContainsString('Adapter Exception', $error);
387+
});
388+
389+
// Act
390+
$this->instance->receive();
391+
392+
// Assert
393+
$this->assertTrue($errorTriggered, 'Error handlers were not triggered on adapter exception.');
394+
}
395+
396+
/**
397+
* Test that `pushMessage` calls the adapter's `pushMessage` method with correct parameters.
398+
*/
399+
public function test_push_message_calls_adapter_correctly(): void
400+
{
401+
// Arrange
402+
$clientId = 'test-client-id';
403+
$message = ['key' => 'value'];
404+
$expectedMessage = json_encode($message);
405+
406+
$adapterMock = $this->createMock(SseAdapterInterface::class);
407+
$adapterMock->expects($this->once())
408+
->method('pushMessage')
409+
->with($clientId, $expectedMessage);
410+
411+
$this->instance->setAdapter($adapterMock);
412+
413+
// Act
414+
$this->instance->pushMessage($clientId, $message);
415+
}
416+
417+
/**
418+
* Test that `pushMessage` throws an exception if adapter is not set.
419+
*/
420+
public function test_push_message_throws_exception_without_adapter(): void
421+
{
422+
// Expect exception
423+
$this->expectException(SseTransportException::class);
424+
$this->expectExceptionMessage('Cannot push message: SSE Adapter is not configured.');
425+
426+
// Act
427+
$this->instance->pushMessage('client-id', ['key' => 'value']);
428+
}
429+
430+
/**
431+
* Test that `pushMessage` throws an exception if JSON encoding fails.
432+
*/
433+
public function test_push_message_throws_exception_on_json_error(): void
434+
{
435+
// Arrange
436+
$clientId = 'test-client-id';
437+
$invalidMessage = "\xB1\x31"; // Invalid UTF-8 sequence
438+
439+
$adapterMock = $this->createMock(SseAdapterInterface::class);
440+
$this->instance->setAdapter($adapterMock);
441+
442+
// Expect exception
443+
$this->expectException(SseTransportException::class);
444+
$this->expectExceptionMessage('Failed to JSON encode message for pushing');
445+
446+
// Act
447+
$this->instance->pushMessage($clientId, [$invalidMessage]);
448+
}
449+
450+
/**
451+
* Test that `processMessage` calls all registered message handlers with the correct parameters.
452+
*/
453+
public function test_process_message_invokes_handlers(): void
454+
{
455+
// Arrange
456+
$messageHandlers = [];
457+
$this->instance->onMessage(function (string $clientId, array $message) use (&$messageHandlers) {
458+
$messageHandlers[] = ['clientId' => $clientId, 'message' => $message];
459+
});
460+
461+
$clientId = 'test-client-id';
462+
$message = ['key' => 'value'];
463+
464+
// Act
465+
$this->instance->processMessage($clientId, $message);
466+
467+
// Assert
468+
$this->assertCount(1, $messageHandlers);
469+
$this->assertSame($clientId, $messageHandlers[0]['clientId']);
470+
$this->assertSame($message, $messageHandlers[0]['message']);
471+
}
472+
473+
/**
474+
* Test that `triggerError` invokes all registered error handlers with the correct message.
475+
*/
476+
public function test_trigger_error_invokes_registered_handlers(): void
477+
{
478+
// Arrange
479+
$errorTriggered = [];
480+
$this->instance->onError(function (string $error) use (&$errorTriggered) {
481+
$errorTriggered[] = $error;
482+
});
483+
$this->instance->onError(function (string $error) use (&$errorTriggered) {
484+
$errorTriggered[] = "Handler 2: $error";
485+
});
486+
487+
$message = 'Test Error Message';
488+
489+
// Act
490+
$this->invokeProtectedMethod($this->instance, 'triggerError', [$message]);
491+
492+
// Assert
493+
$this->assertCount(2, $errorTriggered);
494+
$this->assertSame($message, $errorTriggered[0]);
495+
$this->assertSame("Handler 2: $message", $errorTriggered[1]);
496+
}
497+
498+
/**
499+
* Test that `triggerError` logs exceptions thrown by error handlers.
500+
*/
501+
public function test_trigger_error_logs_exceptions_in_handlers(): void
502+
{
503+
// Arrange
504+
$message = 'Error Message';
505+
$invocations = [
506+
'SSE Transport error: Error Message',
507+
'Error in SSE error handler itself: Test Exception',
508+
];
509+
$this->loggerMock->expects($matcher = $this->exactly(2))
510+
->method('error')
511+
->with($this->callback(function ($arg) use (&$invocations, $matcher) {
512+
$this->assertEquals($arg, $invocations[$matcher->numberOfInvocations() - 1]);
513+
514+
return true;
515+
}));
516+
517+
$this->instance->onError(function () {
518+
throw new \Exception('Test Exception');
519+
});
520+
521+
$handlerCalled = false;
522+
$this->instance->onError(function () use (&$handlerCalled) {
523+
$handlerCalled = true;
524+
});
525+
526+
527+
// Act
528+
$this->invokeProtectedMethod($this->instance, 'triggerError', [$message]);
529+
530+
// Assert
531+
$this->assertTrue($handlerCalled, 'The second handler was not called after the exception.');
532+
}
533+
534+
/**
535+
* Test that `processMessage` logs exceptions in message handlers and continues execution.
536+
*/
537+
public function test_process_message_logs_exceptions_in_handlers(): void
538+
{
539+
// Arrange
540+
$this->loggerMock->expects($this->once())
541+
->method('error')
542+
->with($this->stringContains('Error processing SSE message via handler: Test Exception'));
543+
544+
$this->instance->onMessage(function () {
545+
throw new \Exception('Test Exception');
546+
});
547+
548+
$handlerCalled = false;
549+
$this->instance->onMessage(function () use (&$handlerCalled) {
550+
$handlerCalled = true;
551+
});
552+
553+
$clientId = 'test-client-id';
554+
$message = ['key' => 'value'];
555+
556+
// Act
557+
$this->instance->processMessage($clientId, $message);
558+
559+
// Assert
560+
$this->assertTrue($handlerCalled, 'The second handler was not called after the exception.');
561+
}
562+
563+
/**
564+
* Test that `processMessage` does nothing when no handlers are registered.
565+
*/
566+
public function test_process_message_does_nothing_without_handlers(): void
567+
{
568+
// Arrange
569+
$clientId = 'test-client-id';
570+
$message = ['key' => 'value'];
571+
572+
// Assert no exception is thrown or output generated
573+
$this->expectNotToPerformAssertions();
574+
575+
// Act
576+
$this->instance->processMessage($clientId, $message);
577+
}
578+
579+
private function invokeProtectedMethod(SseTransport $instance, string $string, array $array)
580+
{
581+
$reflection = new \ReflectionClass($instance);
582+
$method = $reflection->getMethod($string);
583+
$method->invokeArgs($instance, $array);
584+
}
585+
586+
private function getProtectedProperty(SseTransport $instance, string $propertyName)
587+
{
588+
$reflection = new \ReflectionClass($instance);
589+
$prop = $reflection->getProperty($propertyName);
590+
591+
return $prop->getValue($instance);
592+
}
593+
594+
private function setProtectedProperty(SseTransport $instance, string $propertyName, string $propertyValue): void
595+
{
596+
$reflection = new \ReflectionClass($instance);
597+
$prop = $reflection->getProperty($propertyName);
598+
$prop->setValue($instance, $propertyValue);
599+
}
311600
}

0 commit comments

Comments
 (0)