feature #30604 [HttpClient] add MockHttpClient (nicolas-grekas)
This PR was merged into the 4.3-dev branch.
Discussion
----------
[HttpClient] add MockHttpClient
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR | -
This PR introduces `MockHttpClient` and `MockResponse`, to be used for testing classes that need an HTTP client without making actual HTTP requests.
`MockHttpClient` is configured via its constructor: you provide it either with an iterable or a callable, and these will be used to provide responses as the consumer requests them.
Example:
```php
$responses = [
new MockResponse($body1, $info1),
new MockResponse($body2, $info2),
];
$client = new MockHttpClient($responses);
$response1 = $client->request(...); // created from $responses[0]
$response2 = $client->request(...); // created from $responses[1]
```
Or alternatively:
```php
$callback = function ($method, $url, $options) {
return new MockResponse(...);
};
$client = new MockHttpClient($callback);
$response = $client->request(...); // calls $callback internal
```
The responses provided to the client don't have to be instances of `MockResponse` - any `ResponseInterface` works (e.g. `$this->getMockBuilder(ResponseInterface::class)->getMock()`).
Using `MockResponse` allows simulating chunked responses and timeouts:
```php
$body = function () {
yield 'hello';
yield ''; // the empty string is turned into a timeout so that they are easy to test
yield 'world';
};
$mockResponse = new Mockresponse($body);
```
Last but not least, the implementation simulates the full lifecycle of a properly behaving `HttpClientInterface` contracts implementation: error handling, progress function, etc. This is "proved" by `MockHttpClientTest`, who implements and passes the reference test suite in `HttpClientTestCase`.
Commits
-------
8fd7584158
[HttpClient] add MockHttpClient
This commit is contained in:
commit
d5d1b50cf7
@ -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) {
|
||||
|
@ -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) {
|
||||
|
85
src/Symfony/Component/HttpClient/MockHttpClient.php
Normal file
85
src/Symfony/Component/HttpClient/MockHttpClient.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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 <p@tchwork.com>
|
||||
*/
|
||||
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));
|
||||
}
|
||||
}
|
265
src/Symfony/Component/HttpClient/Response/MockResponse.php
Normal file
265
src/Symfony/Component/HttpClient/Response/MockResponse.php
Normal file
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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 <p@tchwork.com>
|
||||
*/
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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()];
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
130
src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php
Normal file
130
src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user