From a11b946c7c0d567670735aa37f61cb4952aceb0e Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Sat, 29 Nov 2025 19:36:53 +0100 Subject: [PATCH] rewrite tests from phpspec to phpunit --- .github/workflows/tests.yml | 3 +- composer.json | 5 - phpstan.neon.dist | 283 +++++++++- spec/BatchClientSpec.php | 36 -- spec/BatchResultSpec.php | 59 -- spec/EmulatedHttpAsyncClientSpec.php | 63 --- spec/EmulatedHttpClientSpec.php | 72 --- spec/Exception/BatchExceptionSpec.php | 37 -- spec/FlexibleHttpClientSpec.php | 94 ---- .../HttpClientPool/HttpClientPoolItemSpec.php | 214 -------- .../LeastUsedClientPoolSpec.php | 91 --- spec/HttpClientPool/RandomClientPoolSpec.php | 74 --- .../RoundRobinClientPoolSpec.php | 86 --- spec/HttpClientRouterSpec.php | 71 --- spec/Plugin/AddHostPluginSpec.php | 84 --- spec/Plugin/AddPathPluginSpec.php | 76 --- spec/Plugin/AuthenticationPluginSpec.php | 42 -- spec/Plugin/BaseUriPluginSpec.php | 102 ---- spec/Plugin/ContentLengthPluginSpec.php | 50 -- spec/Plugin/ContentTypePluginSpec.php | 107 ---- spec/Plugin/CookiePluginSpec.php | 212 ------- spec/Plugin/DecoderPluginSpec.php | 114 ---- spec/Plugin/ErrorPluginSpec.php | 88 --- spec/Plugin/HeaderAppendPluginSpec.php | 36 -- spec/Plugin/HeaderDefaultsPluginSpec.php | 37 -- spec/Plugin/HeaderRemovePluginSpec.php | 38 -- spec/Plugin/HeaderSetPluginSpec.php | 36 -- spec/Plugin/HistoryPluginSpec.php | 58 -- spec/Plugin/PluginStub.php | 25 - spec/Plugin/QueryDefaultsPluginSpec.php | 56 -- spec/Plugin/RedirectPluginSpec.php | 516 ------------------ spec/Plugin/RequestMatcherPluginSpec.php | 56 -- spec/Plugin/RequestSeekableBodyPluginSpec.php | 46 -- .../Plugin/ResponseSeekableBodyPluginSpec.php | 56 -- spec/Plugin/RetryPluginSpec.php | 168 ------ spec/PluginClientFactorySpec.php | 28 - spec/PluginClientSpec.php | 91 --- tests/BatchClientTest.php | 62 +++ tests/BatchResultTest.php | 59 ++ tests/EmulatedHttpAsyncClientTest.php | 64 +++ tests/EmulatedHttpClientTest.php | 65 +++ tests/Exception/BatchExceptionTest.php | 28 + tests/FlexibleHttpClientTest.php | 97 ++++ .../HttpClientPool/HttpClientPoolItemTest.php | 181 ++++++ .../LeastUsedClientPoolTest.php | 110 ++++ tests/HttpClientPool/RandomClientPoolTest.php | 95 ++++ .../RoundRobinClientPoolTest.php | 98 ++++ tests/HttpClientRouterTest.php | 87 +++ tests/Plugin/AddHostPluginTest.php | 65 +++ tests/Plugin/AuthenticationPluginTest.php | 34 ++ tests/Plugin/BaseUriPluginTest.php | 69 +++ tests/Plugin/ContentLengthPluginTest.php | 52 ++ tests/Plugin/ContentTypePluginTest.php | 109 ++++ tests/Plugin/CookiePluginTest.php | 187 +++++++ tests/Plugin/DecoderPluginTest.php | 148 +++++ tests/Plugin/ErrorPluginTest.php | 72 +++ tests/Plugin/HeaderAppendPluginTest.php | 30 + tests/Plugin/HeaderDefaultsPluginTest.php | 34 ++ tests/Plugin/HeaderRemovePluginTest.php | 31 ++ tests/Plugin/HeaderSetPluginTest.php | 30 + tests/Plugin/HistoryPluginTest.php | 50 ++ tests/Plugin/QueryDefaultsPluginTest.php | 50 ++ tests/Plugin/RequestMatcherPluginTest.php | 64 +++ .../Plugin/RequestSeekableBodyPluginTest.php | 52 ++ .../Plugin/ResponseSeekableBodyPluginTest.php | 53 ++ tests/Plugin/RetryPluginTest.php | 160 ++++++ tests/PluginChainTest.php | 24 +- tests/PluginClientBuilderTest.php | 10 +- tests/PluginClientFactoryTest.php | 30 + tests/PluginClientTest.php | 125 +++++ 70 files changed, 2692 insertions(+), 3043 deletions(-) delete mode 100644 spec/BatchClientSpec.php delete mode 100644 spec/BatchResultSpec.php delete mode 100644 spec/EmulatedHttpAsyncClientSpec.php delete mode 100644 spec/EmulatedHttpClientSpec.php delete mode 100644 spec/Exception/BatchExceptionSpec.php delete mode 100644 spec/FlexibleHttpClientSpec.php delete mode 100644 spec/HttpClientPool/HttpClientPoolItemSpec.php delete mode 100644 spec/HttpClientPool/LeastUsedClientPoolSpec.php delete mode 100644 spec/HttpClientPool/RandomClientPoolSpec.php delete mode 100644 spec/HttpClientPool/RoundRobinClientPoolSpec.php delete mode 100644 spec/HttpClientRouterSpec.php delete mode 100644 spec/Plugin/AddHostPluginSpec.php delete mode 100644 spec/Plugin/AddPathPluginSpec.php delete mode 100644 spec/Plugin/AuthenticationPluginSpec.php delete mode 100644 spec/Plugin/BaseUriPluginSpec.php delete mode 100644 spec/Plugin/ContentLengthPluginSpec.php delete mode 100644 spec/Plugin/ContentTypePluginSpec.php delete mode 100644 spec/Plugin/CookiePluginSpec.php delete mode 100644 spec/Plugin/DecoderPluginSpec.php delete mode 100644 spec/Plugin/ErrorPluginSpec.php delete mode 100644 spec/Plugin/HeaderAppendPluginSpec.php delete mode 100644 spec/Plugin/HeaderDefaultsPluginSpec.php delete mode 100644 spec/Plugin/HeaderRemovePluginSpec.php delete mode 100644 spec/Plugin/HeaderSetPluginSpec.php delete mode 100644 spec/Plugin/HistoryPluginSpec.php delete mode 100644 spec/Plugin/PluginStub.php delete mode 100644 spec/Plugin/QueryDefaultsPluginSpec.php delete mode 100644 spec/Plugin/RedirectPluginSpec.php delete mode 100644 spec/Plugin/RequestMatcherPluginSpec.php delete mode 100644 spec/Plugin/RequestSeekableBodyPluginSpec.php delete mode 100644 spec/Plugin/ResponseSeekableBodyPluginSpec.php delete mode 100644 spec/Plugin/RetryPluginSpec.php delete mode 100644 spec/PluginClientFactorySpec.php delete mode 100644 spec/PluginClientSpec.php create mode 100644 tests/BatchClientTest.php create mode 100644 tests/BatchResultTest.php create mode 100644 tests/EmulatedHttpAsyncClientTest.php create mode 100644 tests/EmulatedHttpClientTest.php create mode 100644 tests/Exception/BatchExceptionTest.php create mode 100644 tests/FlexibleHttpClientTest.php create mode 100644 tests/HttpClientPool/HttpClientPoolItemTest.php create mode 100644 tests/HttpClientPool/LeastUsedClientPoolTest.php create mode 100644 tests/HttpClientPool/RandomClientPoolTest.php create mode 100644 tests/HttpClientPool/RoundRobinClientPoolTest.php create mode 100644 tests/HttpClientRouterTest.php create mode 100644 tests/Plugin/AddHostPluginTest.php create mode 100644 tests/Plugin/AuthenticationPluginTest.php create mode 100644 tests/Plugin/BaseUriPluginTest.php create mode 100644 tests/Plugin/ContentLengthPluginTest.php create mode 100644 tests/Plugin/ContentTypePluginTest.php create mode 100644 tests/Plugin/CookiePluginTest.php create mode 100644 tests/Plugin/DecoderPluginTest.php create mode 100644 tests/Plugin/ErrorPluginTest.php create mode 100644 tests/Plugin/HeaderAppendPluginTest.php create mode 100644 tests/Plugin/HeaderDefaultsPluginTest.php create mode 100644 tests/Plugin/HeaderRemovePluginTest.php create mode 100644 tests/Plugin/HeaderSetPluginTest.php create mode 100644 tests/Plugin/HistoryPluginTest.php create mode 100644 tests/Plugin/QueryDefaultsPluginTest.php create mode 100644 tests/Plugin/RequestMatcherPluginTest.php create mode 100644 tests/Plugin/RequestSeekableBodyPluginTest.php create mode 100644 tests/Plugin/ResponseSeekableBodyPluginTest.php create mode 100644 tests/Plugin/RetryPluginTest.php create mode 100644 tests/PluginClientFactoryTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 145d475f..da7760b5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,7 @@ jobs: name: PHP ${{ matrix.php }} Latest runs-on: ubuntu-latest strategy: + fail-fast: false matrix: php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] @@ -39,6 +40,7 @@ jobs: name: PHP ${{ matrix.php }} Lowest runs-on: ubuntu-latest strategy: + fail-fast: false matrix: php: ['7.1', '7.4', '8.2', '8.3'] @@ -113,7 +115,6 @@ jobs: - name: Install dependencies run: | - composer require "friends-of-phpspec/phpspec-code-coverage:^4.3.2" --no-interaction --no-update composer update --prefer-dist --no-interaction --no-progress - name: Execute tests diff --git a/composer.json b/composer.json index 25afb927..8d969035 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,6 @@ "doctrine/instantiator": "^1.1", "guzzlehttp/psr7": "^1.4", "nyholm/psr7": "^1.2", - "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", - "phpspec/prophecy": "^1.10.2", "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" }, "suggest": { @@ -42,17 +40,14 @@ }, "autoload-dev": { "psr-4": { - "spec\\Http\\Client\\Common\\": "spec/", "Tests\\Http\\Client\\Common\\": "tests/" } }, "scripts": { "test": [ - "vendor/bin/phpspec run", "vendor/bin/phpunit" ], "test-ci": [ - "vendor/bin/phpspec run -c phpspec.ci.yml", "vendor/bin/phpunit" ] }, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 8886dea1..93f0409b 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,10 +1,10 @@ parameters: level: max - checkMissingIterableValueType: false treatPhpDocTypesAsCertain: false paths: - src ignoreErrors: + - "#.*no value type specified in iterable type array.*#" # Exception still thrown in PHP 8, not sure why phpstan complains - message: "#^Dead catch - UnexpectedValueException is never thrown in the try block\\.$#" @@ -69,6 +69,11 @@ parameters: count: 1 path: src/Plugin/RedirectPlugin.php + - + message: "#^Method Psr\\\\Http\\\\Message\\\\StreamFactoryInterface@anonymous\/Plugin\/RedirectPlugin.php:221\\:\\:createStream\\(\\) should return Psr\\\\Http\\\\Message\\\\StreamInterface but returns mixed\\.$#" + count: 1 + path: src/Plugin/RedirectPlugin.php + - message: "#^Method Http\\\\Client\\\\Common\\\\Plugin\\\\RetryPlugin\\:\\:retry\\(\\) should return Psr\\\\Http\\\\Message\\\\ResponseInterface but returns mixed\\.$#" count: 1 @@ -78,3 +83,279 @@ parameters: message: "#^Method Http\\\\Client\\\\Common\\\\PluginClient\\:\\:sendRequest\\(\\) should return Psr\\\\Http\\\\Message\\\\ResponseInterface but returns mixed\\.$#" count: 2 path: src/PluginClient.php + + - + message: '#^Method Http\\Client\\Common\\BatchResult\:\:getExceptionFor\(\) has UnexpectedValueException in PHPDoc @throws tag but it''s not thrown\.$#' + identifier: throws.unusedType + count: 1 + path: src/BatchResult.php + + - + message: '#^Method Http\\Client\\Common\\BatchResult\:\:getResponseFor\(\) has UnexpectedValueException in PHPDoc @throws tag but it''s not thrown\.$#' + identifier: throws.unusedType + count: 1 + path: src/BatchResult.php + + - + message: '#^Parameter \#1 \$response of method Http\\Client\\Common\\Deferred\:\:resolve\(\) expects Psr\\Http\\Message\\ResponseInterface, mixed given\.$#' + identifier: argument.type + count: 2 + path: src/Deferred.php + + - + message: '#^Invalid type mixed to throw\.$#' + identifier: throw.notThrowable + count: 1 + path: src/HttpClientPool/HttpClientPoolItem.php + + - + message: '#^Method Http\\Client\\Common\\HttpMethodsClient\:\:createRequest\(\) should return Psr\\Http\\Message\\RequestInterface but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/HttpMethodsClient.php + + - + message: '#^Parameter \#2 \$value of method Psr\\Http\\Message\\MessageInterface\:\:withHeader\(\) expects array\\|string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/HttpMethodsClient.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\AddHostPlugin\:\:\$replace \(bool\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/AddHostPlugin.php + + - + message: '#^Parameter \#2 \$config of class Http\\Client\\Common\\Plugin\\AddHostPlugin constructor expects array\{replace\?\: bool\}, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/BaseUriPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\ContentTypePlugin\:\:\$sizeLimit \(int\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/ContentTypePlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\ContentTypePlugin\:\:\$skipDetection \(bool\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/ContentTypePlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\DecoderPlugin\:\:\$useContentEncoding \(bool\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/DecoderPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\ErrorPlugin\:\:\$onlyServerException \(bool\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/ErrorPlugin.php + + - + message: '#^Parameter \#2 \$value of method Psr\\Http\\Message\\MessageInterface\:\:withAddedHeader\(\) expects array\\|string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/HeaderAppendPlugin.php + + - + message: '#^Parameter \#2 \$value of method Psr\\Http\\Message\\MessageInterface\:\:withHeader\(\) expects array\\|string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/HeaderDefaultsPlugin.php + + - + message: '#^Parameter \#1 \$name of method Psr\\Http\\Message\\MessageInterface\:\:hasHeader\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/HeaderRemovePlugin.php + + - + message: '#^Parameter \#1 \$name of method Psr\\Http\\Message\\MessageInterface\:\:withoutHeader\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/HeaderRemovePlugin.php + + - + message: '#^Parameter \#2 \$value of method Psr\\Http\\Message\\MessageInterface\:\:withHeader\(\) expects array\\|string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/HeaderSetPlugin.php + + - + message: '#^Cannot access offset ''multiple'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Cannot access offset ''permanent'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Cannot access offset ''status'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Cannot access offset ''switch'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 4 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Cannot access offset ''to'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Cannot access offset ''unless'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Cannot access offset ''uri'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Method Http\\Client\\Common\\Plugin\\RedirectPlugin\:\:guessStreamFactory\(\) should return Psr\\Http\\Message\\StreamFactoryInterface\|null but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Parameter \#1 \$method of method Psr\\Http\\Message\\RequestInterface\:\:withMethod\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Parameter \#2 \$haystack of function in_array expects array, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Parameter \#2 \$targetUri of method Http\\Client\\Common\\Plugin\\RedirectPlugin\:\:buildRedirectRequest\(\) expects Psr\\Http\\Message\\UriInterface, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Parameter \#3 \$statusCode of method Http\\Client\\Common\\Plugin\\RedirectPlugin\:\:buildRedirectRequest\(\) expects int, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\RedirectPlugin\:\:\$preserveHeader \(array\|bool\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\RedirectPlugin\:\:\$streamFactory \(Psr\\Http\\Message\\StreamFactoryInterface\|null\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\RedirectPlugin\:\:\$useDefaultForMultiple \(bool\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: '#^Cannot use \+\+ on mixed\.$#' + identifier: preInc.type + count: 1 + path: src/Plugin/RetryPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\RetryPlugin\:\:\$errorResponseDecider \(callable\(\)\: mixed\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/RetryPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\RetryPlugin\:\:\$errorResponseDelay \(callable\(\)\: mixed\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/RetryPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\RetryPlugin\:\:\$exceptionDecider \(callable\(\)\: mixed\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/RetryPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\RetryPlugin\:\:\$exceptionDelay \(callable\(\)\: mixed\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/RetryPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\RetryPlugin\:\:\$retry \(int\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/RetryPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\SeekableBodyPlugin\:\:\$memoryBufferSize \(int\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/SeekableBodyPlugin.php + + - + message: '#^Property Http\\Client\\Common\\Plugin\\SeekableBodyPlugin\:\:\$useFileBuffer \(bool\) does not accept mixed\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugin/SeekableBodyPlugin.php + + - + message: '#^Trait Http\\Client\\Common\\Plugin\\VersionBridgePlugin is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Plugin/VersionBridgePlugin.php + + - + message: '#^Method Http\\Client\\Common\\PluginChain\:\:__invoke\(\) should return Http\\Promise\\Promise but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/PluginChain.php + + - + message: '#^Parameter \#1 \$response of class Http\\Client\\Promise\\HttpFulfilledPromise constructor expects Psr\\Http\\Message\\ResponseInterface, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/PluginClient.php + + - + message: '#^Parameter \#3 \$options of class Http\\Client\\Common\\PluginChain constructor expects array\{max_restarts\?\: int\}, array given\.$#' + identifier: argument.type + count: 1 + path: src/PluginClient.php + + - + message: '#^Parameter \#3 \$options of class Http\\Client\\Common\\PluginClient constructor expects array\{max_restarts\?\: int\}, array given\.$#' + identifier: argument.type + count: 1 + path: src/PluginClientBuilder.php + + - + message: '#^Trait Http\\Client\\Common\\VersionBridgeClient is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/VersionBridgeClient.php diff --git a/spec/BatchClientSpec.php b/spec/BatchClientSpec.php deleted file mode 100644 index 7dcb2618..00000000 --- a/spec/BatchClientSpec.php +++ /dev/null @@ -1,36 +0,0 @@ -beAnInstanceOf(BatchClient::class, [$client]); - } - - public function it_send_multiple_request_using_send_request(HttpClient $client, RequestInterface $request1, RequestInterface $request2, ResponseInterface $response1, ResponseInterface $response2) - { - $client->sendRequest($request1)->willReturn($response1); - $client->sendRequest($request2)->willReturn($response2); - - $this->sendRequests([$request1, $request2])->shouldReturnAnInstanceOf(BatchResult::class); - } - - public function it_throw_batch_exception_if_one_or_more_request_failed(HttpClient $client, RequestInterface $request1, RequestInterface $request2, ResponseInterface $response) - { - $client->sendRequest($request1)->willReturn($response); - $client->sendRequest($request2)->willThrow(HttpException::class); - - $this->shouldThrow(BatchException::class)->duringSendRequests([$request1, $request2]); - } -} diff --git a/spec/BatchResultSpec.php b/spec/BatchResultSpec.php deleted file mode 100644 index 775bb500..00000000 --- a/spec/BatchResultSpec.php +++ /dev/null @@ -1,59 +0,0 @@ -beAnInstanceOf(BatchResult::class); - } - - public function it_is_immutable(RequestInterface $request, ResponseInterface $response) - { - $new = $this->addResponse($request, $response); - - $this->getResponses()->shouldReturn([]); - $new->shouldHaveType(BatchResult::class); - $new->getResponses()->shouldReturn([$response]); - } - - public function it_has_a_responses(RequestInterface $request, ResponseInterface $response) - { - $new = $this->addResponse($request, $response); - - $this->hasResponses()->shouldReturn(false); - $this->getResponses()->shouldReturn([]); - $new->hasResponses()->shouldReturn(true); - $new->getResponses()->shouldReturn([$response]); - } - - public function it_has_a_response_for_a_request(RequestInterface $request, ResponseInterface $response) - { - $new = $this->addResponse($request, $response); - - $this->shouldThrow(\UnexpectedValueException::class)->duringGetResponseFor($request); - $this->isSuccessful($request)->shouldReturn(false); - $new->getResponseFor($request)->shouldReturn($response); - $new->isSuccessful($request)->shouldReturn(true); - } - - public function it_keeps_exception_after_add_request(RequestInterface $request1, Exception $exception, RequestInterface $request2, ResponseInterface $response) - { - $new = $this->addException($request1, $exception); - $new = $new->addResponse($request2, $response); - - $new->isSuccessful($request2)->shouldReturn(true); - $new->isFailed($request2)->shouldReturn(false); - $new->getResponseFor($request2)->shouldReturn($response); - $new->isSuccessful($request1)->shouldReturn(false); - $new->isFailed($request1)->shouldReturn(true); - $new->getExceptionFor($request1)->shouldReturn($exception); - } -} diff --git a/spec/EmulatedHttpAsyncClientSpec.php b/spec/EmulatedHttpAsyncClientSpec.php deleted file mode 100644 index d5d114ae..00000000 --- a/spec/EmulatedHttpAsyncClientSpec.php +++ /dev/null @@ -1,63 +0,0 @@ -beConstructedWith($httpClient); - } - - public function it_is_initializable() - { - $this->shouldHaveType(EmulatedHttpAsyncClient::class); - } - - public function it_is_an_http_client() - { - $this->shouldImplement(HttpClient::class); - } - - public function it_is_an_async_http_client() - { - $this->shouldImplement(HttpAsyncClient::class); - } - - public function it_emulates_a_successful_request( - HttpClient $httpClient, - RequestInterface $request, - ResponseInterface $response - ) { - $httpClient->sendRequest($request)->willReturn($response); - - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); - } - - public function it_emulates_a_failed_request(HttpClient $httpClient, RequestInterface $request) - { - $httpClient->sendRequest($request)->willThrow(TransferException::class); - - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - } - - public function it_decorates_the_underlying_client( - HttpClient $httpClient, - RequestInterface $request, - ResponseInterface $response - ) { - $httpClient->sendRequest($request)->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - } -} diff --git a/spec/EmulatedHttpClientSpec.php b/spec/EmulatedHttpClientSpec.php deleted file mode 100644 index cc9f91ee..00000000 --- a/spec/EmulatedHttpClientSpec.php +++ /dev/null @@ -1,72 +0,0 @@ -beConstructedWith($httpAsyncClient); - } - - public function it_is_initializable() - { - $this->shouldHaveType(EmulatedHttpClient::class); - } - - public function it_is_an_http_client() - { - $this->shouldImplement(HttpClient::class); - } - - public function it_is_an_async_http_client() - { - $this->shouldImplement(HttpAsyncClient::class); - } - - public function it_emulates_a_successful_request( - HttpAsyncClient $httpAsyncClient, - RequestInterface $request, - Promise $promise, - ResponseInterface $response - ) { - $promise->wait()->shouldBeCalled(); - $promise->getState()->willReturn(Promise::FULFILLED); - $promise->wait()->willReturn($response); - - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - - $this->sendRequest($request)->shouldReturn($response); - } - - public function it_emulates_a_failed_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) - { - $promise->wait()->shouldBeCalled(); - $promise->getState()->willReturn(Promise::REJECTED); - $promise->wait()->willThrow(new TransferException()); - - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - - $this->shouldThrow(Exception::class)->duringSendRequest($request); - } - - public function it_decorates_the_underlying_client( - HttpAsyncClient $httpAsyncClient, - RequestInterface $request, - Promise $promise - ) { - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - - $this->sendAsyncRequest($request)->shouldReturn($promise); - } -} diff --git a/spec/Exception/BatchExceptionSpec.php b/spec/Exception/BatchExceptionSpec.php deleted file mode 100644 index be72814d..00000000 --- a/spec/Exception/BatchExceptionSpec.php +++ /dev/null @@ -1,37 +0,0 @@ -beConstructedWith($batchResult); - } - - public function it_is_initializable() - { - $this->shouldHaveType(BatchException::class); - } - - public function it_is_a_runtime_exception() - { - $this->shouldHaveType(\RuntimeException::class); - } - - public function it_is_an_exception() - { - $this->shouldImplement(Exception::class); - } - - public function it_has_a_batch_result() - { - $this->getResult()->shouldHaveType(BatchResult::class); - } -} diff --git a/spec/FlexibleHttpClientSpec.php b/spec/FlexibleHttpClientSpec.php deleted file mode 100644 index ec88da72..00000000 --- a/spec/FlexibleHttpClientSpec.php +++ /dev/null @@ -1,94 +0,0 @@ -beConstructedWith($httpClient); - } - - public function it_is_initializable() - { - $this->shouldHaveType(FlexibleHttpClient::class); - } - - public function it_is_an_http_client() - { - $this->shouldImplement(HttpClient::class); - } - - public function it_is_an_async_http_client() - { - $this->shouldImplement(HttpAsyncClient::class); - } - - public function it_throw_type_error_if_invalid_client() - { - $this->beConstructedWith(null); - - $this->shouldThrow(\TypeError::class)->duringInstantiation(); - } - - public function it_emulates_an_async_client( - HttpClient $httpClient, - RequestInterface $syncRequest, - ResponseInterface $syncResponse, - RequestInterface $asyncRequest, - ResponseInterface $asyncResponse - ) { - $this->beConstructedWith($httpClient); - - $httpClient->sendRequest($syncRequest)->willReturn($syncResponse); - $httpClient->sendRequest($asyncRequest)->willReturn($asyncResponse); - - $this->sendRequest($syncRequest)->shouldReturn($syncResponse); - $promise = $this->sendAsyncRequest($asyncRequest); - - $promise->shouldHaveType(Promise::class); - $promise->wait()->shouldReturn($asyncResponse); - } - - public function it_emulates_a_client( - HttpAsyncClient $httpAsyncClient, - RequestInterface $asyncRequest, - Promise $promise, - RequestInterface $syncRequest, - Promise $syncPromise, - ResponseInterface $syncResponse - ) { - $this->beConstructedWith($httpAsyncClient); - - $httpAsyncClient->sendAsyncRequest($asyncRequest)->willReturn($promise); - $httpAsyncClient->sendAsyncRequest($syncRequest)->willReturn($syncPromise); - $syncPromise->wait()->willReturn($syncResponse); - - $this->sendAsyncRequest($asyncRequest)->shouldReturn($promise); - $this->sendRequest($syncRequest)->shouldReturn($syncResponse); - } - - public function it_does_not_emulate_a_client($client, RequestInterface $syncRequest, RequestInterface $asyncRequest) - { - $client->implement(HttpClient::class); - $client->implement(HttpAsyncClient::class); - - $client->sendRequest($syncRequest)->shouldBeCalled(); - $client->sendRequest($asyncRequest)->shouldNotBeCalled(); - $client->sendAsyncRequest($asyncRequest)->shouldBeCalled(); - $client->sendAsyncRequest($syncRequest)->shouldNotBeCalled(); - - $this->beConstructedWith($client); - - $this->sendRequest($syncRequest); - $this->sendAsyncRequest($asyncRequest); - } -} diff --git a/spec/HttpClientPool/HttpClientPoolItemSpec.php b/spec/HttpClientPool/HttpClientPoolItemSpec.php deleted file mode 100644 index 537378fe..00000000 --- a/spec/HttpClientPool/HttpClientPoolItemSpec.php +++ /dev/null @@ -1,214 +0,0 @@ -beConstructedWith($httpClient); - } - - public function it_is_an_http_client() - { - $this->shouldImplement(HttpClient::class); - } - - public function it_is_an_async_http_client() - { - $this->shouldImplement(HttpAsyncClient::class); - } - - public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) - { - $httpClient->sendRequest($request)->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - } - - public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) - { - $this->beConstructedWith($httpAsyncClient); - - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - $promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise); - - $this->sendAsyncRequest($request)->shouldReturn($promise); - } - - public function it_disable_himself_on_send_request(HttpClient $httpClient, RequestInterface $request) - { - $exception = new TransferException(); - $httpClient->sendRequest($request)->willThrow($exception); - $this->shouldThrow($exception)->duringSendRequest($request); - $this->isDisabled()->shouldReturn(true); - $this->shouldThrow(RequestException::class)->duringSendRequest($request); - } - - public function it_disable_himself_on_send_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request) - { - $this->beConstructedWith($httpAsyncClient); - - $promise = new HttpRejectedPromise(new TransferException()); - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $this->isDisabled()->shouldReturn(true); - $this->shouldThrow(RequestException::class)->duringSendAsyncRequest($request); - } - - public function it_reactivate_himself_on_send_request(HttpClient $httpClient, RequestInterface $request) - { - $this->beConstructedWith($httpClient, 0); - - $exception = new TransferException(); - $httpClient->sendRequest($request)->willThrow($exception); - - $this->shouldThrow($exception)->duringSendRequest($request); - $this->isDisabled()->shouldReturn(false); - $this->shouldThrow($exception)->duringSendRequest($request); - } - - public function it_reactivate_himself_on_send_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request) - { - $this->beConstructedWith($httpAsyncClient, 0); - - $promise = new HttpRejectedPromise(new TransferException()); - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $this->isDisabled()->shouldReturn(false); - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - } - - public function it_increments_request_count(HttpAsyncClient $httpAsyncClient, RequestInterface $request, ResponseInterface $response) - { - $this->beConstructedWith($httpAsyncClient, 0); - - $promise = new NotResolvingPromise($response->getWrappedObject()); - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - - $this->getSendingRequestCount()->shouldReturn(0); - $this->sendAsyncRequest($request)->shouldReturn($promise); - $this->getSendingRequestCount()->shouldReturn(1); - $this->sendAsyncRequest($request)->shouldReturn($promise); - $this->getSendingRequestCount()->shouldReturn(2); - } - - public function it_decrements_request_count(HttpAsyncClient $httpAsyncClient, RequestInterface $request, ResponseInterface $response) - { - $this->beConstructedWith($httpAsyncClient, 0); - - $promise = new NotResolvingPromise($response->getWrappedObject()); - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - - $this->getSendingRequestCount()->shouldReturn(0); - $this->sendAsyncRequest($request)->shouldReturn($promise); - $this->getSendingRequestCount()->shouldReturn(1); - - $promise->wait(false); - - $this->getSendingRequestCount()->shouldReturn(0); - } -} - -class NotResolvingPromise implements Promise -{ - private $queue = []; - - private $state = Promise::PENDING; - - private $response; - - private $exception; - - public function __construct(ResponseInterface $response = null, Exception $exception = null) - { - $this->response = $response; - $this->exception = $exception; - } - - public function then(callable $onFulfilled = null, callable $onRejected = null) - { - $this->queue[] = [ - $onFulfilled, - $onRejected, - ]; - - return $this; - } - - public function getState() - { - return $this->state; - } - - public function wait($unwrap = true) - { - if (Promise::FULFILLED === $this->state) { - if (!$unwrap) { - return; - } - - return $this->response; - } - - if (Promise::REJECTED === $this->state) { - if (!$unwrap) { - return; - } - - throw $this->exception; - } - - while (count($this->queue) > 0) { - $callbacks = array_shift($this->queue); - - if (null !== $this->response) { - try { - $this->response = $callbacks[0]($this->response); - $this->exception = null; - } catch (Exception $exception) { - $this->response = null; - $this->exception = $exception; - } - } elseif (null !== $this->exception) { - try { - $this->response = $callbacks[1]($this->exception); - $this->exception = null; - } catch (Exception $exception) { - $this->response = null; - $this->exception = $exception; - } - } - } - - if (null !== $this->response) { - $this->state = Promise::FULFILLED; - - if ($unwrap) { - return $this->response; - } - } - - if (null !== $this->exception) { - $this->state = Promise::REJECTED; - - if ($unwrap) { - throw $this->exception; - } - } - } -} diff --git a/spec/HttpClientPool/LeastUsedClientPoolSpec.php b/spec/HttpClientPool/LeastUsedClientPoolSpec.php deleted file mode 100644 index 367288d9..00000000 --- a/spec/HttpClientPool/LeastUsedClientPoolSpec.php +++ /dev/null @@ -1,91 +0,0 @@ -shouldHaveType(LeastUsedClientPool::class); - } - - public function it_is_an_http_client() - { - $this->shouldImplement(HttpClient::class); - } - - public function it_is_an_async_http_client() - { - $this->shouldImplement(HttpAsyncClient::class); - } - - public function it_throw_exception_with_no_client(RequestInterface $request) - { - $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); - $this->shouldThrow(HttpClientNotFoundException::class)->duringSendAsyncRequest($request); - } - - public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) - { - $this->addHttpClient($httpClient); - $httpClient->sendRequest($request)->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - } - - public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) - { - $this->addHttpClient($httpAsyncClient); - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - $promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise); - - $this->sendAsyncRequest($request)->shouldReturn($promise); - } - - public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request) - { - $this->addHttpClient($client); - $client->sendRequest($request)->willThrow(HttpException::class); - - $this->shouldThrow(HttpException::class)->duringSendRequest($request); - $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); - } - - public function it_reenable_client(HttpClient $client, RequestInterface $request) - { - $this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0)); - $client->sendRequest($request)->willThrow(HttpException::class); - - $this->shouldThrow(HttpException::class)->duringSendRequest($request); - $this->shouldThrow(HttpException::class)->duringSendRequest($request); - } - - public function it_uses_the_lowest_request_client(HttpClientPoolItem $client1, HttpClientPoolItem $client2, RequestInterface $request, ResponseInterface $response) - { - $this->addHttpClient($client1); - $this->addHttpClient($client2); - - $client1->getSendingRequestCount()->willReturn(10); - $client2->getSendingRequestCount()->willReturn(2); - - $client1->isDisabled()->willReturn(false); - $client2->isDisabled()->willReturn(false); - - $client1->sendRequest($request)->shouldNotBeCalled(); - $client2->sendRequest($request)->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - } -} diff --git a/spec/HttpClientPool/RandomClientPoolSpec.php b/spec/HttpClientPool/RandomClientPoolSpec.php deleted file mode 100644 index 5cb34c3b..00000000 --- a/spec/HttpClientPool/RandomClientPoolSpec.php +++ /dev/null @@ -1,74 +0,0 @@ -shouldHaveType(RandomClientPool::class); - } - - public function it_is_an_http_client() - { - $this->shouldImplement(HttpClient::class); - } - - public function it_is_an_async_http_client() - { - $this->shouldImplement(HttpAsyncClient::class); - } - - public function it_throw_exception_with_no_client(RequestInterface $request) - { - $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); - $this->shouldThrow(HttpClientNotFoundException::class)->duringSendAsyncRequest($request); - } - - public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) - { - $this->addHttpClient($httpClient); - $httpClient->sendRequest($request)->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - } - - public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) - { - $this->addHttpClient($httpAsyncClient); - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - $promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise); - - $this->sendAsyncRequest($request)->shouldReturn($promise); - } - - public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request) - { - $this->addHttpClient($client); - $client->sendRequest($request)->willThrow(HttpException::class); - - $this->shouldThrow(HttpException::class)->duringSendRequest($request); - $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); - } - - public function it_reenable_client(HttpClient $client, RequestInterface $request) - { - $this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0)); - $client->sendRequest($request)->willThrow(HttpException::class); - - $this->shouldThrow(HttpException::class)->duringSendRequest($request); - $this->shouldThrow(HttpException::class)->duringSendRequest($request); - } -} diff --git a/spec/HttpClientPool/RoundRobinClientPoolSpec.php b/spec/HttpClientPool/RoundRobinClientPoolSpec.php deleted file mode 100644 index 5a272b27..00000000 --- a/spec/HttpClientPool/RoundRobinClientPoolSpec.php +++ /dev/null @@ -1,86 +0,0 @@ -shouldHaveType(RoundRobinClientPool::class); - } - - public function it_is_an_http_client() - { - $this->shouldImplement(HttpClient::class); - } - - public function it_is_an_async_http_client() - { - $this->shouldImplement(HttpAsyncClient::class); - } - - public function it_throw_exception_with_no_client(RequestInterface $request) - { - $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); - $this->shouldThrow(HttpClientNotFoundException::class)->duringSendAsyncRequest($request); - } - - public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) - { - $this->addHttpClient($httpClient); - $httpClient->sendRequest($request)->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - } - - public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) - { - $this->addHttpClient($httpAsyncClient); - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - $promise->then(Argument::type('callable'), Argument::type('callable'))->willReturn($promise); - - $this->sendAsyncRequest($request)->shouldReturn($promise); - } - - public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request) - { - $this->addHttpClient($client); - $client->sendRequest($request)->willThrow(HttpException::class); - - $this->shouldThrow(HttpException::class)->duringSendRequest($request); - $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); - } - - public function it_reenable_client(HttpClient $client, RequestInterface $request) - { - $this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0)); - $client->sendRequest($request)->willThrow(HttpException::class); - - $this->shouldThrow(HttpException::class)->duringSendRequest($request); - $this->shouldThrow(HttpException::class)->duringSendRequest($request); - } - - public function it_round_between_clients(HttpClient $client1, HttpClient $client2, RequestInterface $request, ResponseInterface $response) - { - $this->addHttpClient($client1); - $this->addHttpClient($client2); - - $client1->sendRequest($request)->willReturn($response); - $client2->sendRequest($request)->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - $this->sendRequest($request)->shouldReturn($response); - } -} diff --git a/spec/HttpClientRouterSpec.php b/spec/HttpClientRouterSpec.php deleted file mode 100644 index db2f1122..00000000 --- a/spec/HttpClientRouterSpec.php +++ /dev/null @@ -1,71 +0,0 @@ -shouldHaveType(HttpClientRouter::class); - } - - public function it_is_an_http_client_router() - { - $this->shouldImplement(HttpClientRouterInterface::class); - } - - public function it_is_an_http_client() - { - $this->shouldImplement(HttpClient::class); - } - - public function it_is_an_async_http_client() - { - $this->shouldImplement(HttpAsyncClient::class); - } - - public function it_send_request(RequestMatcher $matcher, HttpClient $client, RequestInterface $request, ResponseInterface $response) - { - $this->addClient($client, $matcher); - $matcher->matches($request)->willReturn(true); - $client->sendRequest($request)->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - } - - public function it_send_async_request(RequestMatcher $matcher, HttpAsyncClient $client, RequestInterface $request, Promise $promise) - { - $this->addClient($client, $matcher); - $matcher->matches($request)->willReturn(true); - $client->sendAsyncRequest($request)->willReturn($promise); - - $this->sendAsyncRequest($request)->shouldReturn($promise); - } - - public function it_throw_exception_on_send_request(RequestMatcher $matcher, HttpClient $client, RequestInterface $request) - { - $this->addClient($client, $matcher); - $matcher->matches($request)->willReturn(false); - - $this->shouldThrow(HttpClientNoMatchException::class)->duringSendRequest($request); - } - - public function it_throw_exception_on_send_async_request(RequestMatcher $matcher, HttpAsyncClient $client, RequestInterface $request) - { - $this->addClient($client, $matcher); - $matcher->matches($request)->willReturn(false); - - $this->shouldThrow(HttpClientNoMatchException::class)->duringSendAsyncRequest($request); - } -} diff --git a/spec/Plugin/AddHostPluginSpec.php b/spec/Plugin/AddHostPluginSpec.php deleted file mode 100644 index e90ddfc4..00000000 --- a/spec/Plugin/AddHostPluginSpec.php +++ /dev/null @@ -1,84 +0,0 @@ -beConstructedWith($uri); - } - - public function it_is_initializable(UriInterface $uri) - { - $uri->getHost()->shouldBeCalled()->willReturn('example.com'); - - $this->shouldHaveType(AddHostPlugin::class); - } - - public function it_is_a_plugin(UriInterface $uri) - { - $uri->getHost()->shouldBeCalled()->willReturn('example.com'); - - $this->shouldImplement(Plugin::class); - } - - public function it_adds_domain( - RequestInterface $request, - UriInterface $host, - UriInterface $uri - ) { - $host->getScheme()->shouldBeCalled()->willReturn('http://'); - $host->getHost()->shouldBeCalled()->willReturn('example.com'); - $host->getPort()->shouldBeCalled()->willReturn(8000); - - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalled()->willReturn($request); - - $uri->withScheme('http://')->shouldBeCalled()->willReturn($uri); - $uri->withHost('example.com')->shouldBeCalled()->willReturn($uri); - $uri->withPort(8000)->shouldBeCalled()->willReturn($uri); - $uri->getHost()->shouldBeCalled()->willReturn(''); - - $this->beConstructedWith($host); - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_replaces_domain( - RequestInterface $request, - UriInterface $host, - UriInterface $uri - ) { - $host->getScheme()->shouldBeCalled()->willReturn('http://'); - $host->getHost()->shouldBeCalled()->willReturn('example.com'); - $host->getPort()->shouldBeCalled()->willReturn(8000); - - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalled()->willReturn($request); - - $uri->withScheme('http://')->shouldBeCalled()->willReturn($uri); - $uri->withHost('example.com')->shouldBeCalled()->willReturn($uri); - $uri->withPort(8000)->shouldBeCalled()->willReturn($uri); - - $this->beConstructedWith($host, ['replace' => true]); - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_nothing_when_domain_exists( - RequestInterface $request, - UriInterface $host, - UriInterface $uri - ) { - $request->getUri()->shouldBeCalled()->willReturn($uri); - $uri->getHost()->shouldBeCalled()->willReturn('default.com'); - - $this->beConstructedWith($host); - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/AddPathPluginSpec.php b/spec/Plugin/AddPathPluginSpec.php deleted file mode 100644 index 40756224..00000000 --- a/spec/Plugin/AddPathPluginSpec.php +++ /dev/null @@ -1,76 +0,0 @@ -beConstructedWith($uri); - } - - public function it_is_initializable(UriInterface $uri) - { - $uri->getPath()->shouldBeCalled()->willReturn('/api'); - - $this->shouldHaveType(AddPathPlugin::class); - } - - public function it_is_a_plugin(UriInterface $uri) - { - $uri->getPath()->shouldBeCalled()->willReturn('/api'); - - $this->shouldImplement(Plugin::class); - } - - public function it_adds_path( - RequestInterface $request, - UriInterface $host, - UriInterface $uri - ) { - $host->getPath()->shouldBeCalled()->willReturn('/api'); - - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalledTimes(1)->willReturn($request); - - $uri->withPath('/api/users')->shouldBeCalledTimes(1)->willReturn($uri); - $uri->getPath()->shouldBeCalled()->willReturn('/users'); - - $this->beConstructedWith($host); - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_removes_ending_slashes( - RequestInterface $request, - UriInterface $host, - UriInterface $host2, - UriInterface $uri - ) { - $host->getPath()->shouldBeCalled()->willReturn('/api/'); - $host2->getPath()->shouldBeCalled()->willReturn('/api'); - $host->withPath('/api')->shouldBeCalled()->willReturn($host2); - - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalled()->willReturn($request); - - $uri->withPath('/api/users')->shouldBeCalled()->willReturn($uri); - $uri->getPath()->shouldBeCalled()->willReturn('/users'); - - $this->beConstructedWith($host); - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_throws_exception_on_empty_path(UriInterface $host) - { - $host->getPath()->shouldBeCalled()->willReturn(''); - - $this->beConstructedWith($host); - $this->shouldThrow(\LogicException::class)->duringInstantiation(); - } -} diff --git a/spec/Plugin/AuthenticationPluginSpec.php b/spec/Plugin/AuthenticationPluginSpec.php deleted file mode 100644 index 3191ade7..00000000 --- a/spec/Plugin/AuthenticationPluginSpec.php +++ /dev/null @@ -1,42 +0,0 @@ -beConstructedWith($authentication); - } - - public function it_is_initializable(Authentication $authentication) - { - $this->shouldHaveType(AuthenticationPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_sends_an_authenticated_request(Authentication $authentication, RequestInterface $notAuthedRequest, RequestInterface $authedRequest, Promise $promise) - { - $authentication->authenticate($notAuthedRequest)->willReturn($authedRequest); - - $next = function (RequestInterface $request) use ($authedRequest, $promise) { - if (Argument::is($authedRequest->getWrappedObject())->scoreArgument($request)) { - return $promise->getWrappedObject(); - } - }; - - $this->handleRequest($notAuthedRequest, $next, function () {})->shouldReturn($promise); - } -} diff --git a/spec/Plugin/BaseUriPluginSpec.php b/spec/Plugin/BaseUriPluginSpec.php deleted file mode 100644 index 41d876b0..00000000 --- a/spec/Plugin/BaseUriPluginSpec.php +++ /dev/null @@ -1,102 +0,0 @@ -beConstructedWith($uri); - } - - public function it_is_initializable(UriInterface $uri) - { - $uri->getHost()->shouldBeCalled()->willReturn('example.com'); - $uri->getPath()->shouldBeCalled()->willReturn('/api'); - - $this->shouldHaveType(BaseUriPlugin::class); - } - - public function it_is_a_plugin(UriInterface $uri) - { - $uri->getHost()->shouldBeCalled()->willReturn('example.com'); - $uri->getPath()->shouldBeCalled()->willReturn('/api'); - - $this->shouldImplement(Plugin::class); - } - - public function it_adds_domain_and_path( - RequestInterface $request, - UriInterface $host, - UriInterface $uri - ) { - $host->getScheme()->shouldBeCalled()->willReturn('http://'); - $host->getHost()->shouldBeCalled()->willReturn('example.com'); - $host->getPort()->shouldBeCalled()->willReturn(8000); - $host->getPath()->shouldBeCalled()->willReturn('/api'); - - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalled()->willReturn($request); - - $uri->withScheme('http://')->shouldBeCalled()->willReturn($uri); - $uri->withHost('example.com')->shouldBeCalled()->willReturn($uri); - $uri->withPort(8000)->shouldBeCalled()->willReturn($uri); - $uri->withPath('/api/users')->shouldBeCalled()->willReturn($uri); - $uri->getHost()->shouldBeCalled()->willReturn(''); - $uri->getPath()->shouldBeCalled()->willReturn('/users'); - - $this->beConstructedWith($host); - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_adds_domain( - RequestInterface $request, - UriInterface $host, - UriInterface $uri - ) { - $host->getScheme()->shouldBeCalled()->willReturn('http://'); - $host->getHost()->shouldBeCalled()->willReturn('example.com'); - $host->getPort()->shouldBeCalled()->willReturn(8000); - $host->getPath()->shouldBeCalled()->willReturn('/'); - - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalled()->willReturn($request); - - $uri->withScheme('http://')->shouldBeCalled()->willReturn($uri); - $uri->withHost('example.com')->shouldBeCalled()->willReturn($uri); - $uri->withPort(8000)->shouldBeCalled()->willReturn($uri); - $uri->getHost()->shouldBeCalled()->willReturn(''); - - $this->beConstructedWith($host); - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_replaces_domain_and_adds_path( - RequestInterface $request, - UriInterface $host, - UriInterface $uri - ) { - $host->getScheme()->shouldBeCalled()->willReturn('http://'); - $host->getHost()->shouldBeCalled()->willReturn('example.com'); - $host->getPort()->shouldBeCalled()->willReturn(8000); - $host->getPath()->shouldBeCalled()->willReturn('/api'); - - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalled()->willReturn($request); - - $uri->withScheme('http://')->shouldBeCalled()->willReturn($uri); - $uri->withHost('example.com')->shouldBeCalled()->willReturn($uri); - $uri->withPort(8000)->shouldBeCalled()->willReturn($uri); - $uri->withPath('/api/users')->shouldBeCalled()->willReturn($uri); - $uri->getPath()->shouldBeCalled()->willReturn('/users'); - - $this->beConstructedWith($host, ['replace' => true]); - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/ContentLengthPluginSpec.php b/spec/Plugin/ContentLengthPluginSpec.php deleted file mode 100644 index 39a8b711..00000000 --- a/spec/Plugin/ContentLengthPluginSpec.php +++ /dev/null @@ -1,50 +0,0 @@ -shouldHaveType(ContentLengthPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_adds_content_length_header(RequestInterface $request, StreamInterface $stream) - { - $request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false); - $request->getBody()->shouldBeCalled()->willReturn($stream); - $stream->getSize()->shouldBeCalled()->willReturn(100); - $request->withHeader('Content-Length', '100')->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_streams_chunked_if_no_size(RequestInterface $request, StreamInterface $stream) - { - if (defined('HHVM_VERSION')) { - throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm'); - } - - $request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false); - $request->getBody()->shouldBeCalled()->willReturn($stream); - - $stream->getSize()->shouldBeCalled()->willReturn(null); - $request->withBody(Argument::type('Http\Message\Encoding\ChunkStream'))->shouldBeCalled()->willReturn($request); - $request->withAddedHeader('Transfer-Encoding', 'chunked')->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/ContentTypePluginSpec.php b/spec/Plugin/ContentTypePluginSpec.php deleted file mode 100644 index a27d32a9..00000000 --- a/spec/Plugin/ContentTypePluginSpec.php +++ /dev/null @@ -1,107 +0,0 @@ -shouldHaveType(ContentTypePlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_adds_json_content_type_header(RequestInterface $request) - { - $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); - $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for(json_encode(['foo' => 'bar']))); - $request->withHeader('Content-Type', 'application/json')->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_adds_xml_content_type_header(RequestInterface $request) - { - $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); - $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); - $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_set_content_type_header(RequestInterface $request) - { - $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); - $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo')); - $request->withHeader('Content-Type', null)->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_set_content_type_header_if_already_one(RequestInterface $request) - { - $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(true); - $request->getBody()->shouldNotBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo')); - $request->withHeader('Content-Type', null)->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_set_content_type_header_if_size_0_or_unknown(RequestInterface $request) - { - $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); - $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for()); - $request->withHeader('Content-Type', null)->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_adds_xml_content_type_header_if_size_limit_is_not_reached_using_default_value(RequestInterface $request) - { - $this->beConstructedWith([ - 'skip_detection' => true, - ]); - - $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); - $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); - $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_adds_xml_content_type_header_if_size_limit_is_not_reached(RequestInterface $request) - { - $this->beConstructedWith([ - 'skip_detection' => true, - 'size_limit' => 32000000, - ]); - - $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); - $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); - $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_set_content_type_header_if_size_limit_is_reached(RequestInterface $request) - { - $this->beConstructedWith([ - 'skip_detection' => true, - 'size_limit' => 8, - ]); - - $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); - $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); - $request->withHeader('Content-Type', null)->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/CookiePluginSpec.php b/spec/Plugin/CookiePluginSpec.php deleted file mode 100644 index be5cd624..00000000 --- a/spec/Plugin/CookiePluginSpec.php +++ /dev/null @@ -1,212 +0,0 @@ -cookieJar = new CookieJar(); - - $this->beConstructedWith($this->cookieJar); - } - - public function it_is_initializable() - { - $this->shouldHaveType(CookiePlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_loads_cookie(RequestInterface $request, UriInterface $uri, Promise $promise) - { - $cookie = new Cookie('name', 'value', 86400, 'test.com'); - $this->cookieJar->addCookie($cookie); - - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn('test.com'); - $uri->getPath()->willReturn('/'); - - $request->withAddedHeader('Cookie', 'name=value')->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_combines_multiple_cookies_into_one_header(RequestInterface $request, UriInterface $uri, Promise $promise) - { - $cookie = new Cookie('name', 'value', 86400, 'test.com'); - $cookie2 = new Cookie('name2', 'value2', 86400, 'test.com'); - - $this->cookieJar->addCookie($cookie); - $this->cookieJar->addCookie($cookie2); - - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn('test.com'); - $uri->getPath()->willReturn('/'); - - $request->withAddedHeader('Cookie', 'name=value; name2=value2')->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_load_cookie_if_expired(RequestInterface $request, UriInterface $uri, Promise $promise) - { - $cookie = new Cookie('name', 'value', null, 'test.com', false, false, null, (new \DateTime())->modify('-1 day')); - $this->cookieJar->addCookie($cookie); - - $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_load_cookie_if_domain_does_not_match(RequestInterface $request, UriInterface $uri, Promise $promise) - { - $cookie = new Cookie('name', 'value', 86400, 'test2.com'); - $this->cookieJar->addCookie($cookie); - - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn('test.com'); - - $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_load_cookie_on_hackish_domains(RequestInterface $request, UriInterface $uri, Promise $promise) - { - $hackishDomains = [ - 'hacktest.com', - 'test.com.hacked.org', - ]; - $cookie = new Cookie('name', 'value', 86400, 'test.com'); - $this->cookieJar->addCookie($cookie); - - foreach ($hackishDomains as $domain) { - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn($domain); - - $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - } - - public function it_loads_cookie_on_subdomains(RequestInterface $request, UriInterface $uri, Promise $promise) - { - $cookie = new Cookie('name', 'value', 86400, 'test.com'); - $this->cookieJar->addCookie($cookie); - - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn('www.test.com'); - $uri->getPath()->willReturn('/'); - - $request->withAddedHeader('Cookie', 'name=value')->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_load_cookie_if_path_does_not_match(RequestInterface $request, UriInterface $uri, Promise $promise) - { - $cookie = new Cookie('name', 'value', 86400, 'test.com', '/sub'); - $this->cookieJar->addCookie($cookie); - - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn('test.com'); - $uri->getPath()->willReturn('/'); - - $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_load_cookie_when_cookie_is_secure(RequestInterface $request, UriInterface $uri, Promise $promise) - { - $cookie = new Cookie('name', 'value', 86400, 'test.com', null, true); - $this->cookieJar->addCookie($cookie); - - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn('test.com'); - $uri->getPath()->willReturn('/'); - $uri->getScheme()->willReturn('http'); - - $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_loads_cookie_when_cookie_is_secure(RequestInterface $request, UriInterface $uri, Promise $promise) - { - $cookie = new Cookie('name', 'value', 86400, 'test.com', null, true); - $this->cookieJar->addCookie($cookie); - - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn('test.com'); - $uri->getPath()->willReturn('/'); - $uri->getScheme()->willReturn('https'); - - $request->withAddedHeader('Cookie', 'name=value')->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_saves_cookie(RequestInterface $request, ResponseInterface $response, UriInterface $uri) - { - $next = function () use ($response) { - return new HttpFulfilledPromise($response->getWrappedObject()); - }; - - $response->hasHeader('Set-Cookie')->willReturn(true); - $response->getHeader('Set-Cookie')->willReturn([ - 'cookie=value; expires=Tuesday, 31-Mar-99 07:42:12 GMT; Max-Age=60; path=/; domain=test.com; secure; HttpOnly', - ]); - - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn('test.com'); - $uri->getPath()->willReturn('/'); - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldHaveType(Promise::class); - $promise->wait()->shouldReturnAnInstanceOf(ResponseInterface::class); - } - - public function it_throws_exception_on_invalid_expires_date( - RequestInterface $request, - ResponseInterface $response, - UriInterface $uri - ) { - $next = function () use ($response) { - return new HttpFulfilledPromise($response->getWrappedObject()); - }; - - $response->hasHeader('Set-Cookie')->willReturn(true); - $response->getHeader('Set-Cookie')->willReturn([ - 'cookie=value; expires=i-am-an-invalid-date;', - ]); - - $request->getUri()->willReturn($uri); - $uri->getHost()->willReturn('test.com'); - $uri->getPath()->willReturn('/'); - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow(TransferException::class)->duringWait(); - } -} diff --git a/spec/Plugin/DecoderPluginSpec.php b/spec/Plugin/DecoderPluginSpec.php deleted file mode 100644 index c3731c8c..00000000 --- a/spec/Plugin/DecoderPluginSpec.php +++ /dev/null @@ -1,114 +0,0 @@ -shouldHaveType(DecoderPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_decodes(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) - { - if (defined('HHVM_VERSION')) { - throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm'); - } - - $request->withHeader('TE', ['gzip', 'deflate', 'chunked'])->shouldBeCalled()->willReturn($request); - $request->withHeader('Accept-Encoding', ['gzip', 'deflate'])->shouldBeCalled()->willReturn($request); - $next = function () use ($response) { - return new HttpFulfilledPromise($response->getWrappedObject()); - }; - - $response->hasHeader('Transfer-Encoding')->willReturn(true); - $response->getHeader('Transfer-Encoding')->willReturn(['chunked']); - $response->getBody()->willReturn($stream); - $response->withBody(Argument::type('Http\Message\Encoding\DechunkStream'))->willReturn($response); - $response->withoutHeader('Transfer-Encoding')->willReturn($response); - $response->hasHeader('Content-Encoding')->willReturn(false); - - $stream->isReadable()->willReturn(true); - $stream->isWritable()->willReturn(false); - $stream->eof()->willReturn(false); - - $this->handleRequest($request, $next, function () {}); - } - - public function it_decodes_gzip(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) - { - $request->withHeader('TE', ['gzip', 'deflate', 'chunked'])->shouldBeCalled()->willReturn($request); - $request->withHeader('Accept-Encoding', ['gzip', 'deflate'])->shouldBeCalled()->willReturn($request); - $next = function () use ($response) { - return new HttpFulfilledPromise($response->getWrappedObject()); - }; - - $response->hasHeader('Transfer-Encoding')->willReturn(false); - $response->hasHeader('Content-Encoding')->willReturn(true); - $response->getHeader('Content-Encoding')->willReturn(['gzip']); - $response->getBody()->willReturn($stream); - $response->withBody(Argument::type(GzipDecodeStream::class))->willReturn($response); - $response->withoutHeader('Content-Encoding')->willReturn($response); - - $stream->isReadable()->willReturn(true); - $stream->isWritable()->willReturn(false); - $stream->eof()->willReturn(false); - - $this->handleRequest($request, $next, function () {}); - } - - public function it_decodes_deflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) - { - $request->withHeader('TE', ['gzip', 'deflate', 'chunked'])->shouldBeCalled()->willReturn($request); - $request->withHeader('Accept-Encoding', ['gzip', 'deflate'])->shouldBeCalled()->willReturn($request); - $next = function () use ($response) { - return new HttpFulfilledPromise($response->getWrappedObject()); - }; - - $response->hasHeader('Transfer-Encoding')->willReturn(false); - $response->hasHeader('Content-Encoding')->willReturn(true); - $response->getHeader('Content-Encoding')->willReturn(['deflate']); - $response->getBody()->willReturn($stream); - $response->withBody(Argument::type(DecompressStream::class))->willReturn($response); - $response->withoutHeader('Content-Encoding')->willReturn($response); - - $stream->isReadable()->willReturn(true); - $stream->isWritable()->willReturn(false); - $stream->eof()->willReturn(false); - - $this->handleRequest($request, $next, function () {}); - } - - public function it_does_not_decode_with_content_encoding(RequestInterface $request, ResponseInterface $response) - { - $this->beConstructedWith(['use_content_encoding' => false]); - - $request->withHeader('TE', ['gzip', 'deflate', 'chunked'])->shouldBeCalled()->willReturn($request); - $request->withHeader('Accept-Encoding', ['gzip', 'deflate'])->shouldNotBeCalled(); - $next = function () use ($response) { - return new HttpFulfilledPromise($response->getWrappedObject()); - }; - - $response->hasHeader('Transfer-Encoding')->willReturn(false); - $response->hasHeader('Content-Encoding')->shouldNotBeCalled(); - - $this->handleRequest($request, $next, function () {}); - } -} diff --git a/spec/Plugin/ErrorPluginSpec.php b/spec/Plugin/ErrorPluginSpec.php deleted file mode 100644 index 7861246c..00000000 --- a/spec/Plugin/ErrorPluginSpec.php +++ /dev/null @@ -1,88 +0,0 @@ -beAnInstanceOf(ErrorPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_throw_client_error_exception_on_4xx_error(RequestInterface $request, ResponseInterface $response) - { - $response->getStatusCode()->willReturn(400); - $response->getReasonPhrase()->willReturn('Bad request'); - - $next = function (RequestInterface $receivedRequest) use ($request, $response) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($response->getWrappedObject()); - } - }; - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow(ClientErrorException::class)->duringWait(); - } - - public function it_does_not_throw_client_error_exception_on_4xx_error_if_only_server_exception(RequestInterface $request, ResponseInterface $response) - { - $this->beConstructedWith(['only_server_exception' => true]); - - $response->getStatusCode()->willReturn(400); - $response->getReasonPhrase()->willReturn('Bad request'); - - $next = function (RequestInterface $receivedRequest) use ($request, $response) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($response->getWrappedObject()); - } - }; - - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); - } - - public function it_throw_server_error_exception_on_5xx_error(RequestInterface $request, ResponseInterface $response) - { - $response->getStatusCode()->willReturn(500); - $response->getReasonPhrase()->willReturn('Server error'); - - $next = function (RequestInterface $receivedRequest) use ($request, $response) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($response->getWrappedObject()); - } - }; - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow(ServerErrorException::class)->duringWait(); - } - - public function it_returns_response(RequestInterface $request, ResponseInterface $response) - { - $response->getStatusCode()->willReturn(200); - - $next = function (RequestInterface $receivedRequest) use ($request, $response) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($response->getWrappedObject()); - } - }; - - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); - } -} diff --git a/spec/Plugin/HeaderAppendPluginSpec.php b/spec/Plugin/HeaderAppendPluginSpec.php deleted file mode 100644 index 9325069f..00000000 --- a/spec/Plugin/HeaderAppendPluginSpec.php +++ /dev/null @@ -1,36 +0,0 @@ -beConstructedWith([]); - $this->shouldHaveType(HeaderAppendPlugin::class); - } - - public function it_is_a_plugin() - { - $this->beConstructedWith([]); - $this->shouldImplement(Plugin::class); - } - - public function it_appends_the_header(RequestInterface $request) - { - $this->beConstructedWith([ - 'foo' => 'bar', - 'baz' => 'qux', - ]); - - $request->withAddedHeader('foo', 'bar')->shouldBeCalled()->willReturn($request); - $request->withAddedHeader('baz', 'qux')->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/HeaderDefaultsPluginSpec.php b/spec/Plugin/HeaderDefaultsPluginSpec.php deleted file mode 100644 index 5a50a9cd..00000000 --- a/spec/Plugin/HeaderDefaultsPluginSpec.php +++ /dev/null @@ -1,37 +0,0 @@ -beConstructedWith([]); - $this->shouldHaveType(HeaderDefaultsPlugin::class); - } - - public function it_is_a_plugin() - { - $this->beConstructedWith([]); - $this->shouldImplement(Plugin::class); - } - - public function it_sets_the_default_header(RequestInterface $request) - { - $this->beConstructedWith([ - 'foo' => 'bar', - 'baz' => 'qux', - ]); - - $request->hasHeader('foo')->shouldBeCalled()->willReturn(false); - $request->withHeader('foo', 'bar')->shouldBeCalled()->willReturn($request); - $request->hasHeader('baz')->shouldBeCalled()->willReturn(true); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/HeaderRemovePluginSpec.php b/spec/Plugin/HeaderRemovePluginSpec.php deleted file mode 100644 index 3f603595..00000000 --- a/spec/Plugin/HeaderRemovePluginSpec.php +++ /dev/null @@ -1,38 +0,0 @@ -beConstructedWith([]); - $this->shouldHaveType(HeaderRemovePlugin::class); - } - - public function it_is_a_plugin() - { - $this->beConstructedWith([]); - $this->shouldImplement(Plugin::class); - } - - public function it_removes_the_header(RequestInterface $request) - { - $this->beConstructedWith([ - 'foo', - 'baz', - ]); - - $request->hasHeader('foo')->shouldBeCalled()->willReturn(false); - - $request->hasHeader('baz')->shouldBeCalled()->willReturn(true); - $request->withoutHeader('baz')->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/HeaderSetPluginSpec.php b/spec/Plugin/HeaderSetPluginSpec.php deleted file mode 100644 index b152567f..00000000 --- a/spec/Plugin/HeaderSetPluginSpec.php +++ /dev/null @@ -1,36 +0,0 @@ -beConstructedWith([]); - $this->shouldHaveType(HeaderSetPlugin::class); - } - - public function it_is_a_plugin() - { - $this->beConstructedWith([]); - $this->shouldImplement(Plugin::class); - } - - public function it_set_the_header(RequestInterface $request) - { - $this->beConstructedWith([ - 'foo' => 'bar', - 'baz' => 'qux', - ]); - - $request->withHeader('foo', 'bar')->shouldBeCalled()->willReturn($request); - $request->withHeader('baz', 'qux')->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/HistoryPluginSpec.php b/spec/Plugin/HistoryPluginSpec.php deleted file mode 100644 index cd8459b0..00000000 --- a/spec/Plugin/HistoryPluginSpec.php +++ /dev/null @@ -1,58 +0,0 @@ -beConstructedWith($journal); - } - - public function it_is_initializable() - { - $this->beAnInstanceOf('Http\Client\Common\Plugin\JournalPlugin'); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_records_success(Journal $journal, RequestInterface $request, ResponseInterface $response) - { - $next = function (RequestInterface $receivedRequest) use ($request, $response) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($response->getWrappedObject()); - } - }; - - $journal->addSuccess($request, $response)->shouldBeCalled(); - - $this->handleRequest($request, $next, function () {}); - } - - public function it_records_failure(Journal $journal, RequestInterface $request) - { - $exception = new TransferException(); - $next = function (RequestInterface $receivedRequest) use ($request, $exception) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpRejectedPromise($exception); - } - }; - - $journal->addFailure($request, $exception)->shouldBeCalled(); - - $this->handleRequest($request, $next, function () {}); - } -} diff --git a/spec/Plugin/PluginStub.php b/spec/Plugin/PluginStub.php deleted file mode 100644 index ead2a575..00000000 --- a/spec/Plugin/PluginStub.php +++ /dev/null @@ -1,25 +0,0 @@ -beConstructedWith([]); - } - - public function it_is_initializable() - { - $this->shouldHaveType(QueryDefaultsPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_sets_the_default_header(RequestInterface $request, UriInterface $uri) - { - $this->beConstructedWith([ - 'foo' => 'bar', - ]); - - $request->getUri()->shouldBeCalled()->willReturn($uri); - $uri->getQuery()->shouldBeCalled()->willReturn('test=true'); - $uri->withQuery('test=true&foo=bar')->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_replace_existing_request_value(RequestInterface $request, UriInterface $uri) - { - $this->beConstructedWith([ - 'foo' => 'fooDefault', - 'bar' => 'barDefault', - ]); - - $request->getUri()->shouldBeCalled()->willReturn($uri); - $uri->getQuery()->shouldBeCalled()->willReturn('foo=new'); - $uri->withQuery('foo=new&bar=barDefault')->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/RedirectPluginSpec.php b/spec/Plugin/RedirectPluginSpec.php deleted file mode 100644 index 9bcfb7f2..00000000 --- a/spec/Plugin/RedirectPluginSpec.php +++ /dev/null @@ -1,516 +0,0 @@ -shouldHaveType(RedirectPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_redirects_on_302( - UriInterface $uri, - UriInterface $uriRedirect, - RequestInterface $request, - ResponseInterface $responseRedirect, - RequestInterface $modifiedRequest, - ResponseInterface $finalResponse, - Promise $promise - ) { - $this->beConstructedWith(['stream_factory' => null]); - $responseRedirect->getStatusCode()->willReturn(302); - $responseRedirect->hasHeader('Location')->willReturn(true); - $responseRedirect->getHeaderLine('Location')->willReturn('/redirect'); - - $request->getUri()->willReturn($uri); - $request->withUri($uriRedirect)->willReturn($modifiedRequest); - $uri->__toString()->willReturn('/original'); - - $uri->withPath('/redirect')->willReturn($uriRedirect); - $uriRedirect->withFragment('')->willReturn($uriRedirect); - $uriRedirect->withQuery('')->willReturn($uriRedirect); - $uriRedirect->__toString()->willReturn('/redirect'); - - $modifiedRequest->getUri()->willReturn($uriRedirect); - $modifiedRequest->getMethod()->willReturn('GET'); - - $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; - - $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { - if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { - return $promise->getWrappedObject(); - } - }; - - $promise->getState()->willReturn(Promise::FULFILLED); - $promise->wait()->shouldBeCalled()->willReturn($finalResponse); - - $finalPromise = $this->handleRequest($request, $next, $first); - $finalPromise->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); - $finalPromise->wait()->shouldReturn($finalResponse); - } - - public function it_use_storage_on_301( - UriInterface $uri, - UriInterface $uriRedirect, - RequestInterface $request, - RequestInterface $modifiedRequest, - ResponseInterface $finalResponse, - ResponseInterface $redirectResponse - ) { - $this->beConstructedWith(['stream_factory' => null]); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/original'); - $uri->withPath('/redirect')->willReturn($uriRedirect); - $uriRedirect->withQuery('')->willReturn($uriRedirect); - $uriRedirect->withFragment('')->willReturn($uriRedirect); - $request->withUri($uriRedirect)->willReturn($modifiedRequest); - - $modifiedRequest->getUri()->willReturn($uriRedirect); - $modifiedRequest->getMethod()->willReturn('GET'); - - $uriRedirect->__toString()->willReturn('/redirect'); - - $finalResponse->getStatusCode()->willReturn(200); - - $redirectResponse->getStatusCode()->willReturn(301); - $redirectResponse->hasHeader('Location')->willReturn(true); - $redirectResponse->getHeaderLine('Location')->willReturn('/redirect'); - - $nextCalled = false; - $next = function (RequestInterface $request) use (&$nextCalled, $finalResponse, $redirectResponse): Promise { - switch ($request->getUri()) { - case '/original': - if ($nextCalled) { - throw new \Exception('Must only be called once'); - } - $nextCalled = true; - - return new HttpFulfilledPromise($redirectResponse->getWrappedObject()); - case '/redirect': - - return new HttpFulfilledPromise($finalResponse->getWrappedObject()); - default: - throw new \Exception('Test setup error with request uri '.$request->getUri()); - } - }; - $first = $this->buildFirst($modifiedRequest, $next); - - $this->handleRequest($request, $next, $first); - - // rebuild first as this is expected to be called again - $first = $this->buildFirst($modifiedRequest, $next); - // next should not be called again - $this->handleRequest($request, $next, $first); - } - - private function buildFirst(RequestInterface $modifiedRequest, callable $next): callable - { - $redirectPlugin = $this; - $firstCalled = false; - - return function (RequestInterface $request) use (&$modifiedRequest, $redirectPlugin, $next, &$firstCalled) { - if ($firstCalled) { - throw new \Exception('Only one restart expected'); - } - $firstCalled = true; - if ($modifiedRequest->getWrappedObject() !== $request) { - //throw new \Exception('Redirection failed'); - } - - return $redirectPlugin->getWrappedObject()->handleRequest($request, $next, $this); - }; - } - - public function it_replace_full_url( - UriInterface $uri, - UriInterface $uriRedirect, - RequestInterface $request, - ResponseInterface $responseRedirect, - RequestInterface $modifiedRequest, - ResponseInterface $finalResponse, - Promise $promise - ) { - $this->beConstructedWith(['stream_factory' => null]); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/original'); - - $responseRedirect->getStatusCode()->willReturn(302); - $responseRedirect->hasHeader('Location')->willReturn(true); - $responseRedirect->getHeaderLine('Location')->willReturn('https://server.com:8000/redirect?query#fragment'); - - $request->getUri()->willReturn($uri); - $uri->withScheme('https')->willReturn($uriRedirect); - $uri->withPath('/redirect')->willReturn($uri); - $uri->withQuery('query')->willReturn($uri); - $uri->withFragment('fragment')->willReturn($uri); - $uriRedirect->withHost('server.com')->willReturn($uriRedirect); - $uriRedirect->withPort('8000')->willReturn($uriRedirect); - $uriRedirect->withPath('/redirect')->willReturn($uriRedirect); - $uriRedirect->withQuery('query')->willReturn($uriRedirect); - $uriRedirect->withFragment('fragment')->willReturn($uriRedirect); - - $request->withUri($uriRedirect)->willReturn($modifiedRequest); - - $modifiedRequest->getUri()->willReturn($uriRedirect); - $modifiedRequest->getMethod()->willReturn('GET'); - - $uriRedirect->__toString()->willReturn('/redirect'); - - $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; - - $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { - if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { - return $promise->getWrappedObject(); - } - }; - - $promise->getState()->willReturn(Promise::FULFILLED); - $promise->wait()->shouldBeCalled()->willReturn($finalResponse); - - $this->handleRequest($request, $next, $first); - } - - public function it_throws_http_exception_on_no_location(RequestInterface $request, UriInterface $uri, ResponseInterface $responseRedirect) - { - $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; - - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/original'); - $responseRedirect->getStatusCode()->willReturn(302); - $responseRedirect->hasHeader('Location')->willReturn(false); - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow(HttpException::class)->duringWait(); - } - - public function it_throws_http_exception_on_invalid_location(RequestInterface $request, UriInterface $uri, ResponseInterface $responseRedirect) - { - $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; - - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/original'); - $responseRedirect->getHeaderLine('Location')->willReturn('scheme:///invalid'); - - $responseRedirect->getStatusCode()->willReturn(302); - $responseRedirect->hasHeader('Location')->willReturn(true); - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow(HttpException::class)->duringWait(); - } - - public function it_throw_multi_redirect_exception_on_300(RequestInterface $request, ResponseInterface $responseRedirect) - { - $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; - - $this->beConstructedWith(['preserve_header' => true, 'use_default_for_multiple' => false]); - $responseRedirect->getStatusCode()->willReturn(300); - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow(MultipleRedirectionException::class)->duringWait(); - } - - public function it_throw_multi_redirect_exception_on_300_if_no_location(RequestInterface $request, ResponseInterface $responseRedirect) - { - $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; - - $responseRedirect->getStatusCode()->willReturn(300); - $responseRedirect->hasHeader('Location')->willReturn(false); - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow(MultipleRedirectionException::class)->duringWait(); - } - - public function it_switch_method_for_302( - UriInterface $uri, - UriInterface $uriRedirect, - RequestInterface $request, - ResponseInterface $responseRedirect, - RequestInterface $modifiedRequest, - ResponseInterface $finalResponse, - Promise $promise - ) { - $this->beConstructedWith(['stream_factory' => null]); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/original'); - - $responseRedirect->getStatusCode()->willReturn(302); - $responseRedirect->hasHeader('Location')->willReturn(true); - $responseRedirect->getHeaderLine('Location')->willReturn('/redirect'); - - $request->getUri()->willReturn($uri); - $uri->withPath('/redirect')->willReturn($uriRedirect); - $uriRedirect->withFragment('')->willReturn($uriRedirect); - $uriRedirect->withQuery('')->willReturn($uriRedirect); - - $request->withUri($uriRedirect)->willReturn($modifiedRequest); - $modifiedRequest->getUri()->willReturn($uriRedirect); - $uriRedirect->__toString()->willReturn('/redirect'); - $modifiedRequest->getMethod()->willReturn('POST'); - $modifiedRequest->withMethod('GET')->shouldBeCalled()->willReturn($modifiedRequest); - - $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; - - $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { - if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { - return $promise->getWrappedObject(); - } - }; - - $promise->getState()->willReturn(Promise::FULFILLED); - $promise->wait()->shouldBeCalled()->willReturn($finalResponse); - - $this->handleRequest($request, $next, $first); - } - - public function it_does_not_switch_method_for_302_with_strict_option( - UriInterface $uri, - UriInterface $uriRedirect, - RequestInterface $request, - ResponseInterface $responseRedirect, - RequestInterface $modifiedRequest, - ResponseInterface $finalResponse, - Promise $promise - ) { - $this->beConstructedWith(['strict' => true]); - - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/original'); - - $responseRedirect->getStatusCode()->willReturn(302); - $responseRedirect->hasHeader('Location')->willReturn(true); - $responseRedirect->getHeaderLine('Location')->willReturn('/redirect'); - - $request->getUri()->willReturn($uri); - $uri->withPath('/redirect')->willReturn($uriRedirect); - $uriRedirect->withFragment('')->willReturn($uriRedirect); - $uriRedirect->withQuery('')->willReturn($uriRedirect); - - $request->withUri($uriRedirect)->willReturn($modifiedRequest); - $modifiedRequest->getUri()->willReturn($uriRedirect); - $uriRedirect->__toString()->willReturn('/redirect'); - $modifiedRequest->getMethod()->willReturn('POST'); - $modifiedRequest->withMethod('GET')->shouldNotBeCalled(); - - $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; - - $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { - if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { - return $promise->getWrappedObject(); - } - }; - - $promise->getState()->willReturn(Promise::FULFILLED); - $promise->wait()->shouldBeCalled()->willReturn($finalResponse); - - $this->handleRequest($request, $next, $first); - } - - public function it_clears_headers( - UriInterface $uri, - UriInterface $uriRedirect, - RequestInterface $request, - ResponseInterface $responseRedirect, - RequestInterface $modifiedRequest, - ResponseInterface $finalResponse, - Promise $promise - ) { - $this->beConstructedWith([ - 'preserve_header' => ['Accept'], - 'stream_factory' => null, - ]); - - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/original'); - - $responseRedirect->getStatusCode()->willReturn(302); - $responseRedirect->hasHeader('Location')->willReturn(true); - $responseRedirect->getHeaderLine('Location')->willReturn('/redirect'); - - $request->getUri()->willReturn($uri); - $uri->withPath('/redirect')->willReturn($uriRedirect); - $uriRedirect->withFragment('')->willReturn($uriRedirect); - $uriRedirect->withQuery('')->willReturn($uriRedirect); - - $request->withUri($uriRedirect)->willReturn($modifiedRequest); - - $modifiedRequest->getUri()->willReturn($uriRedirect); - $uriRedirect->__toString()->willReturn('/redirect'); - $modifiedRequest->getMethod()->willReturn('GET'); - $modifiedRequest->getHeaders()->willReturn(['Accept' => 'value', 'Cookie' => 'value']); - $modifiedRequest->withoutHeader('Cookie')->willReturn($modifiedRequest); - $modifiedRequest->getUri()->willReturn($uriRedirect); - - $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; - - $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { - if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { - return $promise->getWrappedObject(); - } - }; - - $promise->getState()->willReturn(Promise::FULFILLED); - $promise->wait()->shouldBeCalled()->willReturn($finalResponse); - - $this->handleRequest($request, $next, $first); - } - - /** - * This is the "redirection does not redirect case. - */ - public function it_throws_circular_redirection_exception_on_redirect_that_does_not_change_url( - UriInterface $redirectUri, - RequestInterface $request, - ResponseInterface $redirectResponse - ) { - $redirectResponse->getStatusCode()->willReturn(302); - $redirectResponse->hasHeader('Location')->willReturn(true); - $redirectResponse->getHeaderLine('Location')->willReturn('/redirect'); - - $next = function () use ($redirectResponse): Promise { - return new HttpFulfilledPromise($redirectResponse->getWrappedObject()); - }; - - $first = function () { - throw new \Exception('First should never be called'); - }; - - $request->getUri()->willReturn($redirectUri); - $redirectUri->__toString()->willReturn('/redirect'); - - $redirectUri->withPath('/redirect')->willReturn($redirectUri); - $redirectUri->withFragment('')->willReturn($redirectUri); - $redirectUri->withQuery('')->willReturn($redirectUri); - - $request->withUri($redirectUri)->willReturn($request); - $redirectUri->__toString()->willReturn('/redirect'); - $request->getMethod()->willReturn('GET'); - - $promise = $this->handleRequest($request, $next, $first); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow(CircularRedirectionException::class)->duringWait(); - } - - /** - * This is a redirection flipping back and forth between two paths. - * - * There could be a larger loop but the logic in the plugin stays the same with as many redirects as needed. - */ - public function it_throws_circular_redirection_exception_on_alternating_redirect( - UriInterface $uri, - UriInterface $redirectUri, - RequestInterface $request, - ResponseInterface $redirectResponse1, - ResponseInterface $redirectResponse2, - RequestInterface $modifiedRequest - ) { - $redirectResponse1->getStatusCode()->willReturn(302); - $redirectResponse1->hasHeader('Location')->willReturn(true); - $redirectResponse1->getHeaderLine('Location')->willReturn('/redirect'); - - $redirectResponse2->getStatusCode()->willReturn(302); - $redirectResponse2->hasHeader('Location')->willReturn(true); - $redirectResponse2->getHeaderLine('Location')->willReturn('/original'); - - $next = function (RequestInterface $currentRequest) use ($request, $redirectResponse1, $redirectResponse2): Promise { - return ($currentRequest === $request->getWrappedObject()) - ? new HttpFulfilledPromise($redirectResponse1->getWrappedObject()) - : new HttpFulfilledPromise($redirectResponse2->getWrappedObject()) - ; - }; - - $redirectPlugin = $this; - $firstCalled = false; - $first = function (RequestInterface $request) use (&$firstCalled, $redirectPlugin, $next, &$first) { - if ($firstCalled) { - throw new \Exception('only one redirect expected'); - } - $firstCalled = true; - - return $redirectPlugin->getWrappedObject()->handleRequest($request, $next, $first); - }; - - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/original'); - - $modifiedRequest->getUri()->willReturn($redirectUri); - $redirectUri->__toString()->willReturn('/redirect'); - - $uri->withPath('/redirect')->willReturn($redirectUri); - $redirectUri->withFragment('')->willReturn($redirectUri); - $redirectUri->withQuery('')->willReturn($redirectUri); - - $redirectUri->withPath('/original')->willReturn($uri); - $uri->withFragment('')->willReturn($uri); - $uri->withQuery('')->willReturn($uri); - - $request->withUri($redirectUri)->willReturn($modifiedRequest); - $request->getMethod()->willReturn('GET'); - $modifiedRequest->withUri($uri)->willReturn($request); - $modifiedRequest->getMethod()->willReturn('GET'); - - $promise = $this->handleRequest($request, $next, $first); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow(CircularRedirectionException::class)->duringWait(); - } -} diff --git a/spec/Plugin/RequestMatcherPluginSpec.php b/spec/Plugin/RequestMatcherPluginSpec.php deleted file mode 100644 index 246dc292..00000000 --- a/spec/Plugin/RequestMatcherPluginSpec.php +++ /dev/null @@ -1,56 +0,0 @@ -beConstructedWith($requestMatcher, $plugin); - } - - public function it_is_initializable() - { - $this->shouldHaveType(RequestMatcherPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_matches_a_request_and_delegates_to_plugin( - RequestInterface $request, - RequestMatcher $requestMatcher, - Plugin $plugin - ) { - $requestMatcher->matches($request)->willReturn(true); - $plugin->handleRequest($request, Argument::type('callable'), Argument::type('callable'))->shouldBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_match_a_request( - RequestInterface $request, - RequestMatcher $requestMatcher, - Plugin $plugin, - Promise $promise - ) { - $requestMatcher->matches($request)->willReturn(false); - $plugin->handleRequest($request, Argument::type('callable'), Argument::type('callable'))->shouldNotBeCalled(); - - $next = function (RequestInterface $request) use ($promise) { - return $promise->getWrappedObject(); - }; - - $this->handleRequest($request, $next, function () {})->shouldReturn($promise); - } -} diff --git a/spec/Plugin/RequestSeekableBodyPluginSpec.php b/spec/Plugin/RequestSeekableBodyPluginSpec.php deleted file mode 100644 index fbb55309..00000000 --- a/spec/Plugin/RequestSeekableBodyPluginSpec.php +++ /dev/null @@ -1,46 +0,0 @@ -shouldHaveType(RequestSeekableBodyPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_decorate_request_body_if_not_seekable(RequestInterface $request, StreamInterface $requestStream) - { - $request->getBody()->shouldBeCalled()->willReturn($requestStream); - $requestStream->isSeekable()->shouldBeCalled()->willReturn(false); - $requestStream->getSize()->willReturn(null); - - $request->withBody(Argument::type(BufferedStream::class))->shouldBeCalled()->willReturn($request); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } - - public function it_does_not_decorate_request_body_if_seekable(RequestInterface $request, StreamInterface $requestStream) - { - $request->getBody()->shouldBeCalled()->willReturn($requestStream); - $requestStream->isSeekable()->shouldBeCalled()->willReturn(true); - $requestStream->getSize()->willReturn(null); - - $request->withBody(Argument::type(BufferedStream::class))->shouldNotBeCalled(); - - $this->handleRequest($request, PluginStub::next(), function () {}); - } -} diff --git a/spec/Plugin/ResponseSeekableBodyPluginSpec.php b/spec/Plugin/ResponseSeekableBodyPluginSpec.php deleted file mode 100644 index 4f750273..00000000 --- a/spec/Plugin/ResponseSeekableBodyPluginSpec.php +++ /dev/null @@ -1,56 +0,0 @@ -shouldHaveType(ResponseSeekableBodyPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_decorate_response_body_if_not_seekable(ResponseInterface $response, StreamInterface $responseStream) - { - $next = function () use ($response) { - return new HttpFulfilledPromise($response->getWrappedObject()); - }; - - $response->getBody()->shouldBeCalled()->willReturn($responseStream); - $responseStream->isSeekable()->shouldBeCalled()->willReturn(false); - $responseStream->getSize()->willReturn(null); - - $response->withBody(Argument::type(BufferedStream::class))->shouldBeCalled()->willReturn($response); - - $this->handleRequest(new Request('GET', '/'), $next, function () {}); - } - - public function it_does_not_decorate_response_body_if_seekable(ResponseInterface $response, StreamInterface $responseStream) - { - $next = function () use ($response) { - return new HttpFulfilledPromise($response->getWrappedObject()); - }; - - $response->getBody()->shouldBeCalled()->willReturn($responseStream); - $responseStream->isSeekable()->shouldBeCalled()->willReturn(true); - $responseStream->getSize()->willReturn(null); - - $response->withBody(Argument::type(BufferedStream::class))->shouldNotBeCalled(); - - $this->handleRequest(new Request('GET', '/'), $next, function () {}); - } -} diff --git a/spec/Plugin/RetryPluginSpec.php b/spec/Plugin/RetryPluginSpec.php deleted file mode 100644 index 4b749a76..00000000 --- a/spec/Plugin/RetryPluginSpec.php +++ /dev/null @@ -1,168 +0,0 @@ -shouldHaveType(RetryPlugin::class); - } - - public function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - public function it_returns_response(RequestInterface $request, ResponseInterface $response) - { - $next = function (RequestInterface $receivedRequest) use ($request, $response) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($response->getWrappedObject()); - } - }; - - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); - } - - public function it_throws_exception_on_multiple_exceptions(RequestInterface $request) - { - $exception1 = new Exception\NetworkException('Exception 1', $request->getWrappedObject()); - $exception2 = new Exception\NetworkException('Exception 2', $request->getWrappedObject()); - - $count = 0; - $next = function (RequestInterface $receivedRequest) use ($request, $exception1, $exception2, &$count) { - ++$count; - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - if (1 == $count) { - return new HttpRejectedPromise($exception1); - } - - if (2 == $count) { - return new HttpRejectedPromise($exception2); - } - } - }; - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow($exception2)->duringWait(); - } - - public function it_does_not_retry_client_errors(RequestInterface $request, ResponseInterface $response) - { - $exception = new Exception\HttpException('Exception', $request->getWrappedObject(), $response->getWrappedObject()); - - $seen = false; - $next = function (RequestInterface $receivedRequest) use ($request, $exception, &$seen) { - if (!Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - throw new \Exception('Unexpected request received'); - } - if ($seen) { - throw new \Exception('This should only be called once'); - } - $seen = true; - - return new HttpRejectedPromise($exception); - }; - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); - $promise->shouldThrow($exception)->duringWait(); - } - - public function it_returns_response_on_second_try(RequestInterface $request, ResponseInterface $response) - { - $exception = new Exception\NetworkException('Exception 1', $request->getWrappedObject()); - - $count = 0; - $next = function (RequestInterface $receivedRequest) use ($request, $exception, $response, &$count) { - ++$count; - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - if (1 == $count) { - return new HttpRejectedPromise($exception); - } - - if (2 == $count) { - return new HttpFulfilledPromise($response->getWrappedObject()); - } - } - }; - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); - $promise->wait()->shouldReturn($response); - } - - public function it_respects_custom_exception_decider(RequestInterface $request, ResponseInterface $response) - { - $this->beConstructedWith([ - 'exception_decider' => function (RequestInterface $request, Exception $e) { - return false; - }, - ]); - $exception = new Exception\NetworkException('Exception', $request->getWrappedObject()); - - $called = false; - $next = function (RequestInterface $receivedRequest) use ($exception, &$called) { - if ($called) { - throw new \RuntimeException('Did not expect to be called multiple times'); - } - $called = true; - - return new HttpRejectedPromise($exception); - }; - - $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); - $promise->shouldThrow($exception)->duringWait(); - } - - public function it_does_not_keep_history_of_old_failure(RequestInterface $request, ResponseInterface $response) - { - $exception = new Exception\NetworkException('Exception 1', $request->getWrappedObject()); - - $count = 0; - $next = function (RequestInterface $receivedRequest) use ($request, $exception, $response, &$count) { - ++$count; - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - if (1 == $count % 2) { - return new HttpRejectedPromise($exception); - } - - if (0 == $count % 2) { - return new HttpFulfilledPromise($response->getWrappedObject()); - } - } - }; - - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); - } - - public function it_has_an_exponential_default_error_response_delay(RequestInterface $request, ResponseInterface $response) - { - $this->defaultErrorResponseDelay($request, $response, 0)->shouldBe(500000); - $this->defaultErrorResponseDelay($request, $response, 1)->shouldBe(1000000); - $this->defaultErrorResponseDelay($request, $response, 2)->shouldBe(2000000); - $this->defaultErrorResponseDelay($request, $response, 3)->shouldBe(4000000); - } - - public function it_has_an_exponential_default_exception_delay(RequestInterface $request, Exception\HttpException $exception) - { - $this->defaultExceptionDelay($request, $exception, 0)->shouldBe(500000); - $this->defaultExceptionDelay($request, $exception, 1)->shouldBe(1000000); - $this->defaultExceptionDelay($request, $exception, 2)->shouldBe(2000000); - $this->defaultExceptionDelay($request, $exception, 3)->shouldBe(4000000); - } -} diff --git a/spec/PluginClientFactorySpec.php b/spec/PluginClientFactorySpec.php deleted file mode 100644 index 6b5868d9..00000000 --- a/spec/PluginClientFactorySpec.php +++ /dev/null @@ -1,28 +0,0 @@ -shouldHaveType(PluginClientFactory::class); - } - - public function it_returns_a_plugin_client(HttpClient $httpClient) - { - $client = $this->createClient($httpClient); - - $client->shouldHaveType(PluginClient::class); - } - - public function it_does_not_construct_plugin_client_with_client_name_option(HttpClient $httpClient) - { - $this->createClient($httpClient, [], ['client_name' => 'Default']); - } -} diff --git a/spec/PluginClientSpec.php b/spec/PluginClientSpec.php deleted file mode 100644 index fb6b1535..00000000 --- a/spec/PluginClientSpec.php +++ /dev/null @@ -1,91 +0,0 @@ -beConstructedWith($httpClient); - } - - public function it_is_initializable() - { - $this->shouldHaveType(PluginClient::class); - } - - public function it_is_an_http_client() - { - $this->shouldImplement(HttpClient::class); - } - - public function it_is_an_http_async_client() - { - $this->shouldImplement(HttpAsyncClient::class); - } - - public function it_sends_request_with_underlying_client(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) - { - $httpClient->sendRequest($request)->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - } - - public function it_sends_async_request_with_underlying_client(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) - { - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - - $this->beConstructedWith($httpAsyncClient); - $this->sendAsyncRequest($request)->shouldReturn($promise); - } - - public function it_sends_async_request_if_no_send_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, ResponseInterface $response, Promise $promise) - { - $this->beConstructedWith($httpAsyncClient); - $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - $promise->wait()->willReturn($response); - - $this->sendRequest($request)->shouldReturn($response); - } - - public function it_prefers_send_request($client, RequestInterface $request, ResponseInterface $response) - { - $client->implement(HttpClient::class); - $client->implement(HttpAsyncClient::class); - - $client->sendRequest($request)->willReturn($response); - - $this->beConstructedWith($client); - - $this->sendRequest($request)->shouldReturn($response); - } - - public function it_throws_loop_exception(HttpClient $httpClient, RequestInterface $request, Plugin $plugin) - { - $plugin - ->handleRequest( - $request, - Argument::type('callable'), - Argument::type('callable') - ) - ->will(function ($args) { - return $args[2]($args[0]); - }) - ; - - $this->beConstructedWith($httpClient, [$plugin]); - - $this->shouldThrow(LoopException::class)->duringSendRequest($request); - } -} diff --git a/tests/BatchClientTest.php b/tests/BatchClientTest.php new file mode 100644 index 00000000..6d2791dc --- /dev/null +++ b/tests/BatchClientTest.php @@ -0,0 +1,62 @@ +createMock(ClientInterface::class); + $request1 = $this->createMock(RequestInterface::class); + $request2 = $this->createMock(RequestInterface::class); + $response1 = $this->createMock(ResponseInterface::class); + $response2 = $this->createMock(ResponseInterface::class); + + $client + ->expects($this->exactly(2)) + ->method('sendRequest') + ->willReturnMap([ + [$request1, $response1], + [$request2, $response2], + ]); + + $result = (new BatchClient($client))->sendRequests([$request1, $request2]); + + $this->assertTrue($result->isSuccessful($request1)); + $this->assertTrue($result->isSuccessful($request2)); + } + + public function testThrowsBatchExceptionWhenAnyRequestFails(): void + { + $client = $this->createMock(ClientInterface::class); + $request1 = $this->createMock(RequestInterface::class); + $request2 = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $exception = new HttpException('failed', $request2, $response); + + $client + ->expects($this->exactly(2)) + ->method('sendRequest') + ->willReturnOnConsecutiveCalls($response, $this->throwException($exception)); + + $this->expectException(BatchException::class); + try { + (new BatchClient($client))->sendRequests([$request1, $request2]); + } catch (BatchException $e) { + $this->assertInstanceOf(BatchResult::class, $e->getResult()); + + throw $e; + } + } +} diff --git a/tests/BatchResultTest.php b/tests/BatchResultTest.php new file mode 100644 index 00000000..83db096c --- /dev/null +++ b/tests/BatchResultTest.php @@ -0,0 +1,59 @@ +createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $result = new BatchResult(); + $newResult = $result->addResponse($request, $response); + + $this->assertFalse($result->hasResponses()); + $this->assertSame([], $result->getResponses()); + $this->assertTrue($newResult->hasResponses()); + $this->assertSame([$response], $newResult->getResponses()); + } + + public function testResponseLookups(): void + { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $result = (new BatchResult())->addResponse($request, $response); + $this->assertTrue($result->isSuccessful($request)); + $this->assertSame($response, $result->getResponseFor($request)); + + $this->expectException(\UnexpectedValueException::class); + (new BatchResult())->getResponseFor($request); + } + + public function testKeepsExceptionsWhenAddingResponses(): void + { + $requestFailed = $this->createMock(RequestInterface::class); + $requestSucceeded = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $exception = $this->createMock(Exception::class); + + $result = (new BatchResult()) + ->addException($requestFailed, $exception) + ->addResponse($requestSucceeded, $response); + + $this->assertTrue($result->isFailed($requestFailed)); + $this->assertSame($exception, $result->getExceptionFor($requestFailed)); + $this->assertFalse($result->isFailed($requestSucceeded)); + $this->assertTrue($result->isSuccessful($requestSucceeded)); + $this->assertSame($response, $result->getResponseFor($requestSucceeded)); + } +} diff --git a/tests/EmulatedHttpAsyncClientTest.php b/tests/EmulatedHttpAsyncClientTest.php new file mode 100644 index 00000000..5c359f6d --- /dev/null +++ b/tests/EmulatedHttpAsyncClientTest.php @@ -0,0 +1,64 @@ +createMock(ClientInterface::class); + $emulated = new EmulatedHttpAsyncClient($client); + + $this->assertInstanceOf(HttpClient::class, $emulated); + $this->assertInstanceOf(HttpAsyncClient::class, $emulated); + } + + public function testSendAsyncRequestWrapsSuccessfulResponse(): void + { + $client = $this->createMock(ClientInterface::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $client->expects($this->once())->method('sendRequest')->with($request)->willReturn($response); + + $promise = (new EmulatedHttpAsyncClient($client))->sendAsyncRequest($request); + $this->assertInstanceOf(HttpFulfilledPromise::class, $promise); + $this->assertSame($response, $promise->wait()); + } + + public function testSendAsyncRequestWrapsFailures(): void + { + $client = $this->createMock(ClientInterface::class); + $request = $this->createMock(RequestInterface::class); + $exception = new TransferException('failed'); + $client->expects($this->once())->method('sendRequest')->with($request)->willThrowException($exception); + + $promise = (new EmulatedHttpAsyncClient($client))->sendAsyncRequest($request); + $this->assertInstanceOf(HttpRejectedPromise::class, $promise); + $this->expectException(\RuntimeException::class); + $promise->wait(); + } + + public function testSendRequestDelegatesToClient(): void + { + $client = $this->createMock(ClientInterface::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $client->expects($this->once())->method('sendRequest')->with($request)->willReturn($response); + + $result = (new EmulatedHttpAsyncClient($client))->sendRequest($request); + $this->assertSame($response, $result); + } +} diff --git a/tests/EmulatedHttpClientTest.php b/tests/EmulatedHttpClientTest.php new file mode 100644 index 00000000..6dbe2587 --- /dev/null +++ b/tests/EmulatedHttpClientTest.php @@ -0,0 +1,65 @@ +createMock(HttpAsyncClient::class); + $client = new EmulatedHttpClient($async); + + $this->assertInstanceOf(HttpClient::class, $client); + $this->assertInstanceOf(HttpAsyncClient::class, $client); + } + + public function testSendRequestWaitsOnFulfilledPromise(): void + { + $async = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $async->expects($this->once())->method('sendAsyncRequest')->with($request)->willReturn(new FulfilledPromise($response)); + + $client = new EmulatedHttpClient($async); + + $this->assertSame($response, $client->sendRequest($request)); + } + + public function testSendRequestThrowsOnRejectedPromise(): void + { + $async = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $exception = new TransferException('failed'); + + $async->expects($this->once())->method('sendAsyncRequest')->with($request)->willReturn(new RejectedPromise($exception)); + + $client = new EmulatedHttpClient($async); + + $this->expectException(TransferException::class); + $client->sendRequest($request); + } + + public function testSendAsyncRequestDelegates(): void + { + $async = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $promise = new FulfilledPromise($this->createMock(ResponseInterface::class)); + + $async->expects($this->once())->method('sendAsyncRequest')->with($request)->willReturn($promise); + + $this->assertSame($promise, (new EmulatedHttpClient($async))->sendAsyncRequest($request)); + } +} diff --git a/tests/Exception/BatchExceptionTest.php b/tests/Exception/BatchExceptionTest.php new file mode 100644 index 00000000..822f83bc --- /dev/null +++ b/tests/Exception/BatchExceptionTest.php @@ -0,0 +1,28 @@ +assertInstanceOf(\RuntimeException::class, $exception); + $this->assertInstanceOf(Exception::class, $exception); + } + + public function testStoresBatchResult(): void + { + $result = new BatchResult(); + $exception = new BatchException($result); + + $this->assertSame($result, $exception->getResult()); + } +} diff --git a/tests/FlexibleHttpClientTest.php b/tests/FlexibleHttpClientTest.php new file mode 100644 index 00000000..03bd4586 --- /dev/null +++ b/tests/FlexibleHttpClientTest.php @@ -0,0 +1,97 @@ +expectException(\TypeError::class); + new FlexibleHttpClient(null); + } + + public function testEmulatesAsyncClientWhenOnlySyncAvailable(): void + { + $httpClient = $this->createMock(HttpClient::class); + $requestSync = $this->createMock(RequestInterface::class); + $requestAsync = $this->createMock(RequestInterface::class); + $responseSync = $this->createMock(ResponseInterface::class); + $responseAsync = $this->createMock(ResponseInterface::class); + + $httpClient->expects($this->exactly(2)) + ->method('sendRequest') + ->willReturnMap([ + [$requestSync, $responseSync], + [$requestAsync, $responseAsync], + ]); + + $client = new FlexibleHttpClient($httpClient); + + $this->assertSame($responseSync, $client->sendRequest($requestSync)); + $promise = $client->sendAsyncRequest($requestAsync); + $this->assertInstanceOf(Promise::class, $promise); + $this->assertSame($responseAsync, $promise->wait()); + } + + public function testEmulatesSyncClientWhenOnlyAsyncAvailable(): void + { + $httpAsyncClient = $this->createMock(HttpAsyncClient::class); + $requestAsync = $this->createMock(RequestInterface::class); + $promiseAsync = $this->createMock(Promise::class); + $requestSync = $this->createMock(RequestInterface::class); + $responseSync = $this->createMock(ResponseInterface::class); + + $httpAsyncClient->expects($this->exactly(2)) + ->method('sendAsyncRequest') + ->willReturnMap([ + [$requestAsync, $promiseAsync], + [$requestSync, new FulfilledPromise($responseSync)], + ]); + + $promiseAsync->expects($this->never())->method('wait'); + + $client = new FlexibleHttpClient($httpAsyncClient); + $this->assertSame($promiseAsync, $client->sendAsyncRequest($requestAsync)); + $this->assertSame($responseSync, $client->sendRequest($requestSync)); + } + + public function testUsesNativeImplementationsWhenClientSupportsBoth(): void + { + $client = new class implements HttpClient, HttpAsyncClient { + public $syncCalls = 0; + public $asyncCalls = 0; + + public function sendRequest(RequestInterface $request): ResponseInterface + { + ++$this->syncCalls; + + return new \Nyholm\Psr7\Response(); + } + + public function sendAsyncRequest(RequestInterface $request) + { + ++$this->asyncCalls; + + return new FulfilledPromise(new \Nyholm\Psr7\Response()); + } + }; + + $flexible = new FlexibleHttpClient($client); + $flexible->sendRequest($this->createMock(RequestInterface::class)); + $flexible->sendAsyncRequest($this->createMock(RequestInterface::class)); + + $this->assertSame(1, $client->syncCalls); + $this->assertSame(1, $client->asyncCalls); + } +} diff --git a/tests/HttpClientPool/HttpClientPoolItemTest.php b/tests/HttpClientPool/HttpClientPoolItemTest.php new file mode 100644 index 00000000..78b96026 --- /dev/null +++ b/tests/HttpClientPool/HttpClientPoolItemTest.php @@ -0,0 +1,181 @@ +createMock(HttpClient::class); + $item = new HttpClientPoolItem($client); + $this->assertInstanceOf(HttpClient::class, $item); + $this->assertInstanceOf(HttpAsyncClient::class, $item); + } + + public function testSendRequestSuccess(): void + { + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $client->expects($this->once())->method('sendRequest')->with($request)->willReturn($response); + + $item = new HttpClientPoolItem($client); + $this->assertSame($response, $item->sendRequest($request)); + } + + public function testSendRequestDisablesOnFailure(): void + { + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $exception = new TransferException(); + $client->expects($this->once())->method('sendRequest')->willThrowException($exception); + + $item = new HttpClientPoolItem($client); + try { + $item->sendRequest($request); + $this->fail('Expected exception'); + } catch (TransferException $e) { + $this->assertTrue($item->isDisabled()); + } + + $this->expectException(RequestException::class); + $item->sendRequest($request); + } + + public function testSendRequestReenablesWhenRetryDelayZero(): void + { + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $exception = new TransferException(); + $client->expects($this->exactly(2))->method('sendRequest')->with($request)->willThrowException($exception); + + $item = new HttpClientPoolItem($client, 0); + + try { + $item->sendRequest($request); + } catch (TransferException $e) { + $this->assertFalse($item->isDisabled()); + } + + $this->expectException(TransferException::class); + $item->sendRequest($request); + } + + public function testSendAsyncRequestDisablesOnFailure(): void + { + $client = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $promise = new HttpRejectedPromise(new TransferException()); + $client->expects($this->once())->method('sendAsyncRequest')->willReturn($promise); + + $item = new HttpClientPoolItem($client); + $this->assertInstanceOf(HttpRejectedPromise::class, $item->sendAsyncRequest($request)); + $this->assertTrue($item->isDisabled()); + + $this->expectException(RequestException::class); + $item->sendAsyncRequest($request); + } + + public function testSendAsyncRequestReenablesWhenRetryDelayZero(): void + { + $client = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $promise = new HttpRejectedPromise(new TransferException()); + $client->expects($this->exactly(2))->method('sendAsyncRequest')->willReturn($promise); + + $item = new HttpClientPoolItem($client, 0); + $item->sendAsyncRequest($request); + $this->assertFalse($item->isDisabled()); + $this->assertInstanceOf(HttpRejectedPromise::class, $item->sendAsyncRequest($request)); + } + + public function testRequestCountTracksPendingPromises(): void + { + $client = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $promise = new NotResolvingPromise($response); + + $client->expects($this->exactly(2))->method('sendAsyncRequest')->with($request)->willReturn($promise); + + $item = new HttpClientPoolItem($client, 0); + + $this->assertSame(0, $item->getSendingRequestCount()); + $item->sendAsyncRequest($request); + $this->assertSame(1, $item->getSendingRequestCount()); + $item->sendAsyncRequest($request); + $this->assertSame(2, $item->getSendingRequestCount()); + $promise->wait(false); + $this->assertSame(0, $item->getSendingRequestCount()); + } +} + +class NotResolvingPromise implements Promise +{ + private $queue = []; + + private $state = Promise::PENDING; + + private $response; + + private $exception; + + public function __construct(?ResponseInterface $response = null, ?\Http\Client\Exception $exception = null) + { + $this->response = $response; + $this->exception = $exception; + } + + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) + { + $this->queue[] = [$onFulfilled, $onRejected]; + + return $this; + } + + public function getState() + { + return $this->state; + } + + public function wait($unwrap = true) + { + while (count($this->queue) > 0) { + [$onFulfilled, $onRejected] = array_shift($this->queue); + + if (null !== $this->response && null !== $onFulfilled) { + $this->response = $onFulfilled($this->response); + $this->exception = null; + } elseif (null !== $this->exception && null !== $onRejected) { + $this->response = null; + $this->exception = $onRejected($this->exception); + } + } + + if (null !== $this->response) { + $this->state = Promise::FULFILLED; + + return $this->response; + } + + if (null !== $this->exception) { + $this->state = Promise::REJECTED; + + throw $this->exception; + } + } +} diff --git a/tests/HttpClientPool/LeastUsedClientPoolTest.php b/tests/HttpClientPool/LeastUsedClientPoolTest.php new file mode 100644 index 00000000..505b1966 --- /dev/null +++ b/tests/HttpClientPool/LeastUsedClientPoolTest.php @@ -0,0 +1,110 @@ +assertInstanceOf(HttpClient::class, $pool); + $this->assertInstanceOf(HttpAsyncClient::class, $pool); + } + + public function testThrowsWhenNoClientAvailable(): void + { + $pool = new LeastUsedClientPool(); + $request = $this->createMock(RequestInterface::class); + + $this->expectException(HttpClientNotFoundException::class); + $pool->sendRequest($request); + } + + public function testSendRequestUsesLeastBusyClient(): void + { + $pool = new LeastUsedClientPool(); + $client1 = $this->createMock(HttpClientPoolItem::class); + $client2 = $this->createMock(HttpClientPoolItem::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $pool->addHttpClient($client1); + $pool->addHttpClient($client2); + + $client1->method('getSendingRequestCount')->willReturn(5); + $client2->method('getSendingRequestCount')->willReturn(1); + $client1->method('isDisabled')->willReturn(false); + $client2->method('isDisabled')->willReturn(false); + $client2->expects($this->once())->method('sendRequest')->with($request)->willReturn($response); + $client1->expects($this->never())->method('sendRequest'); + + $this->assertSame($response, $pool->sendRequest($request)); + } + + public function testDisablingAllClientsThrows(): void + { + $pool = new LeastUsedClientPool(); + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $pool->addHttpClient($client); + + $client->expects($this->once())->method('sendRequest')->willThrowException(new HttpException('fail', $request, $response)); + + $this->expectException(HttpException::class); + $pool->sendRequest($request); + + $this->expectException(HttpClientNotFoundException::class); + $pool->sendRequest($request); + } + + public function testClientWithZeroRetryDelayGetsReenabled(): void + { + $pool = new LeastUsedClientPool(); + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $pool->addHttpClient(new HttpClientPoolItem($client, 0)); + + $client->expects($this->exactly(2))->method('sendRequest')->with($request)->willThrowException(new HttpException('fail', $request, $response)); + + try { + $pool->sendRequest($request); + $this->fail('Expected HttpException'); + } catch (HttpException $e) { + } + + try { + $pool->sendRequest($request); + $this->fail('Expected HttpException'); + } catch (HttpException $e) { + } + } + + public function testSendAsyncRequestDelegates(): void + { + $pool = new LeastUsedClientPool(); + $client = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $promise = $this->createMock(Promise::class); + + $pool->addHttpClient($client); + $client->expects($this->once())->method('sendAsyncRequest')->with($request)->willReturn($promise); + $promise->method('then')->willReturn($promise); + + $this->assertSame($promise, $pool->sendAsyncRequest($request)); + } +} diff --git a/tests/HttpClientPool/RandomClientPoolTest.php b/tests/HttpClientPool/RandomClientPoolTest.php new file mode 100644 index 00000000..5a4ee7b4 --- /dev/null +++ b/tests/HttpClientPool/RandomClientPoolTest.php @@ -0,0 +1,95 @@ +createMock(RequestInterface::class); + + $this->expectException(HttpClientNotFoundException::class); + $pool->sendRequest($request); + } + + public function testSendRequestDelegates(): void + { + $pool = new RandomClientPool(); + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $pool->addHttpClient($client); + + $client->expects($this->once())->method('sendRequest')->with($request)->willReturn($response); + + $this->assertSame($response, $pool->sendRequest($request)); + } + + public function testSendAsyncRequestDelegates(): void + { + $pool = new RandomClientPool(); + $client = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $promise = $this->createMock(Promise::class); + $pool->addHttpClient($client); + + $client->expects($this->once())->method('sendAsyncRequest')->with($request)->willReturn($promise); + $promise->method('then')->willReturn($promise); + + $this->assertSame($promise, $pool->sendAsyncRequest($request)); + } + + public function testDisablingClientsThrows(): void + { + $pool = new RandomClientPool(); + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $pool->addHttpClient($client); + + $client->expects($this->once())->method('sendRequest')->willThrowException(new HttpException('fail', $request, $response)); + + $this->expectException(HttpException::class); + $pool->sendRequest($request); + + $this->expectException(HttpClientNotFoundException::class); + $pool->sendRequest($request); + } + + public function testClientWithZeroRetryDelayGetsReenabled(): void + { + $pool = new RandomClientPool(); + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $pool->addHttpClient(new HttpClientPoolItem($client, 0)); + + $client->expects($this->exactly(2))->method('sendRequest')->with($request)->willThrowException(new HttpException('fail', $request, $response)); + + try { + $pool->sendRequest($request); + $this->fail('Expected HttpException'); + } catch (HttpException $e) { + } + + try { + $pool->sendRequest($request); + $this->fail('Expected HttpException'); + } catch (HttpException $e) { + } + } +} diff --git a/tests/HttpClientPool/RoundRobinClientPoolTest.php b/tests/HttpClientPool/RoundRobinClientPoolTest.php new file mode 100644 index 00000000..6ac2d4e6 --- /dev/null +++ b/tests/HttpClientPool/RoundRobinClientPoolTest.php @@ -0,0 +1,98 @@ +createMock(HttpClient::class); + $client2 = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $pool->addHttpClient($client1); + $pool->addHttpClient($client2); + + $client1->expects($this->once())->method('sendRequest')->willReturn($response); + $client2->expects($this->once())->method('sendRequest')->willReturn($response); + + $pool->sendRequest($request); + $pool->sendRequest($request); + } + + public function testThrowsWhenNoClients(): void + { + $pool = new RoundRobinClientPool(); + $this->expectException(HttpClientNotFoundException::class); + $pool->sendRequest($this->createMock(RequestInterface::class)); + } + + public function testSendAsyncRequestDelegates(): void + { + $pool = new RoundRobinClientPool(); + $client = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $promise = $this->createMock(Promise::class); + $pool->addHttpClient($client); + + $client->expects($this->once())->method('sendAsyncRequest')->with($request)->willReturn($promise); + $promise->method('then')->willReturn($promise); + + $this->assertSame($promise, $pool->sendAsyncRequest($request)); + } + + public function testDisablingClientsThrows(): void + { + $pool = new RoundRobinClientPool(); + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $pool->addHttpClient($client); + + $client->expects($this->once())->method('sendRequest')->willThrowException(new HttpException('fail', $request, $response)); + + $this->expectException(HttpException::class); + $pool->sendRequest($request); + + $this->expectException(HttpClientNotFoundException::class); + $pool->sendRequest($request); + } + + public function testClientWithZeroRetryDelayGetsReenabled(): void + { + $pool = new RoundRobinClientPool(); + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $pool->addHttpClient(new HttpClientPoolItem($client, 0)); + + $client->expects($this->exactly(2))->method('sendRequest')->willThrowException(new HttpException('fail', $request, $response)); + + try { + $pool->sendRequest($request); + $this->fail('Expected HttpException'); + } catch (HttpException $e) { + } + + try { + $pool->sendRequest($request); + $this->fail('Expected HttpException'); + } catch (HttpException $e) { + } + } +} diff --git a/tests/HttpClientRouterTest.php b/tests/HttpClientRouterTest.php new file mode 100644 index 00000000..c0978374 --- /dev/null +++ b/tests/HttpClientRouterTest.php @@ -0,0 +1,87 @@ +assertInstanceOf(HttpClientRouterInterface::class, $router); + $this->assertInstanceOf(HttpClient::class, $router); + $this->assertInstanceOf(HttpAsyncClient::class, $router); + } + + public function testSendRequestDelegatesToMatchingClient(): void + { + $router = new HttpClientRouter(); + $matcher = $this->createMock(RequestMatcher::class); + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $router->addClient($client, $matcher); + + $matcher->expects($this->once())->method('matches')->with($request)->willReturn(true); + $client->expects($this->once())->method('sendRequest')->with($request)->willReturn($response); + + $this->assertSame($response, $router->sendRequest($request)); + } + + public function testSendAsyncRequestDelegates(): void + { + $router = new HttpClientRouter(); + $matcher = $this->createMock(RequestMatcher::class); + $client = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + $promise = $this->createMock(Promise::class); + + $router->addClient($client, $matcher); + + $matcher->expects($this->once())->method('matches')->with($request)->willReturn(true); + $client->expects($this->once())->method('sendAsyncRequest')->with($request)->willReturn($promise); + + $this->assertSame($promise, $router->sendAsyncRequest($request)); + } + + public function testSendRequestThrowsWhenNoClientMatches(): void + { + $router = new HttpClientRouter(); + $matcher = $this->createMock(RequestMatcher::class); + $client = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + + $router->addClient($client, $matcher); + $matcher->expects($this->once())->method('matches')->willReturn(false); + + $this->expectException(HttpClientNoMatchException::class); + $router->sendRequest($request); + } + + public function testSendAsyncRequestThrowsWhenNoClientMatches(): void + { + $router = new HttpClientRouter(); + $matcher = $this->createMock(RequestMatcher::class); + $client = $this->createMock(HttpAsyncClient::class); + $request = $this->createMock(RequestInterface::class); + + $router->addClient($client, $matcher); + $matcher->expects($this->once())->method('matches')->willReturn(false); + + $this->expectException(HttpClientNoMatchException::class); + $router->sendAsyncRequest($request); + } +} diff --git a/tests/Plugin/AddHostPluginTest.php b/tests/Plugin/AddHostPluginTest.php new file mode 100644 index 00000000..a24bacd2 --- /dev/null +++ b/tests/Plugin/AddHostPluginTest.php @@ -0,0 +1,65 @@ +handle($plugin, $request, function (RequestInterface $request): void { + $this->assertSame('example.com', $request->getUri()->getHost()); + $this->assertSame('http', $request->getUri()->getScheme()); + $this->assertSame(8000, $request->getUri()->getPort()); + }); + } + + public function testReplacesHostWhenConfigured(): void + { + $plugin = new AddHostPlugin(new Uri('https://example.com:8443'), ['replace' => true]); + $request = new Request('GET', 'http://old.com/path'); + + $this->handle($plugin, $request, function (RequestInterface $request): void { + $this->assertSame('example.com', $request->getUri()->getHost()); + $this->assertSame('https', $request->getUri()->getScheme()); + $this->assertSame(8443, $request->getUri()->getPort()); + }); + } + + public function testDoesNothingWhenHostAlreadyPresent(): void + { + $plugin = new AddHostPlugin(new Uri('https://example.com:443')); + $request = new Request('GET', 'http://existing.com/foo'); + + $this->handle($plugin, $request, function (RequestInterface $request) { + $this->assertSame('existing.com', $request->getUri()->getHost()); + }); + } + + private function handle(AddHostPlugin $plugin, RequestInterface $request, callable $assert): void + { + $plugin->handleRequest( + $request, + function (RequestInterface $request) use ($assert) { + $assert($request); + + return new HttpFulfilledPromise(new Response()); + }, + static function () { + return new HttpFulfilledPromise(new Response()); + } + )->wait(); + } +} diff --git a/tests/Plugin/AuthenticationPluginTest.php b/tests/Plugin/AuthenticationPluginTest.php new file mode 100644 index 00000000..70c1111c --- /dev/null +++ b/tests/Plugin/AuthenticationPluginTest.php @@ -0,0 +1,34 @@ +createMock(Authentication::class); + $request = $this->createMock(RequestInterface::class); + $authenticated = $this->createMock(RequestInterface::class); + $promise = $this->createMock(Promise::class); + + $authentication->expects($this->once())->method('authenticate')->with($request)->willReturn($authenticated); + + $plugin = new AuthenticationPlugin($authentication); + + $next = function (RequestInterface $actual) use ($authenticated, $promise) { + $this->assertSame($authenticated, $actual); + + return $promise; + }; + + $this->assertSame($promise, $plugin->handleRequest($request, $next, static function () {})); + } +} diff --git a/tests/Plugin/BaseUriPluginTest.php b/tests/Plugin/BaseUriPluginTest.php new file mode 100644 index 00000000..a4f3d674 --- /dev/null +++ b/tests/Plugin/BaseUriPluginTest.php @@ -0,0 +1,69 @@ +handle($plugin, $request, function (RequestInterface $request): void { + $uri = $request->getUri(); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame('http', $uri->getScheme()); + $this->assertSame(8000, $uri->getPort()); + $this->assertSame('/api/users', $uri->getPath()); + }); + } + + public function testAddsOnlyHostWhenBaseUriHasNoPath(): void + { + $plugin = new BaseUriPlugin(new Uri('http://example.com:8000/')); + $request = new Request('GET', '/foo'); + + $this->handle($plugin, $request, function (RequestInterface $request): void { + $uri = $request->getUri(); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame('/foo', $uri->getPath()); + }); + } + + public function testReplacesHostAndAddsPathWhenConfigured(): void + { + $plugin = new BaseUriPlugin(new Uri('http://example.com:8000/api'), ['replace' => true]); + $request = new Request('GET', 'https://existing.com/users'); + + $this->handle($plugin, $request, function (RequestInterface $request): void { + $uri = $request->getUri(); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame('/api/users', $uri->getPath()); + }); + } + + private function handle(BaseUriPlugin $plugin, RequestInterface $request, callable $assert): void + { + $plugin->handleRequest( + $request, + function (RequestInterface $request) use ($assert) { + $assert($request); + + return new HttpFulfilledPromise(new Response()); + }, + static function () { + return new HttpFulfilledPromise(new Response()); + } + )->wait(); + } +} diff --git a/tests/Plugin/ContentLengthPluginTest.php b/tests/Plugin/ContentLengthPluginTest.php new file mode 100644 index 00000000..e1b26ce6 --- /dev/null +++ b/tests/Plugin/ContentLengthPluginTest.php @@ -0,0 +1,52 @@ +createMock(RequestInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $request->expects($this->once())->method('hasHeader')->with('Content-Length')->willReturn(false); + $request->expects($this->once())->method('getBody')->willReturn($stream); + $stream->expects($this->exactly(2))->method('getSize')->willReturn(100); + $request->expects($this->once())->method('withHeader')->with('Content-Length', '100')->willReturnSelf(); + + $plugin->handleRequest($request, $this->next(), static function () {}); + } + + public function testStreamsChunkedWhenSizeUnknown(): void + { + $plugin = new ContentLengthPlugin(); + $request = $this->createMock(RequestInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $request->expects($this->once())->method('hasHeader')->with('Content-Length')->willReturn(false); + $request->expects($this->once())->method('getBody')->willReturn($stream); + $stream->expects($this->once())->method('getSize')->willReturn(null); + $request->expects($this->once())->method('withBody')->with($this->isInstanceOf(ChunkStream::class))->willReturnSelf(); + $request->expects($this->once())->method('withAddedHeader')->with('Transfer-Encoding', 'chunked')->willReturnSelf(); + + $plugin->handleRequest($request, $this->next(), static function () {}); + } + + private function next(): callable + { + return static function () { + return new HttpFulfilledPromise(new Response()); + }; + } +} diff --git a/tests/Plugin/ContentTypePluginTest.php b/tests/Plugin/ContentTypePluginTest.php new file mode 100644 index 00000000..78dd53c8 --- /dev/null +++ b/tests/Plugin/ContentTypePluginTest.php @@ -0,0 +1,109 @@ + 'bar']))); + $handled = $this->handle(new ContentTypePlugin(), $request); + + $this->assertSame('application/json', $handled->getHeaderLine('Content-Type')); + } + + public function testAddsXmlContentType(): void + { + $request = new Request('POST', 'https://example.com', [], Stream::create('bar')); + $handled = $this->handle(new ContentTypePlugin(), $request); + + $this->assertSame('application/xml', $handled->getHeaderLine('Content-Type')); + } + + public function testDoesNotSetForUnknownContent(): void + { + $request = new Request('POST', 'https://example.com', [], Stream::create('foo')); + $handled = $this->handle(new ContentTypePlugin(), $request); + + $this->assertFalse($handled->hasHeader('Content-Type')); + } + + public function testDoesNotOverrideExistingHeader(): void + { + $request = (new Request('POST', 'https://example.com', ['Content-Type' => 'text/plain'], Stream::create('foo'))); + $handled = $this->handle(new ContentTypePlugin(), $request); + + $this->assertSame('text/plain', $handled->getHeaderLine('Content-Type')); + } + + public function testDoesNotSetWhenSizeZeroOrUnknown(): void + { + $request = new Request('POST', 'https://example.com', [], Stream::create('')); + $this->assertFalse($this->handle(new ContentTypePlugin(), $request)->hasHeader('Content-Type')); + + $stream = $this->createMock(StreamInterface::class); + $stream->method('isSeekable')->willReturn(true); + $stream->method('getSize')->willReturn(null); + $stream->method('getContents')->willReturn(''); + $stream->method('rewind')->willReturn(null); + $request = (new Request('POST', 'https://example.com'))->withBody($stream); + $this->assertFalse($this->handle(new ContentTypePlugin(), $request)->hasHeader('Content-Type')); + } + + public function testSkipDetectionRespectsDefaultLimit(): void + { + $plugin = new ContentTypePlugin(['skip_detection' => true]); + $request = new Request('POST', 'https://example.com', [], Stream::create('bar')); + $handled = $this->handle($plugin, $request); + + $this->assertSame('application/xml', $handled->getHeaderLine('Content-Type')); + } + + public function testSkipDetectionRespectsCustomLimit(): void + { + $plugin = new ContentTypePlugin(['skip_detection' => true, 'size_limit' => 32000000]); + $request = new Request('POST', 'https://example.com', [], Stream::create('bar')); + $handled = $this->handle($plugin, $request); + + $this->assertSame('application/xml', $handled->getHeaderLine('Content-Type')); + } + + public function testDoesNotSetWhenSizeLimitReached(): void + { + $plugin = new ContentTypePlugin(['skip_detection' => true, 'size_limit' => 8]); + $request = new Request('POST', 'https://example.com', [], Stream::create('bar')); + $handled = $this->handle($plugin, $request); + + $this->assertFalse($handled->hasHeader('Content-Type')); + } + + private function handle(ContentTypePlugin $plugin, RequestInterface $request): RequestInterface + { + $captured = null; + + $plugin->handleRequest( + $request, + function (RequestInterface $request) use (&$captured) { + $captured = $request; + + return new HttpFulfilledPromise(new Response()); + }, + static function () { + return new HttpFulfilledPromise(new Response()); + } + )->wait(); + + return $captured ?? $request; + } +} diff --git a/tests/Plugin/CookiePluginTest.php b/tests/Plugin/CookiePluginTest.php new file mode 100644 index 00000000..8a15b940 --- /dev/null +++ b/tests/Plugin/CookiePluginTest.php @@ -0,0 +1,187 @@ +addCookie(new Cookie('name', 'value', 86400, 'test.com')); + + $plugin = new CookiePlugin($jar); + + $this->assertCookieHeader('name=value', $plugin, new Request('GET', 'http://test.com/')); + } + + public function testCombinesMultipleCookies(): void + { + $jar = new CookieJar(); + $jar->addCookie(new Cookie('name', 'value', 86400, 'test.com')); + $jar->addCookie(new Cookie('name2', 'value2', 86400, 'test.com')); + + $plugin = new CookiePlugin($jar); + + $this->assertCookieHeader('name=value; name2=value2', $plugin, new Request('GET', 'http://test.com/')); + } + + public function testSkipsExpiredCookies(): void + { + $jar = new CookieJar(); + $jar->addCookie(new Cookie('name', 'value', null, 'test.com', '/', false, false, new \DateTime('-1 day'))); + + $plugin = new CookiePlugin($jar); + + $this->assertNoCookieHeader($plugin, new Request('GET', 'http://test.com/')); + } + + public function testSkipsMismatchedDomain(): void + { + $jar = new CookieJar(); + $jar->addCookie(new Cookie('name', 'value', 86400, 'test2.com')); + + $plugin = new CookiePlugin($jar); + + $this->assertNoCookieHeader($plugin, new Request('GET', 'http://test.com/')); + } + + public function testSkipsHackishDomains(): void + { + $jar = new CookieJar(); + $jar->addCookie(new Cookie('name', 'value', 86400, 'test.com')); + + $plugin = new CookiePlugin($jar); + + foreach (['hacktest.com', 'test.com.hacked.org'] as $domain) { + $this->assertNoCookieHeader($plugin, new Request('GET', 'http://'.$domain.'/')); + } + } + + public function testLoadsCookieOnSubdomain(): void + { + $jar = new CookieJar(); + $jar->addCookie(new Cookie('name', 'value', 86400, 'test.com')); + + $plugin = new CookiePlugin($jar); + + $this->assertCookieHeader('name=value', $plugin, new Request('GET', 'http://www.test.com/')); + } + + public function testSkipsPathMismatch(): void + { + $jar = new CookieJar(); + $jar->addCookie(new Cookie('name', 'value', 86400, 'test.com', '/sub')); + $plugin = new CookiePlugin($jar); + + $this->assertNoCookieHeader($plugin, new Request('GET', 'http://test.com/')); + } + + public function testSkipsSecureCookieOnHttp(): void + { + $jar = new CookieJar(); + $jar->addCookie(new Cookie('name', 'value', 86400, 'test.com', null, true)); + + $plugin = new CookiePlugin($jar); + + $this->assertNoCookieHeader($plugin, new Request('GET', 'http://test.com/')); + } + + public function testLoadsSecureCookieOnHttps(): void + { + $jar = new CookieJar(); + $jar->addCookie(new Cookie('name', 'value', 86400, 'test.com', null, true)); + + $plugin = new CookiePlugin($jar); + + $this->assertCookieHeader('name=value', $plugin, new Request('GET', 'https://test.com/')); + } + + public function testStoresCookieFromResponse(): void + { + $jar = new CookieJar(); + $plugin = new CookiePlugin($jar); + + $response = new Response(200, [ + 'Set-Cookie' => ['cookie=value; expires=Tue, 31-Mar-99 07:42:12 GMT; Max-Age=60; path=/; domain=test.com; secure; HttpOnly'], + ]); + + $promise = $plugin->handleRequest( + new Request('GET', 'http://test.com/'), + function () use ($response) { + return new HttpFulfilledPromise($response); + }, + static function () { + } + ); + + $this->assertInstanceOf(ResponseInterface::class, $promise->wait()); + $this->assertNotEmpty($jar->getCookies()); + } + + public function testThrowsOnInvalidExpiresDate(): void + { + $jar = new CookieJar(); + $plugin = new CookiePlugin($jar); + + $response = new Response(200, [ + 'Set-Cookie' => ['cookie=value; expires=i-am-an-invalid-date;'], + ]); + + $promise = $plugin->handleRequest( + new Request('GET', 'http://test.com/'), + function () use ($response) { + return new HttpFulfilledPromise($response); + }, + static function () { + } + ); + + $this->assertInstanceOf(HttpRejectedPromise::class, $promise); + $this->expectException(TransferException::class); + $promise->wait(); + } + + private function assertCookieHeader(string $expected, CookiePlugin $plugin, RequestInterface $request): void + { + $plugin->handleRequest( + $request, + function (RequestInterface $request) use ($expected) { + $this->assertSame($expected, $request->getHeaderLine('Cookie')); + + return new HttpFulfilledPromise(new Response()); + }, + static function () { + return new HttpFulfilledPromise(new Response()); + } + )->wait(); + } + + private function assertNoCookieHeader(CookiePlugin $plugin, RequestInterface $request): void + { + $plugin->handleRequest( + $request, + function (RequestInterface $request) { + $this->assertFalse($request->hasHeader('Cookie')); + + return new HttpFulfilledPromise(new Response()); + }, + static function () { + return new HttpFulfilledPromise(new Response()); + } + )->wait(); + } +} diff --git a/tests/Plugin/DecoderPluginTest.php b/tests/Plugin/DecoderPluginTest.php new file mode 100644 index 00000000..97e174f5 --- /dev/null +++ b/tests/Plugin/DecoderPluginTest.php @@ -0,0 +1,148 @@ +createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $request->expects($this->exactly(2)) + ->method('withHeader') + ->withConsecutive( + [$this->equalTo('Accept-Encoding'), $this->isType('array')], + [$this->equalTo('TE'), $this->callback(function (array $encodings) { + $this->assertContains('chunked', $encodings); + + return true; + })] + ) + ->willReturnSelf(); + + $response->expects($this->exactly(2)) + ->method('hasHeader') + ->withConsecutive(['Transfer-Encoding'], ['Content-Encoding']) + ->willReturnOnConsecutiveCalls(true, false); + $response->expects($this->once())->method('getHeader')->with('Transfer-Encoding')->willReturn(['chunked']); + $response->expects($this->once())->method('getBody')->willReturn($stream); + $response->expects($this->once())->method('withBody')->with($this->isInstanceOf(DechunkStream::class))->willReturnSelf(); + $response->expects($this->once())->method('withoutHeader')->with('Transfer-Encoding')->willReturnSelf(); + + $plugin->handleRequest( + $request, + function () use ($response) { + return new HttpFulfilledPromise($response); + }, + static function () { + } + )->wait(); + } + + public function testDecodesGzipResponses(): void + { + $plugin = new DecoderPlugin(); + $request = $this->mockRequestWithHeaders(); + $response = $this->createMock(ResponseInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $response->expects($this->exactly(2)) + ->method('hasHeader') + ->withConsecutive(['Transfer-Encoding'], ['Content-Encoding']) + ->willReturnOnConsecutiveCalls(false, true); + $response->expects($this->once())->method('getHeader')->with('Content-Encoding')->willReturn(['gzip']); + $response->expects($this->once())->method('getBody')->willReturn($stream); + $response->expects($this->once())->method('withBody')->with($this->isInstanceOf(GzipDecodeStream::class))->willReturnSelf(); + $response->expects($this->once())->method('withoutHeader')->with('Content-Encoding')->willReturnSelf(); + + $plugin->handleRequest( + $request, + function () use ($response) { + return new HttpFulfilledPromise($response); + }, + static function () { + } + )->wait(); + } + + public function testDecodesDeflateResponses(): void + { + $plugin = new DecoderPlugin(); + $request = $this->mockRequestWithHeaders(); + $response = $this->createMock(ResponseInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $response->expects($this->exactly(2)) + ->method('hasHeader') + ->withConsecutive(['Transfer-Encoding'], ['Content-Encoding']) + ->willReturnOnConsecutiveCalls(false, true); + $response->expects($this->once())->method('getHeader')->with('Content-Encoding')->willReturn(['deflate']); + $response->expects($this->once())->method('getBody')->willReturn($stream); + $response->expects($this->once())->method('withBody')->with($this->isInstanceOf(DecompressStream::class))->willReturnSelf(); + $response->expects($this->once())->method('withoutHeader')->with('Content-Encoding')->willReturnSelf(); + + $plugin->handleRequest( + $request, + function () use ($response) { + return new HttpFulfilledPromise($response); + }, + static function () { + } + )->wait(); + } + + public function testSkipsContentEncodingWhenDisabled(): void + { + $plugin = new DecoderPlugin(['use_content_encoding' => false]); + $request = $this->createMock(RequestInterface::class); + $response = new Response(); + + $request->expects($this->once()) + ->method('withHeader') + ->with($this->equalTo('TE'), $this->callback(function (array $encodings) { + $this->assertContains('chunked', $encodings); + + return true; + })) + ->willReturnSelf(); + + $plugin->handleRequest( + $request, + function () use ($response) { + return new HttpFulfilledPromise($response); + }, + static function () { + } + )->wait(); + } + + private function mockRequestWithHeaders(): RequestInterface + { + $request = $this->createMock(RequestInterface::class); + $request->expects($this->exactly(2)) + ->method('withHeader') + ->withConsecutive( + [$this->equalTo('Accept-Encoding'), $this->isType('array')], + [$this->equalTo('TE'), $this->isType('array')] + ) + ->willReturnSelf(); + + return $request; + } +} diff --git a/tests/Plugin/ErrorPluginTest.php b/tests/Plugin/ErrorPluginTest.php new file mode 100644 index 00000000..9b56eb35 --- /dev/null +++ b/tests/Plugin/ErrorPluginTest.php @@ -0,0 +1,72 @@ +expectException(ClientErrorException::class); + + $plugin->handleRequest($request, function () use ($response) { + return new HttpFulfilledPromise($response); + }, static function () { + })->wait(); + } + + public function testDoesNotThrowClientErrorWhenConfigured(): void + { + $plugin = new ErrorPlugin(['only_server_exception' => true]); + $request = new Request('GET', 'https://example.com'); + $response = new Response(400, [], null, '1.1', 'Bad request'); + + $result = $plugin->handleRequest($request, function () use ($response) { + return new HttpFulfilledPromise($response); + }, static function () { + })->wait(); + + $this->assertSame($response, $result); + } + + public function testThrowsServerErrorOn5xx(): void + { + $plugin = new ErrorPlugin(); + $request = new Request('GET', 'https://example.com'); + $response = new Response(500, [], null, '1.1', 'Server error'); + + $this->expectException(ServerErrorException::class); + + $plugin->handleRequest($request, function () use ($response) { + return new HttpFulfilledPromise($response); + }, static function () { + })->wait(); + } + + public function testReturnsResponseOtherwise(): void + { + $plugin = new ErrorPlugin(); + $request = new Request('GET', 'https://example.com'); + $response = new Response(200); + + $result = $plugin->handleRequest($request, function () use ($response) { + return new HttpFulfilledPromise($response); + }, static function () { + })->wait(); + + $this->assertSame($response, $result); + } +} diff --git a/tests/Plugin/HeaderAppendPluginTest.php b/tests/Plugin/HeaderAppendPluginTest.php new file mode 100644 index 00000000..40c6d0cf --- /dev/null +++ b/tests/Plugin/HeaderAppendPluginTest.php @@ -0,0 +1,30 @@ +createMock(RequestInterface::class); + $request->expects($this->exactly(2)) + ->method('withAddedHeader') + ->withConsecutive(['foo', 'bar'], ['baz', 'qux']) + ->willReturnSelf(); + + $plugin = new HeaderAppendPlugin(['foo' => 'bar', 'baz' => 'qux']); + + $plugin->handleRequest($request, static function ($request) { + return new HttpFulfilledPromise(new Response()); + }, static function () { + }); + } +} diff --git a/tests/Plugin/HeaderDefaultsPluginTest.php b/tests/Plugin/HeaderDefaultsPluginTest.php new file mode 100644 index 00000000..4c798cf9 --- /dev/null +++ b/tests/Plugin/HeaderDefaultsPluginTest.php @@ -0,0 +1,34 @@ +createMock(RequestInterface::class); + $request->expects($this->exactly(2)) + ->method('hasHeader') + ->withConsecutive(['foo'], ['baz']) + ->willReturnOnConsecutiveCalls(false, true); + $request->expects($this->once()) + ->method('withHeader') + ->with('foo', 'bar') + ->willReturnSelf(); + + $plugin = new HeaderDefaultsPlugin(['foo' => 'bar', 'baz' => 'qux']); + + $plugin->handleRequest($request, static function ($request) { + return new HttpFulfilledPromise(new Response()); + }, static function () { + }); + } +} diff --git a/tests/Plugin/HeaderRemovePluginTest.php b/tests/Plugin/HeaderRemovePluginTest.php new file mode 100644 index 00000000..e5b85e1d --- /dev/null +++ b/tests/Plugin/HeaderRemovePluginTest.php @@ -0,0 +1,31 @@ +createMock(RequestInterface::class); + $request->expects($this->exactly(2)) + ->method('hasHeader') + ->withConsecutive(['foo'], ['baz']) + ->willReturnOnConsecutiveCalls(false, true); + $request->expects($this->once())->method('withoutHeader')->with('baz')->willReturnSelf(); + + $plugin = new HeaderRemovePlugin(['foo', 'baz']); + + $plugin->handleRequest($request, static function ($request) { + return new HttpFulfilledPromise(new Response()); + }, static function () { + }); + } +} diff --git a/tests/Plugin/HeaderSetPluginTest.php b/tests/Plugin/HeaderSetPluginTest.php new file mode 100644 index 00000000..6d9eef3b --- /dev/null +++ b/tests/Plugin/HeaderSetPluginTest.php @@ -0,0 +1,30 @@ +createMock(RequestInterface::class); + $request->expects($this->exactly(2)) + ->method('withHeader') + ->withConsecutive(['foo', 'bar'], ['baz', 'qux']) + ->willReturnSelf(); + + $plugin = new HeaderSetPlugin(['foo' => 'bar', 'baz' => 'qux']); + + $plugin->handleRequest($request, static function ($request) { + return new HttpFulfilledPromise(new Response()); + }, static function () { + }); + } +} diff --git a/tests/Plugin/HistoryPluginTest.php b/tests/Plugin/HistoryPluginTest.php new file mode 100644 index 00000000..60131c58 --- /dev/null +++ b/tests/Plugin/HistoryPluginTest.php @@ -0,0 +1,50 @@ +createMock(Journal::class); + $plugin = new HistoryPlugin($journal); + $request = new Request('GET', 'https://example.com'); + $response = new Response(); + + $journal->expects($this->once())->method('addSuccess')->with($request, $response); + + $plugin->handleRequest($request, function () use ($response) { + return new HttpFulfilledPromise($response); + }, static function () { + })->wait(); + } + + public function testRecordsFailure(): void + { + $journal = $this->createMock(Journal::class); + $plugin = new HistoryPlugin($journal); + $request = new Request('GET', 'https://example.com'); + $exception = new RequestException('error', $request); + + $journal->expects($this->once())->method('addFailure')->with($request, $exception); + + $promise = $plugin->handleRequest($request, function () use ($exception) { + return new HttpRejectedPromise($exception); + }, static function () { + }); + + $this->expectException(RequestException::class); + $promise->wait(); + } +} diff --git a/tests/Plugin/QueryDefaultsPluginTest.php b/tests/Plugin/QueryDefaultsPluginTest.php new file mode 100644 index 00000000..e4a3742d --- /dev/null +++ b/tests/Plugin/QueryDefaultsPluginTest.php @@ -0,0 +1,50 @@ + 'bar']); + $request = new Request('GET', 'https://example.com/?test=true'); + + $this->handle($plugin, $request, function (RequestInterface $request) { + $this->assertSame('test=true&foo=bar', $request->getUri()->getQuery()); + }); + } + + public function testDoesNotOverrideExistingParameters(): void + { + $plugin = new QueryDefaultsPlugin(['foo' => 'fooDefault', 'bar' => 'barDefault']); + $request = new Request('GET', 'https://example.com/?foo=new'); + + $this->handle($plugin, $request, function (RequestInterface $request) { + $this->assertSame('foo=new&bar=barDefault', $request->getUri()->getQuery()); + }); + } + + private function handle(QueryDefaultsPlugin $plugin, RequestInterface $request, callable $assert): void + { + $plugin->handleRequest( + $request, + function (RequestInterface $request) use ($assert) { + $assert($request); + + return new HttpFulfilledPromise(new Response()); + }, + static function () { + return new HttpFulfilledPromise(new Response()); + } + )->wait(); + } +} diff --git a/tests/Plugin/RequestMatcherPluginTest.php b/tests/Plugin/RequestMatcherPluginTest.php new file mode 100644 index 00000000..df1683f9 --- /dev/null +++ b/tests/Plugin/RequestMatcherPluginTest.php @@ -0,0 +1,64 @@ +createMock(RequestMatcher::class); + $success = $this->createMock(Plugin::class); + $request = new Request('GET', 'https://example.com'); + $plugin = new RequestMatcherPlugin($matcher, $success); + + $matcher->expects($this->once())->method('matches')->with($request)->willReturn(true); + $success->expects($this->once())->method('handleRequest')->with($request)->willReturn(new HttpFulfilledPromise(new Response())); + + $plugin->handleRequest($request, function () { + return new HttpFulfilledPromise(new Response()); + }, static function () { + }); + } + + public function testDelegatesToFailurePluginWhenNotMatched(): void + { + $matcher = $this->createMock(RequestMatcher::class); + $failure = $this->createMock(Plugin::class); + $request = new Request('GET', 'https://example.com'); + $plugin = new RequestMatcherPlugin($matcher, null, $failure); + + $matcher->expects($this->once())->method('matches')->with($request)->willReturn(false); + $failure->expects($this->once())->method('handleRequest')->with($request)->willReturn(new HttpFulfilledPromise(new Response())); + + $plugin->handleRequest($request, function () { + return new HttpFulfilledPromise(new Response()); + }, static function () { + }); + } + + public function testCallsNextWhenNoDelegateMatches(): void + { + $matcher = $this->createMock(RequestMatcher::class); + $request = new Request('GET', 'https://example.com'); + $plugin = new RequestMatcherPlugin($matcher, null, null); + + $matcher->expects($this->once())->method('matches')->willReturn(false); + + $result = $plugin->handleRequest($request, function () { + return new HttpFulfilledPromise(new Response()); + }, static function () { + })->wait(); + + $this->assertInstanceOf(Response::class, $result); + } +} diff --git a/tests/Plugin/RequestSeekableBodyPluginTest.php b/tests/Plugin/RequestSeekableBodyPluginTest.php new file mode 100644 index 00000000..16bc1ac6 --- /dev/null +++ b/tests/Plugin/RequestSeekableBodyPluginTest.php @@ -0,0 +1,52 @@ +createMock(RequestInterface::class); + $stream = $this->createMock(StreamInterface::class); + $stream->method('isSeekable')->willReturn(false); + + $request->expects($this->exactly(2))->method('getBody')->willReturn($stream); + $request->expects($this->once()) + ->method('withBody') + ->with($this->isInstanceOf(BufferedStream::class)) + ->willReturnSelf(); + + (new RequestSeekableBodyPlugin())->handleRequest($request, $this->next(), static function () { + }); + } + + public function testLeavesSeekableBodyUntouched(): void + { + $request = $this->createMock(RequestInterface::class); + $stream = $this->createMock(StreamInterface::class); + $stream->method('isSeekable')->willReturn(true); + + $request->expects($this->once())->method('getBody')->willReturn($stream); + $request->expects($this->never())->method('withBody'); + + (new RequestSeekableBodyPlugin())->handleRequest($request, $this->next(), static function () { + }); + } + + private function next(): callable + { + return static function () { + return new HttpFulfilledPromise(new Response()); + }; + } +} diff --git a/tests/Plugin/ResponseSeekableBodyPluginTest.php b/tests/Plugin/ResponseSeekableBodyPluginTest.php new file mode 100644 index 00000000..2089ebc9 --- /dev/null +++ b/tests/Plugin/ResponseSeekableBodyPluginTest.php @@ -0,0 +1,53 @@ +createMock(ResponseInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $response->expects($this->exactly(2))->method('getBody')->willReturn($stream); + $stream->method('isSeekable')->willReturn(false); + $response->expects($this->once()) + ->method('withBody') + ->with($this->isInstanceOf(BufferedStream::class)) + ->willReturnSelf(); + + $plugin->handleRequest(new Request('GET', '/'), function () use ($response) { + return new HttpFulfilledPromise($response); + }, static function () { + })->wait(); + } + + public function testLeavesSeekableResponseUntouched(): void + { + $plugin = new ResponseSeekableBodyPlugin(); + $response = $this->createMock(ResponseInterface::class); + $stream = $this->createMock(StreamInterface::class); + + $response->expects($this->once())->method('getBody')->willReturn($stream); + $stream->method('isSeekable')->willReturn(true); + $response->expects($this->never())->method('withBody'); + + $result = $plugin->handleRequest(new Request('GET', '/'), function () use ($response) { + return new HttpFulfilledPromise($response); + }, static function () { + })->wait(); + + $this->assertSame($response, $result); + } +} diff --git a/tests/Plugin/RetryPluginTest.php b/tests/Plugin/RetryPluginTest.php new file mode 100644 index 00000000..e3d19d4f --- /dev/null +++ b/tests/Plugin/RetryPluginTest.php @@ -0,0 +1,160 @@ +handleRequest($request, function () use ($response) { + return new HttpFulfilledPromise($response); + }, static function () { + })->wait(); + + $this->assertSame($response, $result); + } + + public function testThrowsLastExceptionAfterRetries(): void + { + $request = new Request('GET', 'https://example.com'); + $exception1 = new NetworkException('first', $request); + $exception2 = new NetworkException('second', $request); + $count = 0; + + $plugin = new RetryPlugin(['exception_delay' => function () { + return 0; + }]); + + $next = function () use (&$count, $exception1, $exception2) { + ++$count; + + return new HttpRejectedPromise(1 === $count ? $exception1 : $exception2); + }; + + $this->expectExceptionObject($exception2); + $plugin->handleRequest($request, $next, static function () { + })->wait(); + } + + public function testDoesNotRetryClientErrors(): void + { + $request = new Request('GET', 'https://example.com'); + $response = new Response(400); + $exception = new HttpException('client error', $request, $response); + + $plugin = new RetryPlugin(['exception_delay' => function () { + return 0; + }]); + + $this->expectExceptionObject($exception); + $plugin->handleRequest($request, function () use ($exception) { + return new HttpRejectedPromise($exception); + }, static function () { + })->wait(); + } + + public function testReturnsResponseOnSecondTry(): void + { + $request = new Request('GET', 'https://example.com'); + $response = new Response(); + $exception = new NetworkException('fail', $request); + $count = 0; + + $plugin = new RetryPlugin(['exception_delay' => function () { + return 0; + }]); + + $result = $plugin->handleRequest( + $request, + function () use (&$count, $exception, $response) { + ++$count; + + return 1 === $count ? new HttpRejectedPromise($exception) : new HttpFulfilledPromise($response); + }, + static function () { + } + )->wait(); + + $this->assertSame($response, $result); + } + + public function testRespectsCustomExceptionDecider(): void + { + $request = new Request('GET', 'https://example.com'); + $exception = new NetworkException('fail', $request); + + $plugin = new RetryPlugin([ + 'exception_decider' => static function () { + return false; + }, + ]); + + $this->expectExceptionObject($exception); + $plugin->handleRequest($request, function () use ($exception) { + return new HttpRejectedPromise($exception); + }, static function () { + })->wait(); + } + + public function testDoesNotKeepHistoryBetweenRequests(): void + { + $request = new Request('GET', 'https://example.com'); + $response = new Response(); + $exception = new NetworkException('fail', $request); + $count = 0; + + $plugin = new RetryPlugin(['exception_delay' => function () { + return 0; + }]); + + $next = function () use (&$count, $exception, $response) { + ++$count; + + return 1 === $count % 2 ? new HttpRejectedPromise($exception) : new HttpFulfilledPromise($response); + }; + + $this->assertSame($response, $plugin->handleRequest($request, $next, static function () { + })->wait()); + $this->assertSame($response, $plugin->handleRequest($request, $next, static function () { + })->wait()); + } + + public function testDefaultErrorResponseDelay(): void + { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $this->assertSame(500000, RetryPlugin::defaultErrorResponseDelay($request, $response, 0)); + $this->assertSame(1000000, RetryPlugin::defaultErrorResponseDelay($request, $response, 1)); + $this->assertSame(2000000, RetryPlugin::defaultErrorResponseDelay($request, $response, 2)); + $this->assertSame(4000000, RetryPlugin::defaultErrorResponseDelay($request, $response, 3)); + } + + public function testDefaultExceptionDelay(): void + { + $request = $this->createMock(RequestInterface::class); + $exception = $this->createMock(\Psr\Http\Client\ClientExceptionInterface::class); + + $this->assertSame(500000, RetryPlugin::defaultExceptionDelay($request, $exception, 0)); + $this->assertSame(1000000, RetryPlugin::defaultExceptionDelay($request, $exception, 1)); + $this->assertSame(2000000, RetryPlugin::defaultExceptionDelay($request, $exception, 2)); + $this->assertSame(4000000, RetryPlugin::defaultExceptionDelay($request, $exception, 3)); + } +} diff --git a/tests/PluginChainTest.php b/tests/PluginChainTest.php index 7a536811..759b5483 100644 --- a/tests/PluginChainTest.php +++ b/tests/PluginChainTest.php @@ -43,11 +43,11 @@ public function testChainShouldInvokePluginsInReversedOrder(): void $pluginOrderCalls[] = 'plugin2'; }); - $request = $this->prophesize(RequestInterface::class); - $responsePromise = $this->prophesize(Promise::class); + $request = $this->createMock(RequestInterface::class); + $responsePromise = $this->createMock(Promise::class); $clientCallable = static function () use ($responsePromise) { - return $responsePromise->reveal(); + return $responsePromise; }; $pluginOrderCalls = []; @@ -59,9 +59,9 @@ public function testChainShouldInvokePluginsInReversedOrder(): void $pluginChain = new PluginChain($plugins, $clientCallable); - $result = $pluginChain($request->reveal()); + $result = $pluginChain($request); - $this->assertSame($responsePromise->reveal(), $result); + $this->assertSame($responsePromise, $result); $this->assertSame(['plugin1', 'plugin2'], $pluginOrderCalls); } @@ -69,23 +69,23 @@ public function testShouldThrowLoopExceptionOnMaxRestarts(): void { $this->expectException(LoopException::class); - $request = $this->prophesize(RequestInterface::class); - $responsePromise = $this->prophesize(Promise::class); + $request = $this->createMock(RequestInterface::class); + $responsePromise = $this->createMock(Promise::class); $calls = 0; $clientCallable = static function () use ($responsePromise, &$calls) { ++$calls; - return $responsePromise->reveal(); + return $responsePromise; }; $pluginChain = new PluginChain([], $clientCallable, ['max_restarts' => 2]); - $pluginChain($request->reveal()); + $pluginChain($request); $this->assertSame(1, $calls); - $pluginChain($request->reveal()); + $pluginChain($request); $this->assertSame(2, $calls); - $pluginChain($request->reveal()); + $pluginChain($request); $this->assertSame(3, $calls); - $pluginChain($request->reveal()); + $pluginChain($request); } } diff --git a/tests/PluginClientBuilderTest.php b/tests/PluginClientBuilderTest.php index 93728596..58f44bbb 100644 --- a/tests/PluginClientBuilderTest.php +++ b/tests/PluginClientBuilderTest.php @@ -19,16 +19,16 @@ public function testPriority(string $client): void $builder = new PluginClientBuilder(); $plugins = [ - 10 => $this->prophesize(Plugin::class)->reveal(), - -10 => $this->prophesize(Plugin::class)->reveal(), - 0 => $this->prophesize(Plugin::class)->reveal(), + 10 => $this->createMock(Plugin::class), + -10 => $this->createMock(Plugin::class), + 0 => $this->createMock(Plugin::class), ]; foreach ($plugins as $priority => $plugin) { $builder->addPlugin($plugin, $priority); } - $client = $this->prophesize($client)->reveal(); + $client = $this->createMock($client); $client = $builder->createClient($client); $closure = \Closure::bind( @@ -54,7 +54,7 @@ public function testOptions(string $client): void $builder = new PluginClientBuilder(); $builder->setOption('max_restarts', 5); - $client = $this->prophesize($client)->reveal(); + $client = $this->createMock($client); $client = $builder->createClient($client); $closure = \Closure::bind( diff --git a/tests/PluginClientFactoryTest.php b/tests/PluginClientFactoryTest.php new file mode 100644 index 00000000..6bca9e21 --- /dev/null +++ b/tests/PluginClientFactoryTest.php @@ -0,0 +1,30 @@ +createMock(HttpClient::class); + + $this->assertInstanceOf(PluginClient::class, $factory->createClient($client)); + } + + public function testAcceptsClientNameOption(): void + { + $factory = new PluginClientFactory(); + $client = $this->createMock(HttpClient::class); + + $factory->createClient($client, [], ['client_name' => 'Default']); + $this->addToAssertionCount(1); + } +} diff --git a/tests/PluginClientTest.php b/tests/PluginClientTest.php index 26547bdc..997d784a 100644 --- a/tests/PluginClientTest.php +++ b/tests/PluginClientTest.php @@ -4,11 +4,13 @@ namespace Tests\Http\Client\Common; +use Http\Client\Common\Exception\LoopException; use Http\Client\Common\Plugin; use Http\Client\Common\Plugin\HeaderAppendPlugin; use Http\Client\Common\Plugin\RedirectPlugin; use Http\Client\Common\PluginClient; use Http\Client\HttpAsyncClient; +use Http\Client\HttpClient; use Http\Client\Promise\HttpFulfilledPromise; use Http\Promise\Promise; use Nyholm\Psr7\Request; @@ -20,6 +22,129 @@ class PluginClientTest extends TestCase { + public function testItImplementsHttpAndAsyncClients(): void + { + $httpClient = $this->createMock(HttpClient::class); + + $pluginClient = new PluginClient($httpClient); + + $this->assertInstanceOf(PluginClient::class, $pluginClient); + $this->assertInstanceOf(HttpClient::class, $pluginClient); + $this->assertInstanceOf(HttpAsyncClient::class, $pluginClient); + } + + public function testSendRequestUsesUnderlyingClient(): void + { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $httpClient = $this->createMock(HttpClient::class); + $httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($request) + ->willReturn($response); + + $pluginClient = new PluginClient($httpClient); + + $this->assertSame($response, $pluginClient->sendRequest($request)); + } + + public function testSendAsyncRequestUsesUnderlyingClient(): void + { + $request = $this->createMock(RequestInterface::class); + $promise = $this->createMock(Promise::class); + + $httpAsyncClient = $this->createMock(HttpAsyncClient::class); + $httpAsyncClient + ->expects($this->once()) + ->method('sendAsyncRequest') + ->with($request) + ->willReturn($promise); + + $pluginClient = new PluginClient($httpAsyncClient); + + $this->assertSame($promise, $pluginClient->sendAsyncRequest($request)); + } + + public function testSendRequestFallsBackToAsyncClient(): void + { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $promise = $this->createMock(Promise::class); + + $httpAsyncClient = $this->createMock(HttpAsyncClient::class); + $httpAsyncClient + ->expects($this->once()) + ->method('sendAsyncRequest') + ->with($request) + ->willReturn($promise); + + $promise + ->expects($this->once()) + ->method('wait') + ->willReturn($response); + + $pluginClient = new PluginClient($httpAsyncClient); + + $this->assertSame($response, $pluginClient->sendRequest($request)); + } + + public function testSendRequestPrefersSynchronousCallWhenAvailable(): void + { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $client = new class($response) implements HttpClient, HttpAsyncClient { + public $syncCalls = 0; + public $asyncCalls = 0; + private $response; + + public function __construct(ResponseInterface $response) + { + $this->response = $response; + } + + public function sendRequest(RequestInterface $request): ResponseInterface + { + ++$this->syncCalls; + + return $this->response; + } + + public function sendAsyncRequest(RequestInterface $request) + { + ++$this->asyncCalls; + + return new HttpFulfilledPromise($this->response); + } + }; + + $pluginClient = new PluginClient($client); + + $this->assertSame($response, $pluginClient->sendRequest($request)); + $this->assertSame(1, $client->syncCalls); + $this->assertSame(0, $client->asyncCalls); + } + + public function testLoopDetectionThrowsException(): void + { + $httpClient = $this->createMock(HttpClient::class); + $request = $this->createMock(RequestInterface::class); + + $loopPlugin = new class implements Plugin { + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + return $first($request); + } + }; + + $pluginClient = new PluginClient($httpClient, [$loopPlugin]); + + $this->expectException(LoopException::class); + $pluginClient->sendRequest($request); + } + /** * @dataProvider clientAndMethodProvider */