fix: make bpn response failures diagnosable
This commit is contained in:
@@ -6,6 +6,7 @@ namespace App\Tests\BusProNet;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||
use App\Service\RequestIdGenerator;
|
||||
@@ -70,7 +71,7 @@ class ApiClientReceiveTest extends TestCase
|
||||
$this->invokeReceive($stream);
|
||||
}
|
||||
|
||||
public function testNonEmptyResponseReturnsData(): void
|
||||
public function testNonEmptyResponseReturnsPayloadWithoutTheLengthHeader(): void
|
||||
{
|
||||
$stream = fopen('php://memory', 'r+');
|
||||
fwrite($stream, '0000000005Hello');
|
||||
@@ -80,7 +81,56 @@ class ApiClientReceiveTest extends TestCase
|
||||
|
||||
$result = $this->invokeReceive($stream);
|
||||
|
||||
self::assertSame('0000000005Hello', $result);
|
||||
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
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ResponseParserException;
|
||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* A broken response used to collapse into either "Empty response received from server" —
|
||||
* which is wrong for a malformed body — or an opaque "Unable to parse XML response".
|
||||
* Each shape must now name itself, otherwise a production parse failure is not
|
||||
* diagnosable after the fact.
|
||||
*/
|
||||
class ApiResponseParserFailureTest extends TestCase
|
||||
{
|
||||
public function testEmptyResponse(): void
|
||||
{
|
||||
$this->expectException(ResponseParserException::class);
|
||||
$this->expectExceptionMessage('empty response (0 bytes)');
|
||||
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_NOTIFICATION, '');
|
||||
}
|
||||
|
||||
public function testTruncatedResponseNamesTheLibxmlReason(): void
|
||||
{
|
||||
$xml = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="HINWEIS"></satz><hinweis';
|
||||
|
||||
try {
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_NOTIFICATION, $xml);
|
||||
$this->fail('Expected a ResponseParserException');
|
||||
} catch (ResponseParserException $e) {
|
||||
self::assertStringContainsString('('.strlen($xml).' bytes)', $e->getMessage());
|
||||
self::assertStringContainsString('line 1', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function testWellFormedResponseOfAnUnknownType(): void
|
||||
{
|
||||
$xml = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="IRGENDWAS"></satz></ergebnis>';
|
||||
|
||||
$this->expectException(ResponseParserException::class);
|
||||
$this->expectExceptionMessage('Unrecognised BusProNet response type "IRGENDWAS"');
|
||||
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_NOTIFICATION, $xml);
|
||||
}
|
||||
|
||||
public function testUnknownCustomerDataSubtype(): void
|
||||
{
|
||||
$xml = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="KUNDENKONTO"></satz><art>Irgendwas</art></ergebnis>';
|
||||
|
||||
$this->expectException(ResponseParserException::class);
|
||||
$this->expectExceptionMessage('Unrecognised customer data subtype "Irgendwas"');
|
||||
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $xml);
|
||||
}
|
||||
|
||||
public function testDiagnosingDoesNotLeakLibxmlErrorState(): void
|
||||
{
|
||||
$previous = libxml_use_internal_errors(false);
|
||||
|
||||
try {
|
||||
try {
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_NOTIFICATION, '<ergebnis');
|
||||
} catch (ResponseParserException) {
|
||||
}
|
||||
|
||||
self::assertFalse(libxml_use_internal_errors(false));
|
||||
self::assertSame([], libxml_get_errors());
|
||||
} finally {
|
||||
libxml_use_internal_errors($previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user