refactor: move buspro socket framing into SocketTransport
This commit is contained in:
1 parent
e51664df6c
commit
3c81c79819
4 files changed
+289
-277
No files matched your search
@@ -0,0 +1,134 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user