200]) ); $sut = new SafeRemoteContentClient($client); $response = $sut->sendRequest(new Request('GET', 'https://example.com/logo.png')); self::assertSame(200, $response->getStatusCode()); self::assertSame('image-bytes', $response->getBody()->getContents()); } public function testNon2xxResponseIsForwardedWithoutThrowing(): void { $client = new MockHttpClient( new MockResponse('not found', ['http_code' => 404]) ); $sut = new SafeRemoteContentClient($client); $response = $sut->sendRequest(new Request('GET', 'https://example.com/missing.png')); self::assertSame(404, $response->getStatusCode()); } public function testTransportExceptionResultsInNon2xxResponse(): void { // Simulates NoPrivateNetworkHttpClient blocking the request, a DNS // failure, or a connection timeout — none of which must crash the // PDF rendering pipeline. $client = new MockHttpClient(static function (): MockResponse { throw new TransportException('IP blocked'); }); $sut = new SafeRemoteContentClient($client); $response = $sut->sendRequest(new Request('GET', 'http://127.0.0.1/internal')); self::assertSame(502, $response->getStatusCode()); } public function testRequestIsBlockedWhenWrappedWithNoPrivateNetworkHttpClient(): void { // Wraps a mock client that would otherwise succeed. The decorator // must reject the localhost URL before any request is dispatched. $inner = new MockHttpClient(new MockResponse('should-not-be-reached')); $safe = new NoPrivateNetworkHttpClient($inner); $sut = new SafeRemoteContentClient($safe); $response = $sut->sendRequest(new Request('GET', 'http://127.0.0.1/internal')); self::assertSame(502, $response->getStatusCode()); self::assertSame('', $response->getBody()->getContents()); } }