├── fixture ├── files │ ├── file1.txt │ ├── file2.txt │ ├── file3.txt │ └── resource.txt └── server.php ├── .php_cs.dist ├── bin └── http_test_server ├── src ├── PHPUnitUtility.php ├── HttpClientTest.php ├── HttpAsyncClientTest.php ├── HttpFeatureTest.php └── HttpBaseTest.php ├── LICENSE ├── README.md ├── composer.json └── CHANGELOG.md /fixture/files/file1.txt: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /fixture/files/file2.txt: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /fixture/files/file3.txt: -------------------------------------------------------------------------------- 1 | baz 2 | -------------------------------------------------------------------------------- /fixture/files/resource.txt: -------------------------------------------------------------------------------- 1 | abcdefghijklmnopqrstuvwxyz -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 5 | ->setRules([ 6 | '@PSR2' => true, 7 | ]) 8 | ->setFinder( 9 | PhpCsFixer\Finder::create() 10 | ->in(__DIR__.'/src') 11 | ->name('*.php') 12 | ) 13 | ; 14 | 15 | return $config; 16 | -------------------------------------------------------------------------------- /bin/http_test_server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 4 | 5 | # Installed as a dependency 6 | if [ -f "$DIR/../autoload.php" ]; then 7 | php -S 127.0.0.1:10000 -t "$DIR/../php-http/client-integration-tests/fixture" 8 | # Development 9 | elif [ -f "$DIR/../vendor/autoload.php" ]; then 10 | php -S 127.0.0.1:10000 -t "$DIR/../fixture" 11 | # Installed as a dependency, but not accessed using the symlink (e.g. Windows) 12 | elif [ -f "$DIR/../composer.json" -a -f "$DIR/../fixture/server.php" ] && grep -q php-http/client-integration-tests "$DIR/../composer.json"; then 13 | php -S 127.0.0.1:10000 -t "$DIR/../fixture" 14 | # Not working 15 | else 16 | echo "*** Can't find the fixture folder ***" >&2 17 | echo "Please write your own way to start a PHP web server on port 10000 for the 'fixture' directory." >&2 18 | exit 1 19 | fi 20 | -------------------------------------------------------------------------------- /src/PHPUnitUtility.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class PHPUnitUtility 11 | { 12 | /** 13 | * Gets the uri. 14 | * 15 | * @return string|bool The uri or FALSE if there is none. 16 | */ 17 | public static function getUri() 18 | { 19 | return array_key_exists('TEST_SERVER', $_SERVER) ? $_SERVER['TEST_SERVER'] : false; 20 | } 21 | 22 | /** 23 | * Gets the file. 24 | * 25 | * @param bool $tmp TRUE if the file should be in the "/tmp" directory else FALSE. 26 | * 27 | * @return string The file. 28 | */ 29 | public static function getFile(bool $tmp = true, ?string $name = null): string 30 | { 31 | return ($tmp ? realpath(sys_get_temp_dir()) : '').'/'.(null === $name ? uniqid() : $name); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Eric GELOEN 2 | Copyright (c) 2015-2016 PHP HTTP Team 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Client integration tests 2 | 3 | [![Latest Version](https://img.shields.io/github/release/php-http/client-integration-tests.svg?style=flat-square)](https://github.com/php-http/client-integration-tests/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/php-http/client-integration-tests.svg?style=flat-square)](https://packagist.org/packages/php-http/client-integration-tests) 6 | 7 | **HTTP Client integration tests.** 8 | 9 | 10 | ## Install 11 | 12 | Via Composer 13 | 14 | ```bash 15 | composer require php-http/client-integration-tests 16 | ``` 17 | 18 | 19 | ## Usage 20 | 21 | This package should not be used on its own. It provides integration tests for HTTP Clients. 22 | 23 | 24 | ### Running tests for HTTP Adapters 25 | 26 | Start the HTTP Test server: 27 | 28 | ```bash 29 | vendor/bin/http_test_server 30 | ``` 31 | 32 | Install an adapter. 33 | 34 | ```bash 35 | composer require php-http/curl-client laminas/laminas-diactoros 36 | ``` 37 | 38 | Run the tests. 39 | 40 | ```bash 41 | ./vendor/bin/phpunit --testsuite curl --printer "Http\Client\Tests\ResultPrinter" 42 | ``` 43 | 44 | 45 | ## Contributing 46 | 47 | Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). 48 | 49 | 50 | ## Security 51 | 52 | If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). 53 | 54 | 55 | ## License 56 | 57 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 58 | -------------------------------------------------------------------------------- /fixture/server.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please read the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require_once __DIR__.'/../src/PHPUnitUtility.php'; 13 | 14 | use Http\Client\Tests\PHPUnitUtility; 15 | 16 | $file = fopen(PHPUnitUtility::getFile(true, 'php-http-adapter.log'), 'c'); 17 | flock($file, LOCK_EX); 18 | ftruncate($file, 0); 19 | 20 | $serverError = isset($_GET['server_error']) ? $_GET['server_error'] : false; 21 | $clientError = isset($_GET['client_error']) ? $_GET['client_error'] : false; 22 | $delay = isset($_GET['delay']) ? $_GET['delay'] : 0; 23 | $redirect = isset($_GET['redirect']) ? $_GET['redirect'] : false; 24 | 25 | if ($serverError) { 26 | header($_SERVER['SERVER_PROTOCOL'].' 500 Internal Server Error', true, 500); 27 | } 28 | 29 | if ($clientError) { 30 | header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request', true, 400); 31 | } 32 | 33 | if ($delay > 0) { 34 | usleep($delay * 1000000); 35 | } 36 | 37 | if ($redirect) { 38 | header($_SERVER['SERVER_PROTOCOL'].' 302 Found', true, 302); 39 | header('Location: http://'.$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']); 40 | echo 'Redirect'; 41 | } else { 42 | echo 'Ok'; 43 | } 44 | 45 | fwrite( 46 | $file, 47 | json_encode(array( 48 | 'SERVER' => $_SERVER, 49 | 'GET' => $_GET, 50 | 'POST' => $_POST, 51 | 'FILES' => $_FILES, 52 | 'INPUT' => file_get_contents('php://input'), 53 | )) 54 | ); 55 | 56 | fflush($file); 57 | flock($file, LOCK_UN); 58 | fclose($file); 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-http/client-integration-tests", 3 | "description": "HTTP Client integration tests", 4 | "license": "MIT", 5 | "keywords": ["http", "client", "integration", "tests"], 6 | "homepage": "http://httplug.io", 7 | "authors": [ 8 | { 9 | "name": "Eric GELOEN", 10 | "email": "geloen.eric@gmail.com" 11 | }, 12 | { 13 | "name": "Márk Sági-Kazár", 14 | "email": "mark.sagikazar@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.4 || ^8.0", 19 | "ext-json": "*", 20 | "phpunit/phpunit": "^9.6.17 || ^10.0 || ^11.0 || ^12.0", 21 | "php-http/message": "^1.0 || ^2.0", 22 | "guzzlehttp/psr7": "^1.9 || ^2.0", 23 | "th3n3rd/cartesian-product": "^0.3" 24 | }, 25 | "suggest": { 26 | "php-http/httplug": "To test async client" 27 | }, 28 | "require-dev": { 29 | "php-http/httplug": "^2.0", 30 | "nyholm/psr7": "^1.8" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Http\\Client\\Tests\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Http\\Client\\Curl\\Tests\\": "vendor/php-http/curl-client/tests/", 40 | "Http\\Client\\Socket\\Tests\\": "vendor/php-http/socket-client/tests/", 41 | "Http\\Adapter\\Guzzle5\\Tests\\": "vendor/php-http/guzzle5-adapter/tests/", 42 | "Http\\Adapter\\Guzzle6\\Tests\\": "vendor/php-http/guzzle6-adapter/tests/", 43 | "Http\\Adapter\\Buzz\\Tests\\": "vendor/php-http/buzz-adapter/tests/", 44 | "Http\\Adapter\\React\\Tests\\": "vendor/php-http/react-adapter/tests/", 45 | "Http\\Adapter\\Cake\\Tests\\": "vendor/php-http/cachephp-adapter/tests/", 46 | "Http\\Adapter\\Zend\\Tests\\": "vendor/php-http/zend-adapter/tests/" 47 | } 48 | }, 49 | "bin": [ 50 | "bin/http_test_server" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /src/HttpClientTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class HttpClientTest extends HttpBaseTest 14 | { 15 | protected ClientInterface $httpAdapter; 16 | 17 | protected function setUp(): void 18 | { 19 | $this->httpAdapter = $this->createHttpAdapter(); 20 | } 21 | 22 | protected function tearDown(): void 23 | { 24 | unset($this->httpAdapter); 25 | } 26 | 27 | abstract protected function createHttpAdapter(): ClientInterface; 28 | 29 | /** 30 | * @dataProvider requestProvider 31 | * @group integration 32 | */ 33 | #[Group('integration')] 34 | #[DataProvider('requestProvider')] 35 | public function testSendRequest(string $method, string $uri, array $headers, ?string $body) 36 | { 37 | if (null != $body) { 38 | $headers['Content-Length'] = (string) strlen($body); 39 | } 40 | 41 | $request = self::$requestFactory->createRequest($method, $uri); 42 | foreach ($headers as $name => $value) { 43 | $request = $request->withHeader($name, $value); 44 | } 45 | if (null !== $body) { 46 | $request = $request->withBody(self::$streamFactory->createStream($body)); 47 | } 48 | 49 | 50 | $response = $this->httpAdapter->sendRequest($request); 51 | 52 | $this->assertResponse( 53 | $response, 54 | [ 55 | 'body' => 'HEAD' === $method ? null : 'Ok', 56 | ] 57 | ); 58 | $this->assertRequest($method, $headers, $body, '1.1'); 59 | } 60 | 61 | /** 62 | * @dataProvider requestWithOutcomeProvider 63 | * @group integration 64 | */ 65 | #[Group('integration')] 66 | #[DataProvider('requestWithOutcomeProvider')] 67 | public function testSendRequestWithOutcome(array $uriAndOutcome, string $protocolVersion, array $headers, ?string $body): void 68 | { 69 | if ('1.0' === $protocolVersion) { 70 | $body = null; 71 | } 72 | 73 | if (null != $body) { 74 | $headers['Content-Length'] = (string) strlen($body); 75 | } 76 | 77 | $request = self::$requestFactory->createRequest($method = 'GET', $uriAndOutcome[0]); 78 | foreach ($headers as $name => $value) { 79 | $request = $request->withHeader($name, $value); 80 | } 81 | if (null !== $body) { 82 | $request = $request->withBody(self::$streamFactory->createStream($body)); 83 | } 84 | $request->withProtocolVersion($protocolVersion); 85 | 86 | $response = $this->httpAdapter->sendRequest($request); 87 | 88 | $outcome = $uriAndOutcome[1]; 89 | $outcome['protocolVersion'] = $protocolVersion; 90 | 91 | $this->assertResponse($response, $outcome); 92 | $this->assertRequest($method, $headers, $body, $protocolVersion); 93 | } 94 | 95 | /** 96 | * @group integration 97 | */ 98 | #[Group('integration')] 99 | public function testSendWithInvalidUri(): void 100 | { 101 | $request = self::$requestFactory->createRequest( 102 | 'GET', 103 | $this->getInvalidUri(), 104 | ); 105 | foreach (self::$defaultHeaders as $name => $value) { 106 | $request = $request->withHeader($name, $value); 107 | } 108 | 109 | $this->expectException(NetworkExceptionInterface::class); 110 | $this->httpAdapter->sendRequest($request); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 7 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | # 4.x 10 | 11 | ## [4.0.0] - 2025-12-09 12 | 13 | - Allow PHPUnit > 9 14 | - Removed deprecated PhpUnitBackwardCompatibleTrait 15 | - Replaced deprecated HttpBaseTest::$messageFactory with $requestFactory and $streamFactory 16 | - Parameter type and return type declarations on all test classes 17 | 18 | # 3.x 19 | 20 | ## [3.1.3] - 2025-12-08 21 | 22 | - Fixed BC breaks accidentally introduced in 3.1.2 23 | 24 | ## [3.1.2] - 2025-12-08 25 | 26 | - Allow PHPUnit > 9 27 | - Deprecate PhpUnitBackwardCompatibleTrait 28 | 29 | ## [3.1.1] - 2024-09-01 30 | 31 | - Switched to `httpbin.org` for tests now that its fixed. (Reverts [#56](https://github.com/php-http/client-integration-tests/pull/56)) 32 | 33 | ## [3.1.0] - 2024-03-05 34 | 35 | - Removed builds for abandoned guzzle5 and guzzle6 adapters 36 | - Support PSR-17 factories 37 | 38 | ## [3.0.1] - 2021-03-21 39 | 40 | - Allow to be installed with Guzzle PSR-7 2.0 41 | 42 | ## [3.0.0] - 2020-10-01 43 | 44 | - Only support HTTPlug 2.0 and PSR-18 45 | - HTTPlug 2.0 is now optional (only require it if you need to test async) 46 | - HttpClientTest now relies only on PSR-18 (no need for HTTPlug) 47 | - Added support for PHPUnit 8 and 9 48 | 49 | ## [2.0.1] - 2018-12-27 50 | 51 | - Use `__toString()` instead of `getContents()` 52 | 53 | ## [2.0.0] - 2018-11-03 54 | 55 | 56 | ## [1.0.0] - 2018-11-03 57 | 58 | ### Changed 59 | 60 | - Don't test `TRACE` requests with request bodies, as they're not valid requests according to the [RFC](https://tools.ietf.org/html/rfc7231#section-4.3.8). 61 | - Make the test suite PHPUnit 6 compatible 62 | 63 | 64 | ## [0.6.2] - 2017-07-10 65 | 66 | 67 | ## [0.6.1] - 2017-07-10 68 | 69 | 70 | ## [0.6.0] - 2017-05-29 71 | 72 | 73 | ## [0.5.1] - 2016-07-18 74 | 75 | ### Fixed 76 | 77 | - Old name 78 | 79 | 80 | ## [0.5.0] - 2016-07-18 81 | 82 | ### Changed 83 | 84 | - Renamed to client-integration-tests 85 | - Improved pacakge 86 | 87 | 88 | ## [0.4.0] - 2016-03-02 89 | 90 | ### Removed 91 | 92 | - Discovery dependency 93 | 94 | 95 | ## [0.3.1] - 2016-02-11 96 | 97 | ### Changed 98 | 99 | - Updated message dependency 100 | 101 | 102 | ## [0.3.0] - 2016-01-21 103 | 104 | ### Changed 105 | 106 | - Updated discovery dependency 107 | 108 | 109 | ## [0.2.0] - 2016-01-13 110 | 111 | ### Changed 112 | 113 | - Updated to latest HTTPlug version 114 | - Updated package files 115 | 116 | 117 | ## 0.1.0 - 2015-06-12 118 | 119 | ### Added 120 | 121 | - Initial release 122 | 123 | 124 | [Unreleased]: https://github.com/php-http/client-integration-tests/compare/v2.0.0...HEAD 125 | [2.0.0]: https://github.com/php-http/client-integration-tests/compare/v1.0.0...v2.0.0 126 | [1.0.0]: https://github.com/php-http/client-integration-tests/compare/v0.6.2...v1.0.0 127 | [0.6.2]: https://github.com/php-http/client-integration-tests/compare/v0.6.1...v0.6.2 128 | [0.6.1]: https://github.com/php-http/client-integration-tests/compare/v0.6.0...v0.6.1 129 | [0.6.0]: https://github.com/php-http/client-integration-tests/compare/v0.5.1...v0.6.0 130 | [0.5.1]: https://github.com/php-http/client-integration-tests/compare/v0.5.0...v0.5.1 131 | [0.5.0]: https://github.com/php-http/client-integration-tests/compare/v0.4.0...v0.5.0 132 | [0.4.0]: https://github.com/php-http/client-integration-tests/compare/v0.3.1...v0.4.0 133 | [0.3.1]: https://github.com/php-http/client-integration-tests/compare/v0.3.0...v0.3.1 134 | [0.3.0]: https://github.com/php-http/client-integration-tests/compare/v0.2.0...v0.3.0 135 | [0.2.0]: https://github.com/php-http/client-integration-tests/compare/v0.1.0...v0.2.0 136 | -------------------------------------------------------------------------------- /src/HttpAsyncClientTest.php: -------------------------------------------------------------------------------- 1 | httpAsyncClient = $this->createHttpAsyncClient(); 18 | } 19 | 20 | protected function tearDown(): void 21 | { 22 | unset($this->httpAdapter); 23 | } 24 | 25 | abstract protected function createHttpAsyncClient(): HttpAsyncClient; 26 | 27 | public function testSuccessiveCallMustUseResponseInterface(): void 28 | { 29 | $request = self::$requestFactory->createRequest('GET', self::getUri()); 30 | foreach (self::$defaultHeaders as $name => $value) { 31 | $request = $request->withHeader($name, $value); 32 | } 33 | 34 | 35 | $promise = $this->httpAsyncClient->sendAsyncRequest($request); 36 | $this->assertInstanceOf(Promise::class, $promise); 37 | 38 | $response = null; 39 | $promise->then()->then()->then(function ($r) use (&$response) { 40 | $response = $r; 41 | 42 | return $r; 43 | }); 44 | 45 | $promise->wait(false); 46 | $this->assertResponse( 47 | $response, 48 | [ 49 | 'body' => 'Ok', 50 | ] 51 | ); 52 | } 53 | 54 | public function testSuccessiveInvalidCallMustUseException(): void 55 | { 56 | $request = self::$requestFactory->createRequest('GET', $this->getInvalidUri()); 57 | foreach (self::$defaultHeaders as $name => $value) { 58 | $request = $request->withHeader($name, $value); 59 | } 60 | 61 | $promise = $this->httpAsyncClient->sendAsyncRequest($request); 62 | $this->assertInstanceOf(Promise::class, $promise); 63 | 64 | $exception = null; 65 | $response = null; 66 | $promise->then()->then()->then(function ($r) use (&$response) { 67 | $response = $r; 68 | 69 | return $r; 70 | }, function ($e) use (&$exception) { 71 | $exception = $e; 72 | 73 | throw $e; 74 | }); 75 | 76 | $promise->wait(false); 77 | 78 | $this->assertNull($response); 79 | $this->assertNotNull($exception); 80 | $this->assertInstanceOf(Exception::class, $exception); 81 | } 82 | 83 | /** 84 | * @dataProvider requestProvider 85 | * @group integration 86 | */ 87 | #[DataProvider('requestProvider')] 88 | #[Group('integration')] 89 | public function testAsyncSendRequest(string $method, string $uri, array $headers, ?string $body): void 90 | { 91 | if (null != $body) { 92 | $headers['Content-Length'] = (string) strlen($body); 93 | } 94 | 95 | $request = self::$requestFactory->createRequest($method, $uri); 96 | foreach ($headers as $name => $value) { 97 | $request = $request->withHeader($name, $value); 98 | } 99 | if (null !== $body) { 100 | $request = $request->withBody(self::$streamFactory->createStream($body)); 101 | } 102 | 103 | $promise = $this->httpAsyncClient->sendAsyncRequest($request); 104 | $this->assertInstanceOf(Promise::class, $promise); 105 | 106 | $response = null; 107 | $promise->then(function ($r) use (&$response) { 108 | $response = $r; 109 | 110 | return $response; 111 | }); 112 | 113 | $promise->wait(); 114 | $this->assertResponse( 115 | $response, 116 | [ 117 | 'body' => 'HEAD' === $method ? null : 'Ok', 118 | ] 119 | ); 120 | $this->assertRequest($method, $headers, $body, '1.1'); 121 | } 122 | 123 | /** 124 | * @group integration 125 | */ 126 | #[Group('integration')] 127 | public function testSendAsyncWithInvalidUri(): void 128 | { 129 | $request = self::$requestFactory->createRequest('GET', $this->getInvalidUri()); 130 | foreach (self::$defaultHeaders as $name => $value) { 131 | $request = $request->withHeader($name, $value); 132 | } 133 | 134 | $exception = null; 135 | $response = null; 136 | $promise = $this->httpAsyncClient->sendAsyncRequest($request); 137 | $this->assertInstanceOf(Promise::class, $promise); 138 | 139 | $promise->then(function ($r) use (&$response) { 140 | $response = $r; 141 | 142 | return $response; 143 | }, function ($e) use (&$exception) { 144 | $exception = $e; 145 | 146 | throw $e; 147 | }); 148 | $promise->wait(false); 149 | 150 | $this->assertNull($response); 151 | $this->assertNotNull($exception); 152 | $this->assertInstanceOf(Exception::class, $exception); 153 | } 154 | 155 | /** 156 | * @dataProvider requestWithOutcomeProvider 157 | * @group integration 158 | */ 159 | #[Group('integration')] 160 | #[DataProvider('requestWithOutcomeProvider')] 161 | public function testSendAsyncRequestWithOutcome(array $uriAndOutcome, string $protocolVersion, array $headers, ?string $body): void 162 | { 163 | if ('1.0' === $protocolVersion) { 164 | $body = null; 165 | } 166 | 167 | if (null != $body) { 168 | $headers['Content-Length'] = (string) strlen($body); 169 | } 170 | 171 | $request = self::$requestFactory->createRequest($method = 'GET', $uriAndOutcome[0]); 172 | foreach ($headers as $name => $value) { 173 | $request = $request->withHeader($name, $value); 174 | } 175 | if (null !== $body) { 176 | $request = $request->withBody(self::$streamFactory->createStream($body)); 177 | } 178 | $request = $request->withProtocolVersion($protocolVersion); 179 | 180 | $outcome = $uriAndOutcome[1]; 181 | $outcome['protocolVersion'] = $protocolVersion; 182 | 183 | $response = null; 184 | $promise = $this->httpAsyncClient->sendAsyncRequest($request); 185 | $promise->then(function ($r) use (&$response) { 186 | $response = $r; 187 | 188 | return $response; 189 | }); 190 | 191 | $this->assertInstanceOf(Promise::class, $promise); 192 | $promise->wait(); 193 | $this->assertResponse( 194 | $response, 195 | $outcome 196 | ); 197 | $this->assertRequest($method, $headers, $body, $protocolVersion); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/HttpFeatureTest.php: -------------------------------------------------------------------------------- 1 | createRequest( 29 | 'GET', 30 | 'https://httpbin.org/get' 31 | ); 32 | 33 | $response = $this->createClient()->sendRequest($request); 34 | 35 | $this->assertSame(200, $response->getStatusCode()); 36 | } 37 | 38 | /** 39 | * @feature Send a POST Request 40 | */ 41 | public function testPost(): void 42 | { 43 | $testData = 'Test data'; 44 | $request = self::$messageFactory->createRequest('POST', 'https://httpbin.org/post'); 45 | $request = $request->withHeader('Content-Length', strlen($testData)); 46 | $request = $request->withBody(self::$streamFactory->createStream($testData)); 47 | 48 | $response = $this->createClient()->sendRequest($request); 49 | 50 | $this->assertSame(200, $response->getStatusCode()); 51 | 52 | $contents = json_decode($response->getBody()->__toString()); 53 | 54 | $this->assertEquals($testData, $contents->data); 55 | } 56 | 57 | /** 58 | * @feature Send a PATCH Request 59 | */ 60 | public function testPatch(): void 61 | { 62 | $request = self::$messageFactory->createRequest( 63 | 'PATCH', 64 | 'https://httpbin.org/patch' 65 | ); 66 | 67 | $response = $this->createClient()->sendRequest($request); 68 | 69 | $this->assertSame(200, $response->getStatusCode()); 70 | } 71 | 72 | /** 73 | * @feature Send a PUT Request 74 | */ 75 | public function testPut(): void 76 | { 77 | $request = self::$messageFactory->createRequest( 78 | 'PUT', 79 | 'https://httpbin.org/put' 80 | ); 81 | 82 | $response = $this->createClient()->sendRequest($request); 83 | 84 | $this->assertSame(200, $response->getStatusCode()); 85 | } 86 | 87 | /** 88 | * @feature Send a DELETE Request 89 | */ 90 | public function testDelete(): void 91 | { 92 | $request = self::$messageFactory->createRequest( 93 | 'DELETE', 94 | 'https://httpbin.org/delete' 95 | ); 96 | 97 | $response = $this->createClient()->sendRequest($request); 98 | 99 | $this->assertSame(200, $response->getStatusCode()); 100 | } 101 | 102 | /** 103 | * @feature Auto fixing content length header 104 | */ 105 | public function testAutoSetContentLength(): void 106 | { 107 | $testData = 'Test data'; 108 | $request = self::$messageFactory->createRequest( 109 | 'POST', 110 | 'https://httpbin.org/post', 111 | ); 112 | $request = $request->withBody(self::$streamFactory->createStream($testData)); 113 | 114 | $response = $this->createClient()->sendRequest($request); 115 | 116 | $this->assertSame(200, $response->getStatusCode()); 117 | 118 | $contents = json_decode($response->getBody()->__toString()); 119 | 120 | $this->assertEquals($testData, $contents->data); 121 | } 122 | 123 | /** 124 | * @feature Encoding in UTF8 125 | */ 126 | public function testEncoding(): void 127 | { 128 | $request = self::$messageFactory->createRequest( 129 | 'GET', 130 | 'https://httpbin.org/encoding/utf8' 131 | ); 132 | 133 | $response = $this->createClient()->sendRequest($request); 134 | 135 | $this->assertSame(200, $response->getStatusCode()); 136 | $this->assertStringContainsString('€', $response->getBody()->__toString()); 137 | } 138 | 139 | /** 140 | * @feature Gzip content decoding 141 | */ 142 | public function testGzip(): void 143 | { 144 | $request = self::$messageFactory->createRequest( 145 | 'GET', 146 | 'https://httpbin.org/gzip' 147 | ); 148 | 149 | $response = $this->createClient()->sendRequest($request); 150 | 151 | $this->assertSame(200, $response->getStatusCode()); 152 | $this->assertStringContainsString('gzip', $response->getBody()->__toString()); 153 | } 154 | 155 | /** 156 | * @feature Deflate content decoding 157 | */ 158 | public function testDeflate(): void 159 | { 160 | $request = self::$messageFactory->createRequest( 161 | 'GET', 162 | 'https://httpbin.org/deflate' 163 | ); 164 | 165 | $response = $this->createClient()->sendRequest($request); 166 | 167 | $this->assertSame(200, $response->getStatusCode()); 168 | $this->assertStringContainsString('deflate', $response->getBody()->__toString()); 169 | } 170 | 171 | /** 172 | * @feature Follow redirection 173 | */ 174 | public function testRedirect(): void 175 | { 176 | $request = self::$messageFactory->createRequest( 177 | 'GET', 178 | 'https://httpbin.org/redirect/1' 179 | ); 180 | 181 | $response = $this->createClient()->sendRequest($request); 182 | 183 | $this->assertSame(200, $response->getStatusCode()); 184 | } 185 | 186 | /** 187 | * @feature Dechunk stream body 188 | */ 189 | public function testChunked(): void 190 | { 191 | $request = self::$messageFactory->createRequest( 192 | 'GET', 193 | 'https://httpbin.org/stream/1' 194 | ); 195 | 196 | $response = $this->createClient()->sendRequest($request); 197 | 198 | $this->assertSame(200, $response->getStatusCode()); 199 | 200 | $content = @json_decode($response->getBody()->__toString()); 201 | 202 | $this->assertNotNull($content); 203 | } 204 | 205 | /** 206 | * @feature Ssl connection 207 | */ 208 | public function testSsl(): void 209 | { 210 | $request = self::$messageFactory->createRequest( 211 | 'GET', 212 | 'https://httpbin.org/get' 213 | ); 214 | 215 | $response = $this->createClient()->sendRequest($request); 216 | 217 | $this->assertSame(200, $response->getStatusCode()); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/HttpBaseTest.php: -------------------------------------------------------------------------------- 1 | '1.1', 21 | 'statusCode' => 200, 22 | 'reasonPhrase' => 'OK', 23 | 'headers' => ['Content-Type' => 'text/html'], 24 | 'body' => 'Ok', 25 | ]; 26 | 27 | protected static array $defaultHeaders = [ 28 | 'Connection' => 'close', 29 | 'User-Agent' => 'PHP HTTP Adapter', 30 | 'Content-Length' => '0', 31 | ]; 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public static function setUpBeforeClass(): void 37 | { 38 | self::$logPath = PHPUnitUtility::getFile(true, 'php-http-adapter.log'); 39 | self::$requestFactory = self::$streamFactory = new HttpFactory(); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public static function tearDownAfterClass(): void 46 | { 47 | if (file_exists(self::$logPath)) { 48 | unlink(self::$logPath); 49 | } 50 | } 51 | 52 | public static function requestProvider(): array 53 | { 54 | $sets = [ 55 | 'methods' => self::getMethods(), 56 | 'uris' => [self::getUri()], 57 | 'headers' => self::getHeaders(), 58 | 'body' => self::getBodies(), 59 | ]; 60 | 61 | $cartesianProduct = new CartesianProduct($sets); 62 | 63 | $cases = $cartesianProduct->compute(); 64 | 65 | // Filter all TRACE requests with a body, as they're not HTTP spec compliant 66 | return array_filter($cases, function ($case) { 67 | if ('TRACE' === $case[0] && null !== $case[3]) { 68 | return false; 69 | } 70 | 71 | return true; 72 | }); 73 | } 74 | 75 | public static function requestWithOutcomeProvider(): array 76 | { 77 | $sets = [ 78 | 'urisAndOutcomes' => self::getUrisAndOutcomes(), 79 | 'protocolVersions' => self::getProtocolVersions(), 80 | 'headers' => self::getHeaders(), 81 | 'body' => self::getBodies(), 82 | ]; 83 | 84 | $cartesianProduct = new CartesianProduct($sets); 85 | 86 | return $cartesianProduct->compute(); 87 | } 88 | 89 | private static function getMethods(): array 90 | { 91 | return [ 92 | 'GET', 93 | 'HEAD', 94 | 'TRACE', 95 | 'POST', 96 | 'PUT', 97 | 'DELETE', 98 | 'OPTIONS', 99 | ]; 100 | } 101 | 102 | /** 103 | * @param string[] $query 104 | * 105 | * @return string|null 106 | */ 107 | protected static function getUri(array $query = []): ?string 108 | { 109 | return !empty($query) 110 | ? PHPUnitUtility::getUri().'?'.http_build_query($query, '', '&') 111 | : PHPUnitUtility::getUri(); 112 | } 113 | 114 | protected function getInvalidUri(): string 115 | { 116 | return 'http://invalid.php-http.org'; 117 | } 118 | 119 | private static function getUrisAndOutcomes(): array 120 | { 121 | return [ 122 | [ 123 | self::getUri(['client_error' => true]), 124 | [ 125 | 'statusCode' => 400, 126 | 'reasonPhrase' => 'Bad Request', 127 | ], 128 | ], 129 | [ 130 | self::getUri(['server_error' => true]), 131 | [ 132 | 'statusCode' => 500, 133 | 'reasonPhrase' => 'Internal Server Error', 134 | ], 135 | ], 136 | [ 137 | self::getUri(['redirect' => true]), 138 | [ 139 | 'statusCode' => 302, 140 | 'reasonPhrase' => 'Found', 141 | 'body' => 'Redirect', 142 | ], 143 | ], 144 | ]; 145 | } 146 | 147 | private static function getProtocolVersions(): array 148 | { 149 | return ['1.1', '1.0']; 150 | } 151 | 152 | /** 153 | * @return string[] 154 | */ 155 | private static function getHeaders(): array 156 | { 157 | $headers = self::$defaultHeaders; 158 | $headers['Accept-Charset'] = 'utf-8'; 159 | $headers['Accept-Language'] = 'en'; 160 | 161 | return [ 162 | self::$defaultHeaders, 163 | $headers, 164 | ]; 165 | } 166 | 167 | private static function getBodies(): array 168 | { 169 | return [ 170 | null, 171 | http_build_query(self::getData(), '', '&'), 172 | ]; 173 | } 174 | 175 | private static function getData(): array 176 | { 177 | return ['param1' => 'foo', 'param2' => ['bar', ['baz']]]; 178 | } 179 | 180 | protected function assertResponse(ResponseInterface $response, array $options = []) 181 | { 182 | $options = array_merge($this->defaultOptions, $options); 183 | 184 | // The response version may be greater or equal to the request version. See https://tools.ietf.org/html/rfc2145#section-2.3 185 | $this->assertTrue(substr($options['protocolVersion'], 0, 1) === substr($response->getProtocolVersion(), 0, 1) && 1 !== version_compare($options['protocolVersion'], $response->getProtocolVersion())); 186 | $this->assertSame($options['statusCode'], $response->getStatusCode()); 187 | $this->assertSame($options['reasonPhrase'], $response->getReasonPhrase()); 188 | 189 | $this->assertNotEmpty($response->getHeaders()); 190 | 191 | foreach ($options['headers'] as $name => $value) { 192 | $this->assertTrue($response->hasHeader($name)); 193 | $this->assertStringStartsWith($value, $response->getHeaderLine($name)); 194 | } 195 | 196 | if (null === $options['body']) { 197 | $this->assertEmpty($response->getBody()->__toString()); 198 | } else { 199 | self::assertStringContainsString($options['body'], $response->getBody()->__toString()); 200 | } 201 | } 202 | 203 | /** 204 | * @param string[] $headers 205 | */ 206 | protected function assertRequest( 207 | string $method, 208 | array $headers = [], 209 | ?string $body = null, 210 | string $protocolVersion = '1.1' 211 | ) { 212 | $request = $this->getRequest(); 213 | 214 | $actualProtocolVersion = substr($request['SERVER']['SERVER_PROTOCOL'], 5); 215 | $this->assertTrue(substr($protocolVersion, 0, 1) === substr($actualProtocolVersion, 0, 1) && 1 !== version_compare($protocolVersion, $actualProtocolVersion)); 216 | $this->assertSame($method, $request['SERVER']['REQUEST_METHOD']); 217 | 218 | $defaultHeaders = [ 219 | 'Connection' => 'close', 220 | 'User-Agent' => 'PHP HTTP Adapter', 221 | ]; 222 | 223 | $headers = array_merge($defaultHeaders, $headers); 224 | 225 | foreach ($headers as $name => $value) { 226 | if (is_int($name)) { 227 | list($name, $value) = explode(':', $value); 228 | } 229 | 230 | $name = strtoupper(str_replace('-', '_', 'http-'.$name)); 231 | 232 | if ('TRACE' === $method && 'HTTP_CONTENT_LENGTH' === $name && !isset($request['SERVER'][$name])) { 233 | continue; 234 | } 235 | 236 | $this->assertArrayHasKey($name, $request['SERVER']); 237 | $this->assertSame($value, $request['SERVER'][$name], "Failed asserting value for {$name}."); 238 | } 239 | } 240 | 241 | protected function getRequest(): array 242 | { 243 | $file = fopen(self::$logPath, 'r'); 244 | flock($file, LOCK_EX); 245 | $request = json_decode(stream_get_contents($file), true); 246 | flock($file, LOCK_UN); 247 | fclose($file); 248 | 249 | return $request; 250 | } 251 | } 252 | --------------------------------------------------------------------------------