From 8fd7584158a39a52ed8dfb9f22e1696208a9755f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 19 Mar 2019 10:44:44 +0100 Subject: [PATCH] [HttpClient] add MockHttpClient --- .../Component/HttpClient/Chunk/ErrorChunk.php | 17 +- .../Component/HttpClient/HttpClientTrait.php | 4 +- .../Component/HttpClient/MockHttpClient.php | 85 ++++++ .../HttpClient/Response/MockResponse.php | 265 ++++++++++++++++++ .../HttpClient/Response/NativeResponse.php | 2 +- .../HttpClient/Response/ResponseTrait.php | 10 +- .../HttpClient/Tests/CurlHttpClientTest.php | 2 +- .../HttpClient/Tests/MockHttpClientTest.php | 130 +++++++++ .../HttpClient/Tests/NativeHttpClientTest.php | 2 +- .../HttpClient/Test/HttpClientTestCase.php | 85 +++--- 10 files changed, 549 insertions(+), 53 deletions(-) create mode 100644 src/Symfony/Component/HttpClient/MockHttpClient.php create mode 100644 src/Symfony/Component/HttpClient/Response/MockResponse.php create mode 100644 src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php diff --git a/src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php b/src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php index 0c3f8dfc62..5899a9ce7c 100644 --- a/src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php +++ b/src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php @@ -21,19 +21,14 @@ use Symfony\Contracts\HttpClient\ChunkInterface; */ class ErrorChunk implements ChunkInterface { - protected $didThrow; - + private $didThrow = false; private $offset; private $errorMessage; private $error; - /** - * @param bool &$didThrow Allows monitoring when the $error has been thrown or not - */ - public function __construct(bool &$didThrow, int $offset, \Throwable $error = null) + public function __construct(int $offset, \Throwable $error = null) { $didThrow = false; - $this->didThrow = &$didThrow; $this->offset = $offset; $this->error = $error; $this->errorMessage = null !== $error ? $error->getMessage() : 'Reading from the response stream reached the inactivity timeout.'; @@ -96,6 +91,14 @@ class ErrorChunk implements ChunkInterface return $this->errorMessage; } + /** + * @return bool Whether the wrapped error has been thrown or not + */ + public function didThrow(): bool + { + return $this->didThrow; + } + public function __destruct() { if (!$this->didThrow) { diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 223eba3e01..24bcdee217 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -117,7 +117,7 @@ trait HttpClientTrait // Finalize normalization of options $options['headers'] = $headers; - $options['http_version'] = (string) ($options['http_version'] ?? ''); + $options['http_version'] = (string) ($options['http_version'] ?? '') ?: null; $options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout')); return [$url, $options]; @@ -128,6 +128,8 @@ trait HttpClientTrait */ private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array { + unset($options['raw_headers'], $defaultOptions['raw_headers']); + $options['headers'] = self::normalizeHeaders($options['headers'] ?? []); if ($defaultOptions['headers'] ?? false) { diff --git a/src/Symfony/Component/HttpClient/MockHttpClient.php b/src/Symfony/Component/HttpClient/MockHttpClient.php new file mode 100644 index 0000000000..1d3d7b8b68 --- /dev/null +++ b/src/Symfony/Component/HttpClient/MockHttpClient.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient; + +use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\HttpClient\Response\ResponseStream; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; + +/** + * A test-friendly HttpClient that doesn't make actual HTTP requests. + * + * @author Nicolas Grekas + */ +class MockHttpClient implements HttpClientInterface +{ + use HttpClientTrait; + + private $responseFactory; + private $baseUri; + + /** + * @param callable|ResponseInterface|ResponseInterface[]|iterable $responseFactory + */ + public function __construct($responseFactory, string $baseUri = null) + { + if ($responseFactory instanceof ResponseInterface) { + $responseFactory = [$responseFactory]; + } + + if (!\is_callable($responseFactory) && !$responseFactory instanceof \Iterator) { + $responseFactory = (function () use ($responseFactory) { + yield from $responseFactory; + })(); + } + + $this->responseFactory = $responseFactory; + $this->baseUri = $baseUri; + } + + /** + * {@inheritdoc} + */ + public function request(string $method, string $url, array $options = []): ResponseInterface + { + [$url, $options] = $this->prepareRequest($method, $url, $options, ['base_uri' => $this->baseUri], true); + $url = implode('', $url); + + if (\is_callable($this->responseFactory)) { + $response = ($this->responseFactory)($method, $url, $options); + } elseif (!$this->responseFactory->valid()) { + throw new TransportException('The response factory iterator passed to MockHttpClient is empty.'); + } else { + $response = $this->responseFactory->current(); + $this->responseFactory->next(); + } + + return MockResponse::fromRequest($method, $url, $options, $response); + } + + /** + * {@inheritdoc} + */ + public function stream($responses, float $timeout = null): ResponseStreamInterface + { + if ($responses instanceof ResponseInterface) { + $responses = [$responses]; + } elseif (!\is_iterable($responses)) { + throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of MockResponse objects, %s given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses))); + } + + return new ResponseStream(MockResponse::stream($responses, $timeout)); + } +} diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php new file mode 100644 index 0000000000..4d0bc43ea4 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -0,0 +1,265 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient\Response; + +use Symfony\Component\HttpClient\Chunk\ErrorChunk; +use Symfony\Component\HttpClient\Chunk\FirstChunk; +use Symfony\Component\HttpClient\Exception\InvalidArgumentException; +use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * A test-friendly response. + * + * @author Nicolas Grekas + */ +class MockResponse implements ResponseInterface +{ + use ResponseTrait; + + private $body; + + private static $mainMulti; + private static $idSequence = 0; + + /** + * @param string|string[]|iterable $body The response body as a string or an iterable of strings, + * yielding an empty string simulates a timeout, + * exceptions are turned to TransportException + * + * @see ResponseInterface::getInfo() for possible info, e.g. "raw_headers" + */ + public function __construct($body = '', array $info = []) + { + $this->body = \is_iterable($body) ? $body : (string) $body; + $this->info = $info + $this->info; + } + + /** + * {@inheritdoc} + */ + public function getInfo(string $type = null) + { + return null !== $type ? $this->info[$type] ?? null : $this->info; + } + + /** + * {@inheritdoc} + */ + protected function close(): void + { + $this->body = []; + } + + /** + * @internal + */ + public static function fromRequest(string $method, string $url, array $options, ResponseInterface $mock): self + { + $response = new self([]); + $response->id = ++self::$idSequence; + $response->content = ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : null; + $response->initializer = static function (self $response) { + if (null !== $response->info['error']) { + throw new TransportException($response->info['error']); + } + + if (\is_array($response->body[0] ?? null)) { + // Consume the first chunk if it's not yielded yet + self::stream([$response])->current(); + } + }; + + $response->info['redirect_count'] = 0; + $response->info['redirect_url'] = null; + $response->info['start_time'] = microtime(true); + $response->info['http_method'] = $method; + $response->info['http_code'] = 0; + $response->info['user_data'] = $options['user_data'] ?? null; + $response->info['url'] = $url; + + self::writeRequest($response, $options, $mock); + $response->body[] = [$options, $mock]; + + return $response; + } + + /** + * {@inheritdoc} + */ + protected static function schedule(self $response, array &$runningResponses): void + { + if (!$response->id) { + throw new InvalidArgumentException('MockResponse instances must be issued by MockHttpClient before processing.'); + } + + $multi = self::$mainMulti ?? self::$mainMulti = (object) [ + 'handlesActivity' => [], + 'openHandles' => [], + ]; + + if (!isset($runningResponses[0])) { + $runningResponses[0] = [$multi, []]; + } + + $runningResponses[0][1][$response->id] = $response; + } + + /** + * {@inheritdoc} + */ + protected static function perform(\stdClass $multi, array &$responses): void + { + foreach ($responses as $response) { + $id = $response->id; + + if (!$response->body) { + // Last chunk + $multi->handlesActivity[$id][] = null; + $multi->handlesActivity[$id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null; + } elseif (null === $chunk = array_shift($response->body)) { + // Last chunk + $multi->handlesActivity[$id][] = null; + $multi->handlesActivity[$id][] = array_shift($response->body); + } elseif (\is_array($chunk)) { + // First chunk + try { + $offset = 0; + $chunk[1]->getStatusCode(); + $response->headers = $chunk[1]->getHeaders(false); + $multi->handlesActivity[$id][] = new FirstChunk(); + self::readResponse($response, $chunk[0], $chunk[1], $offset); + } catch (\Throwable $e) { + $multi->handlesActivity[$id][] = null; + $multi->handlesActivity[$id][] = $e; + } + } else { + // Data or timeout chunk + $multi->handlesActivity[$id][] = $chunk; + + if (\is_string($chunk) && null !== $response->content) { + // Buffer response body + fwrite($response->content, $chunk); + } + } + } + } + + /** + * {@inheritdoc} + */ + protected static function select(\stdClass $multi, float $timeout): int + { + return 42; + } + + /** + * Simulates sending the request. + */ + private static function writeRequest(self $response, array $options, ResponseInterface $mock) + { + $onProgress = $options['on_progress'] ?? static function () {}; + $response->info += $mock->getInfo() ?: []; + + // simulate "size_upload" if it is set + if (isset($response->info['size_upload'])) { + $response->info['size_upload'] = 0.0; + } + + // simulate "total_time" if it is set + if (isset($response->info['total_time'])) { + $response->info['total_time'] = microtime(true) - $response->info['start_time']; + } + + // "notify" DNS resolution + $onProgress(0, 0, $response->info); + + // consume the request body + if (\is_resource($body = $options['body'] ?? '')) { + $data = stream_get_contents($body); + if (isset($response->info['size_upload'])) { + $response->info['size_upload'] += \strlen($data); + } + } elseif ($body instanceof \Closure) { + while ('' !== $data = $body(16372)) { + if (!\is_string($data)) { + throw new TransportException(sprintf('Return value of the "body" option callback must be string, %s returned.', \gettype($data))); + } + + // "notify" upload progress + if (isset($response->info['size_upload'])) { + $response->info['size_upload'] += \strlen($data); + } + + $onProgress(0, 0, $response->info); + } + } + } + + /** + * Simulates reading the response. + */ + private static function readResponse(self $response, array $options, ResponseInterface $mock, int &$offset) + { + $onProgress = $options['on_progress'] ?? static function () {}; + + // populate info related to headers + $info = $mock->getInfo() ?: []; + $response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode(false) ?: 200; + $response->addRawHeaders($info['raw_headers'] ?? [], $response->info, $response->headers); + $dlSize = (int) ($response->headers['content-length'][0] ?? 0); + + $response->info = [ + 'start_time' => $response->info['start_time'], + 'user_data' => $response->info['user_data'], + 'http_code' => $response->info['http_code'], + ] + $info + $response->info; + + if (isset($response->info['total_time'])) { + $response->info['total_time'] = microtime(true) - $response->info['start_time']; + } + + // "notify" headers arrival + $onProgress(0, $dlSize, $response->info); + + // cast response body to activity list + $body = $mock instanceof self ? $mock->body : $mock->getContent(false); + + if (!\is_string($body)) { + foreach ($body as $chunk) { + if ('' === $chunk = (string) $chunk) { + // simulate a timeout + $response->body[] = new ErrorChunk($offset); + } else { + $response->body[] = $chunk; + $offset += \strlen($chunk); + // "notify" download progress + $onProgress($offset, $dlSize, $response->info); + } + } + } elseif ('' !== $body) { + $response->body[] = $body; + $offset = \strlen($body); + } + + if (isset($response->info['total_time'])) { + $response->info['total_time'] = microtime(true) - $response->info['start_time']; + } + + // "notify" completion + $onProgress($offset, $dlSize, $response->info); + + if (isset($response->headers['content-length']) && $offset !== $dlSize) { + throw new TransportException(sprintf('Transfer closed with %s bytes remaining to read.', $dlSize - $offset)); + } + } +} diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index f210e75440..4071a9f918 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -147,7 +147,7 @@ final class NativeResponse implements ResponseInterface $this->inflate = null; } - $this->multi->openHandles[$this->id] = [$h, $this->buffer, $this->inflate, &$this->content, $this->onProgress, &$this->remaining, &$this->info]; + $this->multi->openHandles[$this->id] = [$h, $this->buffer, $this->inflate, $this->content, $this->onProgress, &$this->remaining, &$this->info]; $this->multi->handlesActivity[$this->id] = [new FirstChunk()]; } diff --git a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php index 381bab5dc3..7429547293 100644 --- a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php +++ b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php @@ -259,9 +259,7 @@ trait ResponseTrait foreach ($responses as $j => $response) { $timeoutMax = $timeout ?? max($timeoutMax, $response->timeout); $timeoutMin = min($timeoutMin, $response->timeout, 1); - // ErrorChunk instances will set $didThrow to true when the - // exception they wrap has been thrown after yielding - $chunk = $didThrow = false; + $chunk = false; if (isset($multi->handlesActivity[$j])) { // no-op @@ -269,7 +267,7 @@ trait ResponseTrait unset($responses[$j]); continue; } elseif ($isTimeout) { - $multi->handlesActivity[$j] = [new ErrorChunk($didThrow, $response->offset)]; + $multi->handlesActivity[$j] = [new ErrorChunk($response->offset)]; } else { continue; } @@ -293,7 +291,7 @@ trait ResponseTrait throw $e; } - $chunk = new ErrorChunk($didThrow, $response->offset, $e); + $chunk = new ErrorChunk($response->offset, $e); } else { $chunk = new LastChunk($response->offset); } @@ -310,7 +308,7 @@ trait ResponseTrait if ($chunk instanceof FirstChunk && null === $response->initializer) { // Ensure the HTTP status code is always checked $response->getHeaders(true); - } elseif ($chunk instanceof ErrorChunk && !$didThrow) { + } elseif ($chunk instanceof ErrorChunk && !$chunk->didThrow()) { // Ensure transport exceptions are always thrown $chunk->getContent(); } diff --git a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php index 7c51d42236..476dc72976 100644 --- a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php @@ -20,7 +20,7 @@ use Symfony\Contracts\HttpClient\Test\HttpClientTestCase; */ class CurlHttpClientTest extends HttpClientTestCase { - protected function getHttpClient(): HttpClientInterface + protected function getHttpClient(string $testCase): HttpClientInterface { return new CurlHttpClient(); } diff --git a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php new file mode 100644 index 0000000000..1484526e18 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient\Tests; + +use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\NativeHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\Test\HttpClientTestCase; + +class MockHttpClientTest extends HttpClientTestCase +{ + protected function getHttpClient(string $testCase): HttpClientInterface + { + $responses = []; + + $headers = [ + 'Host: localhost:8057', + 'Content-Type: application/json', + ]; + + $body = '{ + "SERVER_PROTOCOL": "HTTP/1.1", + "SERVER_NAME": "127.0.0.1", + "REQUEST_URI": "/", + "REQUEST_METHOD": "GET", + "HTTP_FOO": "baR", + "HTTP_HOST": "localhost:8057" + }'; + + $client = new NativeHttpClient(); + + switch ($testCase) { + default: + return new MockHttpClient(function (string $method, string $url, array $options) use ($client) { + try { + // force the request to be completed so that we don't test side effects of the transport + $response = $client->request($method, $url, $options); + $content = $response->getContent(false); + + return new MockResponse($content, $response->getInfo()); + } catch (\Throwable $e) { + $this->fail($e->getMessage()); + } + }); + + case 'testUnsupportedOption': + $this->markTestSkipped('MockHttpClient accepts any options by default'); + break; + + case 'testChunkedEncoding': + $this->markTestSkipped("MockHttpClient doesn't dechunk"); + break; + + case 'testGzipBroken': + $this->markTestSkipped("MockHttpClient doesn't unzip"); + break; + + case 'testDestruct': + $this->markTestSkipped("MockHttpClient doesn't timeout on destruct"); + break; + + case 'testGetRequest': + array_unshift($headers, 'HTTP/1.1 200 OK'); + $responses[] = new MockResponse($body, ['raw_headers' => $headers]); + + $headers = [ + 'Host: localhost:8057', + 'Content-Length: 1000', + 'Content-Type: application/json', + ]; + + $responses[] = new MockResponse($body, ['raw_headers' => $headers]); + break; + + case 'testDnsError': + $mock = $this->getMockBuilder(ResponseInterface::class)->getMock(); + $mock->expects($this->any()) + ->method('getStatusCode') + ->willThrowException(new TransportException('DSN error')); + $mock->expects($this->any()) + ->method('getInfo') + ->willReturn([]); + + $responses[] = $mock; + $responses[] = $mock; + break; + + case 'testBadRequestBody': + case 'testOnProgressCancel': + case 'testOnProgressError': + $responses[] = new MockResponse($body, ['raw_headers' => $headers]); + break; + + case 'testTimeoutOnAccess': + $mock = $this->getMockBuilder(ResponseInterface::class)->getMock(); + $mock->expects($this->any()) + ->method('getHeaders') + ->willThrowException(new TransportException('Timeout')); + + $responses[] = $mock; + break; + + case 'testResolve': + $responses[] = new MockResponse($body, ['raw_headers' => $headers]); + $responses[] = new MockResponse($body, ['raw_headers' => $headers]); + $responses[] = $client->request('GET', 'http://symfony.com:8057/'); + break; + + case 'testTimeoutOnStream': + case 'testUncheckedTimeoutThrows': + $body = ['<1>', '', '<2>']; + $responses[] = new MockResponse($body, ['raw_headers' => $headers]); + break; + } + + return new MockHttpClient($responses); + } +} diff --git a/src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php index d2af0584f9..783167791d 100644 --- a/src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php @@ -17,7 +17,7 @@ use Symfony\Contracts\HttpClient\Test\HttpClientTestCase; class NativeHttpClientTest extends HttpClientTestCase { - protected function getHttpClient(): HttpClientInterface + protected function getHttpClient(string $testCase): HttpClientInterface { return new NativeHttpClient(); } diff --git a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php index 9c98d5bb32..400f8dc6b2 100644 --- a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php +++ b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php @@ -31,11 +31,11 @@ abstract class HttpClientTestCase extends TestCase TestHttpServer::start(); } - abstract protected function getHttpClient(): HttpClientInterface; + abstract protected function getHttpClient(string $testCase): HttpClientInterface; public function testGetRequest() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057', [ 'headers' => ['Foo' => 'baR'], 'user_data' => $data = new \stdClass(), @@ -74,7 +74,7 @@ abstract class HttpClientTestCase extends TestCase public function testNonBufferedGetRequest() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057', [ 'buffer' => false, 'headers' => ['Foo' => 'baR'], @@ -89,7 +89,7 @@ abstract class HttpClientTestCase extends TestCase public function testUnsupportedOption() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $this->expectException(\InvalidArgumentException::class); $client->request('GET', 'http://localhost:8057', [ @@ -99,7 +99,7 @@ abstract class HttpClientTestCase extends TestCase public function testHttpVersion() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057', [ 'http_version' => 1.0, ]); @@ -116,7 +116,7 @@ abstract class HttpClientTestCase extends TestCase public function testChunkedEncoding() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/chunked'); $this->assertSame(['chunked'], $response->getHeaders()['transfer-encoding']); @@ -130,7 +130,7 @@ abstract class HttpClientTestCase extends TestCase public function testClientError() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/404'); $client->stream($response)->valid(); @@ -156,7 +156,7 @@ abstract class HttpClientTestCase extends TestCase public function testIgnoreErrors() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/404'); $this->assertSame(404, $response->getStatusCode()); @@ -164,7 +164,7 @@ abstract class HttpClientTestCase extends TestCase public function testDnsError() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/301/bad-tld'); try { @@ -201,7 +201,7 @@ abstract class HttpClientTestCase extends TestCase public function testInlineAuth() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://foo:bar%3Dbar@localhost:8057'); $body = $response->toArray(); @@ -210,9 +210,22 @@ abstract class HttpClientTestCase extends TestCase $this->assertSame('bar=bar', $body['PHP_AUTH_PW']); } + public function testBadRequestBody() + { + $client = $this->getHttpClient(__FUNCTION__); + + $this->expectException(TransportExceptionInterface::class); + + $response = $client->request('POST', 'http://localhost:8057/', [ + 'body' => function () { yield []; }, + ]); + + $response->getStatusCode(); + } + public function testRedirects() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('POST', 'http://localhost:8057/301', [ 'auth_basic' => 'foo:bar', 'body' => function () { @@ -248,7 +261,7 @@ abstract class HttpClientTestCase extends TestCase public function testRelativeRedirects() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/302/relative'); $body = $response->toArray(); @@ -266,7 +279,7 @@ abstract class HttpClientTestCase extends TestCase public function testRedirect307() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('POST', 'http://localhost:8057/307', [ 'body' => function () { @@ -288,7 +301,7 @@ abstract class HttpClientTestCase extends TestCase public function testMaxRedirects() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/301', [ 'max_redirects' => 1, 'auth_basic' => 'foo:bar', @@ -322,7 +335,7 @@ abstract class HttpClientTestCase extends TestCase public function testStream() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057'); $chunks = $client->stream($response); @@ -354,7 +367,7 @@ abstract class HttpClientTestCase extends TestCase public function testAddToStream() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $r1 = $client->request('GET', 'http://localhost:8057'); @@ -385,7 +398,7 @@ abstract class HttpClientTestCase extends TestCase public function testCompleteTypeError() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $this->expectException(\TypeError::class); $client->stream(123); @@ -393,7 +406,7 @@ abstract class HttpClientTestCase extends TestCase public function testOnProgress() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('POST', 'http://localhost:8057/post', [ 'headers' => ['Content-Length' => 14], 'body' => 'foo=0123456789', @@ -411,7 +424,7 @@ abstract class HttpClientTestCase extends TestCase public function testPostJson() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('POST', 'http://localhost:8057/post', [ 'json' => ['foo' => 'bar'], @@ -426,7 +439,7 @@ abstract class HttpClientTestCase extends TestCase public function testPostArray() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('POST', 'http://localhost:8057/post', [ 'body' => ['foo' => 'bar'], @@ -437,7 +450,7 @@ abstract class HttpClientTestCase extends TestCase public function testPostResource() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $h = fopen('php://temp', 'w+'); fwrite($h, 'foo=0123456789'); @@ -454,7 +467,7 @@ abstract class HttpClientTestCase extends TestCase public function testPostCallback() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('POST', 'http://localhost:8057/post', [ 'body' => function () { @@ -470,7 +483,7 @@ abstract class HttpClientTestCase extends TestCase public function testOnProgressCancel() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/timeout-body', [ 'on_progress' => function ($dlNow) { if (0 < $dlNow) { @@ -494,7 +507,7 @@ abstract class HttpClientTestCase extends TestCase public function testOnProgressError() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/timeout-body', [ 'on_progress' => function ($dlNow) { if (0 < $dlNow) { @@ -518,7 +531,7 @@ abstract class HttpClientTestCase extends TestCase public function testResolve() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://symfony.com:8057/', [ 'resolve' => ['symfony.com' => '127.0.0.1'], ]); @@ -533,7 +546,7 @@ abstract class HttpClientTestCase extends TestCase public function testTimeoutOnAccess() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/timeout-header', [ 'timeout' => 0.1, ]); @@ -545,7 +558,7 @@ abstract class HttpClientTestCase extends TestCase public function testTimeoutOnStream() { usleep(300000); // wait for the previous test to release the server - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/timeout-body'); $this->assertSame(200, $response->getStatusCode()); @@ -577,7 +590,7 @@ abstract class HttpClientTestCase extends TestCase public function testUncheckedTimeoutThrows() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/timeout-body'); $chunks = $client->stream([$response], 0.1); @@ -589,7 +602,7 @@ abstract class HttpClientTestCase extends TestCase public function testDestruct() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $downloaded = 0; $start = microtime(true); @@ -603,7 +616,7 @@ abstract class HttpClientTestCase extends TestCase public function testProxy() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/', [ 'proxy' => 'http://localhost:8057', ]); @@ -625,7 +638,7 @@ abstract class HttpClientTestCase extends TestCase putenv('no_proxy='.$_SERVER['no_proxy'] = 'example.com, localhost'); try { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/', [ 'proxy' => 'http://localhost:8057', ]); @@ -646,7 +659,7 @@ abstract class HttpClientTestCase extends TestCase */ public function testAutoEncodingRequest() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057'); $this->assertSame(200, $response->getStatusCode()); @@ -663,7 +676,7 @@ abstract class HttpClientTestCase extends TestCase public function testBaseUri() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', '../404', [ 'base_uri' => 'http://localhost:8057/abc/', ]); @@ -674,7 +687,7 @@ abstract class HttpClientTestCase extends TestCase public function testQuery() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/?a=a', [ 'query' => ['b' => 'b'], ]); @@ -689,7 +702,7 @@ abstract class HttpClientTestCase extends TestCase */ public function testUserlandEncodingRequest() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057', [ 'headers' => ['Accept-Encoding' => 'gzip'], ]); @@ -711,7 +724,7 @@ abstract class HttpClientTestCase extends TestCase */ public function testGzipBroken() { - $client = $this->getHttpClient(); + $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://localhost:8057/gzip-broken'); $this->expectException(TransportExceptionInterface::class);