135 lines
4.1 KiB
PHP
135 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\BusProNet\Transport;
|
|
|
|
use App\BusProNet\Exception\ApiClientException;
|
|
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
|
use App\BusProNet\Transport\SocketTransport;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class SocketTransportTest extends TestCase
|
|
{
|
|
private ?SocketTransport $transport = null;
|
|
private LoggerInterface $logger;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->logger = $this->createStub(LoggerInterface::class);
|
|
}
|
|
|
|
private function transport(): SocketTransport
|
|
{
|
|
return $this->transport ??= new SocketTransport($this->logger, '127.0.0.1', [9000]);
|
|
}
|
|
|
|
public function testEmptyResponseThrowsImmediateConnectionCloseException(): void
|
|
{
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
|
|
$stream = fopen('php://memory', 'r+');
|
|
// Write nothing — stream is immediately at EOF
|
|
rewind($stream);
|
|
|
|
$this->setOperationStartTime();
|
|
$this->setSelectedPort(9000);
|
|
|
|
$this->logger->expects($this->once())
|
|
->method('warning')
|
|
->with(
|
|
'Server closed connection without sending data',
|
|
$this->callback(static function (array $context): bool {
|
|
return 9000 === $context['port'];
|
|
}),
|
|
);
|
|
|
|
$this->expectException(ImmediateConnectionCloseException::class);
|
|
$this->expectExceptionMessage('Server closed connection without sending data');
|
|
|
|
$this->invokeReceive($stream);
|
|
}
|
|
|
|
public function testNonEmptyResponseReturnsPayloadWithoutTheLengthHeader(): void
|
|
{
|
|
$stream = fopen('php://memory', 'r+');
|
|
fwrite($stream, '0000000005Hello');
|
|
rewind($stream);
|
|
|
|
$this->setOperationStartTime();
|
|
|
|
$result = $this->invokeReceive($stream);
|
|
|
|
self::assertSame('Hello', $result);
|
|
}
|
|
|
|
/**
|
|
* A peer closing mid-stream used to yield a silently shortened payload, which only
|
|
* surfaced later as an unexplained parse failure. The announced length catches it here.
|
|
*/
|
|
public function testTruncatedResponseThrows(): void
|
|
{
|
|
$stream = fopen('php://memory', 'r+');
|
|
fwrite($stream, '0000000050Hello');
|
|
rewind($stream);
|
|
|
|
$this->setOperationStartTime();
|
|
$this->setSelectedPort(9000);
|
|
|
|
$this->expectException(ApiClientException::class);
|
|
$this->expectExceptionMessage('Truncated response, got 5 of 50 announced bytes');
|
|
|
|
$this->invokeReceive($stream);
|
|
}
|
|
|
|
public function testResponseShorterThanTheLengthHeaderThrows(): void
|
|
{
|
|
$stream = fopen('php://memory', 'r+');
|
|
fwrite($stream, '00000');
|
|
rewind($stream);
|
|
|
|
$this->setOperationStartTime();
|
|
$this->setSelectedPort(9000);
|
|
|
|
$this->expectException(ApiClientException::class);
|
|
$this->expectExceptionMessage('Incomplete response header, got 5 of 10 bytes');
|
|
|
|
$this->invokeReceive($stream);
|
|
}
|
|
|
|
public function testHeaderOnlyResponseThrows(): void
|
|
{
|
|
$stream = fopen('php://memory', 'r+');
|
|
fwrite($stream, '0000000000');
|
|
rewind($stream);
|
|
|
|
$this->setOperationStartTime();
|
|
$this->setSelectedPort(9000);
|
|
|
|
$this->expectException(ApiClientException::class);
|
|
$this->expectExceptionMessage('Response announced an empty body (header "0000000000")');
|
|
|
|
$this->invokeReceive($stream);
|
|
}
|
|
|
|
private function invokeReceive($socket): string
|
|
{
|
|
$method = new \ReflectionMethod(SocketTransport::class, 'receive');
|
|
|
|
return $method->invoke($this->transport(), $socket);
|
|
}
|
|
|
|
private function setOperationStartTime(): void
|
|
{
|
|
$property = new \ReflectionProperty(SocketTransport::class, 'operationStartTime');
|
|
$property->setValue($this->transport(), microtime(true));
|
|
}
|
|
|
|
private function setSelectedPort(int $port): void
|
|
{
|
|
$property = new \ReflectionProperty(SocketTransport::class, 'selectedPort');
|
|
$property->setValue($this->transport(), $port);
|
|
}
|
|
}
|