├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── BaseTest.php ├── MessageTrait.php ├── RequestIntegrationTest.php ├── ResponseIntegrationTest.php ├── ServerRequestIntegrationTest.php ├── StreamIntegrationTest.php ├── UploadedFileIntegrationTest.php └── UriIntegrationTest.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.4.0] - 2024-08-02 9 | 10 | ### Added 11 | 12 | - Support for PHPUnit 10. 13 | 14 | ### Removed 15 | 16 | - Support for PHP 7.2 and PHPUnit 9. 17 | 18 | ## [1.3.0] - 2023-04-28 19 | 20 | ### Added 21 | 22 | - Adds `UriIntegrationTest::testSpecialCharsInUserInfo` and `UriIntegrationTest::testAlreadyEncodedUserInfo`. 23 | These validate that usernames and passwords which contain reserved characters (defined by RFC3986) are being encoded 24 | so that the URI does not contain these reserved characters at any time. 25 | 26 | - Adds support for testing against PSR-7 1.1 and 2.0. In particular, it adapts tests that were verifying invalid parameters threw `InvalidArgumentException` previously now either throw that OR (more correctly) raise a `TypeError`. 27 | 28 | ## [1.2.0] - 2022-12-01 29 | 30 | ### Added 31 | 32 | - Adds `UriIntegrationTest::testGetPathNormalizesMultipleLeadingSlashesToSingleSlashToPreventXSS()`, `UriIntegrationTest::testStringRepresentationWithMultipleSlashes(array $test)`, and `RequestIntegrationTest::testGetRequestTargetInOriginFormNormalizesUriWithMultipleLeadingSlashesInPath()`. 33 | These validate that a path containing multiple leading slashes is (a) represented with a single slash when calling `UriInterface::getPath()`, and (b) represented without changes when calling `UriInterface::__toString()`, including when calling `RequestInterface::getRequestTarget()` (which returns the path without the URI authority by default, to comply with origin-form). 34 | This is done to validate mitigations for [CVE-2015-3257](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-3257). 35 | 36 | ### Changed 37 | 38 | - Modifies `UriIntegrationTest::testPathWithMultipleSlashes()` to only validate multiple slashes in the middle of a path. 39 | Multiple leading slashes are covered with the newly introduced tests. 40 | 41 | 42 | ## [1.1.1] - 2021-02-20 43 | 44 | ### Changed 45 | 46 | - Replace deprecated assertRegExp() with assertMatchesRegularExpression() 47 | 48 | ## [1.1.0] - 2020-10-17 49 | 50 | ### Added 51 | 52 | - Support for PHP8 and PHPUnit 8 and 9 53 | 54 | ## [1.0.0] - 2019-12-16 55 | 56 | ### Added 57 | - Compatible with PHP5 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 PHP HTTP Team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Message 2 | 3 | [![Total Downloads](https://img.shields.io/packagist/dt/php-http/psr7-integration-tests.svg?style=flat-square)](https://packagist.org/packages/php-http/psr7-integration-tests) 4 | 5 | **Test PSR7 implementations against the specification.** 6 | 7 | ## Status 8 | 9 | | PSR7 Implementation | Status | Legacy | 10 | |---------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------:| 11 | | Guzzle | [![Guzzle](https://github.com/php-http/psr7-integration-tests/actions/workflows/guzzle.yml/badge.svg)](https://github.com/php-http/psr7-integration-tests/actions/workflows/guzzle.yml) | 12 | | Laminas | [![Laminas](https://github.com/php-http/psr7-integration-tests/actions/workflows/laminas.yml/badge.svg)](https://github.com/php-http/psr7-integration-tests/actions/workflows/laminas.yml) | [Legacy](https://github.com/php-http/psr7-integration-tests/actions/workflows/laminas-legacy.yml) (failures expected) | 13 | | Slim | [![Slim](https://github.com/php-http/psr7-integration-tests/actions/workflows/slim.yml/badge.svg)](https://github.com/php-http/psr7-integration-tests/actions/workflows/slim.yml) | 14 | | Nyholm | [![Nyholm](https://github.com/php-http/psr7-integration-tests/actions/workflows/nyholm.yml/badge.svg)](https://github.com/php-http/psr7-integration-tests/actions/workflows/nyholm.yml) | 15 | | RingCentral | [![RingCentral](https://github.com/php-http/psr7-integration-tests/actions/workflows/ringcentral.yml/badge.svg)](https://github.com/php-http/psr7-integration-tests/actions/workflows/ringcentral.yml) | 16 | | HttpSoft | [![HttpSoft](https://github.com/php-http/psr7-integration-tests/actions/workflows/httpsoft.yml/badge.svg)](https://github.com/php-http/psr7-integration-tests/actions/workflows/httpsoft.yml) | 17 | | Fatfree | [![HttpSoft](https://github.com/php-http/psr7-integration-tests/actions/workflows/fatfree.yml/badge.svg)](https://github.com/php-http/psr7-integration-tests/actions/workflows/fatfree.yml) | 18 | 19 | ## Install 20 | 21 | To use the integration tests with a PSR-7 implementation, add this package to the dev dependencies: 22 | 23 | ``` bash 24 | $ composer require --dev php-http/psr7-integration-tests 25 | ``` 26 | 27 | Then set up phpunit to run the tests for your implementation. 28 | 29 | ## Documentation 30 | 31 | Please see the [official documentation](http://docs.php-http.org/en/latest). 32 | 33 | 34 | ## Testing 35 | 36 | This repository also is set up to test a couple of implementations directly. You need to install dependencies from source for the tests to work: 37 | 38 | ``` bash 39 | $ composer update --prefer-source 40 | ``` 41 | 42 | **Note:** If you already have the sources installed, you need to delete the vendor folder before running the above command. 43 | 44 | Run the test suite for one implementation with: 45 | 46 | ``` bash 47 | $ composer test -- --testsuite 48 | ``` 49 | 50 | The names are `Guzzle`, `Laminas`, `Slim`, `Nyholm`, `RingCentral`, `HttpSoft`, `Fatfree`. 51 | 52 | It is also possible to exclude tests that require a live internet connection: 53 | 54 | ``` bash 55 | $ composer test -- --testsuite --exclude-group internet 56 | ``` 57 | 58 | ## Contributing 59 | 60 | Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). 61 | 62 | ## Security 63 | 64 | If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). 65 | 66 | ## License 67 | 68 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-http/psr7-integration-tests", 3 | "description": "Test suite for PSR7", 4 | "keywords": [ 5 | "test", 6 | "psr-7" 7 | ], 8 | "homepage": "http://php-http.org", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Tobias Nyholm", 13 | "email": "tobias.nyholm@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^7.3 || ^8.0", 18 | "phpunit/phpunit": "^9.3 || ^10.0", 19 | "psr/http-message": "^1.0 || ^2.0" 20 | }, 21 | "require-dev": { 22 | "f3-factory/fatfree-psr7": "^2.0", 23 | "guzzlehttp/psr7": "^1.7 || ^2.0", 24 | "httpsoft/http-message": "^1.1", 25 | "laminas/laminas-diactoros": "^2.1", 26 | "nyholm/psr7": "^1.0", 27 | "ringcentral/psr7": "^1.2", 28 | "slim/psr7": "^1.4" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Http\\Psr7Test\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Http\\Psr7Test\\Tests\\": "tests/" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "phpunit" 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/BaseTest.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | abstract class BaseTest extends TestCase 35 | { 36 | protected function assertNotSameObject($a, $b) 37 | { 38 | $this->assertFalse($a === $b, 'Object does not have different references.'); 39 | } 40 | 41 | protected function buildUri($uri) 42 | { 43 | if (defined('URI_FACTORY')) { 44 | $factoryClass = URI_FACTORY; 45 | $factory = new $factoryClass(); 46 | if ($factory instanceof HttplugUriFactory) { 47 | return $factory->createUri($uri); 48 | } 49 | if ($factory instanceof PsrUriFactoryInterface) { 50 | if ($uri instanceof PsrUriInterface) { 51 | return $uri; 52 | } 53 | 54 | return $factory->createUri($uri); 55 | } 56 | 57 | throw new \RuntimeException('Constant "URI_FACTORY" must be a reference to a '.HttplugUriFactory::class.' or '.PsrUriFactoryInterface::class); 58 | } 59 | 60 | if (class_exists(HttpSoftUri::class)) { 61 | return new HttpSoftUri($uri); 62 | } 63 | 64 | if (class_exists(GuzzleUri::class)) { 65 | return new GuzzleUri($uri); 66 | } 67 | 68 | if (class_exists(SlimUri::class)) { 69 | if (class_exists(SlimUriFactory::class)) { 70 | return (new SlimUriFactory())->createUri($uri); 71 | } 72 | 73 | return SlimUri::createFromString($uri); 74 | } 75 | 76 | if (class_exists(LaminasUri::class)) { 77 | return new LaminasUri($uri); 78 | } 79 | 80 | if (class_exists(NyholmFactory::class)) { 81 | return (new NyholmFactory())->createUri($uri); 82 | } 83 | 84 | if (class_exists(RingCentralUri::class)) { 85 | return new RingCentralUri($uri); 86 | } 87 | 88 | if (class_exists(FatfreeFactory::class)) { 89 | return (new FatfreeFactory())->createUri($uri); 90 | } 91 | 92 | throw new \RuntimeException('Could not create URI. Check your config'); 93 | } 94 | 95 | protected function buildStream($data) 96 | { 97 | if (defined('STREAM_FACTORY')) { 98 | $factoryClass = STREAM_FACTORY; 99 | $factory = new $factoryClass(); 100 | if ($factory instanceof HttplugStreamFactory) { 101 | return $factory->createStream($data); 102 | } 103 | if ($factory instanceof PsrStreamFactoryInterface) { 104 | if (is_string($data)) { 105 | return $factory->createStream($data); 106 | } 107 | 108 | return $factory->createStreamFromResource($data); 109 | } 110 | 111 | throw new \RuntimeException('Constant "STREAM_FACTORY" must be a reference to a '.HttplugStreamFactory::class.' or '.PsrStreamFactoryInterface::class); 112 | } 113 | 114 | if (class_exists(GuzzleStream::class)) { 115 | return GuzzleUtils::streamFor($data); 116 | } 117 | 118 | $factory = null; 119 | if (class_exists(HttpSoftStreamFactory::class)) { 120 | $factory = new HttpSoftStreamFactory(); 121 | } 122 | if (class_exists(LaminasStreamFactory::class)) { 123 | $factory = new LaminasStreamFactory(); 124 | } 125 | if (class_exists(NyholmFactory::class)) { 126 | $factory = new NyholmFactory(); 127 | } 128 | if (class_exists(SlimStreamFactory::class)) { 129 | $factory = new SlimStreamFactory(); 130 | } 131 | if (class_exists(FatfreeFactory::class)) { 132 | $factory = new FatfreeFactory(); 133 | } 134 | if ($factory) { 135 | if (is_string($data)) { 136 | return $factory->createStream($data); 137 | } 138 | 139 | return $factory->createStreamFromResource($data); 140 | } 141 | 142 | if (function_exists('ring_central_stream_for')) { 143 | return ring_central_stream_for($data); 144 | } 145 | 146 | throw new \RuntimeException('Could not create Stream. Check your config'); 147 | } 148 | 149 | protected function buildUploadableFile($data) 150 | { 151 | if (defined('UPLOADED_FILE_FACTORY')) { 152 | $factoryClass = UPLOADED_FILE_FACTORY; 153 | $factory = new $factoryClass(); 154 | if (!$factory instanceof PsrUploadedFileFactoryInterface) { 155 | throw new \RuntimeException('Constant "UPLOADED_FILE_FACTORY" must be a reference to a '.PsrUploadedFileFactoryInterface::class); 156 | } 157 | 158 | $stream = $this->buildStream($data); 159 | 160 | return $factory->createUploadedFile($stream); 161 | } 162 | 163 | if (class_exists(HttpSoftUploadedFile::class)) { 164 | return new HttpSoftUploadedFile($data, strlen($data), 0); 165 | } 166 | 167 | if (class_exists(GuzzleUploadedFile::class)) { 168 | return new GuzzleUploadedFile($data, strlen($data), 0); 169 | } 170 | 171 | if (class_exists(LaminasUploadedFile::class)) { 172 | return new LaminasUploadedFile($data, strlen($data), 0); 173 | } 174 | 175 | if (class_exists(NyholmFactory::class)) { 176 | $stream = $this->buildStream($data); 177 | 178 | return (new NyholmFactory())->createUploadedFile($stream); 179 | } 180 | 181 | if (class_exists(SlimUploadedFileFactory::class)) { 182 | $stream = $this->buildStream($data); 183 | 184 | return (new SlimUploadedFileFactory())->createUploadedFile($stream); 185 | } 186 | 187 | if (class_exists(FatfreeFactory::class)) { 188 | $stream = $this->buildStream($data); 189 | 190 | return (new FatfreeFactory())->createUploadedFile($stream); 191 | } 192 | 193 | throw new \RuntimeException('Could not create Stream. Check your config'); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/MessageTrait.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | trait MessageTrait 17 | { 18 | /** 19 | * @return MessageInterface 20 | */ 21 | abstract protected function getMessage(); 22 | 23 | public function testProtocolVersion() 24 | { 25 | if (isset($this->skippedTests[__FUNCTION__])) { 26 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 27 | } 28 | 29 | $initialMessage = $this->getMessage(); 30 | $original = clone $initialMessage; 31 | 32 | $message = $initialMessage->withProtocolVersion('1.0'); 33 | 34 | $this->assertNotSameObject($initialMessage, $message); 35 | $this->assertEquals($initialMessage, $original, 'Message object MUST not be mutated'); 36 | 37 | $this->assertSame('1.0', $message->getProtocolVersion()); 38 | } 39 | 40 | public function testGetHeaders() 41 | { 42 | if (isset($this->skippedTests[__FUNCTION__])) { 43 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 44 | } 45 | 46 | $initialMessage = $this->getMessage(); 47 | $original = clone $initialMessage; 48 | 49 | $message = $initialMessage 50 | ->withAddedHeader('content-type', 'text/html') 51 | ->withAddedHeader('content-type', 'text/plain'); 52 | 53 | $this->assertEquals($initialMessage, $original, 'Message object MUST not be mutated'); 54 | 55 | $headers = $message->getHeaders(); 56 | 57 | $this->assertTrue(isset($headers['content-type'])); 58 | $this->assertCount(2, $headers['content-type']); 59 | $this->assertContains('text/html', $headers['content-type']); 60 | $this->assertContains('text/plain', $headers['content-type']); 61 | } 62 | 63 | public function testHasHeader() 64 | { 65 | if (isset($this->skippedTests[__FUNCTION__])) { 66 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 67 | } 68 | 69 | $message = $this->getMessage()->withAddedHeader('content-type', 'text/html'); 70 | 71 | $this->assertTrue($message->hasHeader('content-type')); 72 | $this->assertTrue($message->hasHeader('Content-Type')); 73 | $this->assertTrue($message->hasHeader('ConTent-Type')); 74 | } 75 | 76 | public function testGetHeader() 77 | { 78 | if (isset($this->skippedTests[__FUNCTION__])) { 79 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 80 | } 81 | 82 | $message = $this->getMessage()->withAddedHeader('content-type', 'text/html'); 83 | $message = $message->withAddedHeader('content-type', 'text/plain'); 84 | $this->assertCount(2, $message->getHeader('content-type')); 85 | $this->assertCount(2, $message->getHeader('Content-Type')); 86 | $this->assertCount(2, $message->getHeader('CONTENT-TYPE')); 87 | $emptyHeader = $message->getHeader('Bar'); 88 | $this->assertCount(0, $emptyHeader); 89 | $this->assertIsArray($emptyHeader); 90 | } 91 | 92 | public function testGetHeaderLine() 93 | { 94 | if (isset($this->skippedTests[__FUNCTION__])) { 95 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 96 | } 97 | 98 | $message = $this->getMessage()->withAddedHeader('content-type', 'text/html'); 99 | $message = $message->withAddedHeader('content-type', 'text/plain'); 100 | $this->assertMatchesRegexp('|text/html, ?text/plain|', $message->getHeaderLine('content-type')); 101 | $this->assertMatchesRegexp('|text/html, ?text/plain|', $message->getHeaderLine('Content-Type')); 102 | $this->assertMatchesRegexp('|text/html, ?text/plain|', $message->getHeaderLine('CONTENT-TYPE')); 103 | 104 | $this->assertSame('', $message->getHeaderLine('Bar')); 105 | } 106 | 107 | public function testWithHeader() 108 | { 109 | if (isset($this->skippedTests[__FUNCTION__])) { 110 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 111 | } 112 | 113 | $initialMessage = $this->getMessage(); 114 | $original = clone $initialMessage; 115 | 116 | $message = $initialMessage->withHeader('content-type', 'text/html'); 117 | $this->assertNotSameObject($initialMessage, $message); 118 | $this->assertEquals($initialMessage, $original, 'Message object MUST not be mutated'); 119 | $this->assertEquals('text/html', $message->getHeaderLine('content-type')); 120 | 121 | $message = $initialMessage->withHeader('content-type', 'text/plain'); 122 | $this->assertEquals('text/plain', $message->getHeaderLine('content-type')); 123 | 124 | $message = $initialMessage->withHeader('Content-TYPE', 'text/script'); 125 | $this->assertEquals('text/script', $message->getHeaderLine('content-type')); 126 | 127 | $message = $initialMessage->withHeader('x-foo', ['bar', 'baz']); 128 | $this->assertMatchesRegexp('|bar, ?baz|', $message->getHeaderLine('x-foo')); 129 | 130 | $message = $initialMessage->withHeader('Bar', ''); 131 | $this->assertTrue($message->hasHeader('Bar')); 132 | $this->assertSame([''], $message->getHeader('Bar')); 133 | } 134 | 135 | /** 136 | * @dataProvider getInvalidHeaderArguments 137 | */ 138 | public function testWithHeaderInvalidArguments($name, $value) 139 | { 140 | if (isset($this->skippedTests[__FUNCTION__])) { 141 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 142 | } 143 | 144 | try { 145 | $initialMessage = $this->getMessage(); 146 | $initialMessage->withHeader($name, $value); 147 | $this->fail('withHeader() should have raised exception on invalid argument'); 148 | } catch (AssertionFailedError $e) { 149 | // invalid argument not caught 150 | throw $e; 151 | } catch (TypeError|InvalidArgumentException $e) { 152 | // valid 153 | $this->assertTrue($e instanceof Throwable); 154 | } catch (Throwable $e) { 155 | // invalid 156 | $this->fail(sprintf( 157 | 'Unexpected exception (%s) thrown from withHeader(); expected TypeError or InvalidArgumentException', 158 | gettype($e) 159 | )); 160 | } 161 | } 162 | 163 | public static function getInvalidHeaderArguments() 164 | { 165 | return [ 166 | [[], 'foo'], 167 | ['foo', []], 168 | ['', ''], 169 | ['foo', false], 170 | [false, 'foo'], 171 | ['foo', new \stdClass()], 172 | [new \stdClass(), 'foo'], 173 | ]; 174 | } 175 | 176 | public function testWithAddedHeader() 177 | { 178 | if (isset($this->skippedTests[__FUNCTION__])) { 179 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 180 | } 181 | 182 | $message = $this->getMessage()->withAddedHeader('content-type', 'text/html'); 183 | $message = $message->withAddedHeader('CONTENT-type', 'text/plain'); 184 | $this->assertMatchesRegexp('|text/html, ?text/plain|', $message->getHeaderLine('content-type')); 185 | $this->assertMatchesRegexp('|text/html, ?text/plain|', $message->getHeaderLine('Content-Type')); 186 | } 187 | 188 | /** 189 | * @dataProvider getInvalidHeaderArguments 190 | */ 191 | public function testWithAddedHeaderInvalidArguments($name, $value) 192 | { 193 | if (isset($this->skippedTests[__FUNCTION__])) { 194 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 195 | } 196 | 197 | try { 198 | $initialMessage = $this->getMessage(); 199 | $initialMessage->withAddedHeader($name, $value); 200 | $this->fail('withAddedHeader() should have raised exception on invalid argument'); 201 | } catch (AssertionFailedError $e) { 202 | // invalid argument not caught 203 | throw $e; 204 | } catch (TypeError|InvalidArgumentException $e) { 205 | // valid 206 | $this->assertTrue($e instanceof Throwable); 207 | } catch (Throwable $e) { 208 | // invalid 209 | $this->fail(sprintf( 210 | 'Unexpected exception (%s) thrown from withAddedHeader(); expected TypeError or InvalidArgumentException', 211 | gettype($e) 212 | )); 213 | } 214 | } 215 | 216 | /** 217 | * Make sure we maintain headers when we add array values. 218 | */ 219 | public function testWithAddedHeaderArrayValue() 220 | { 221 | if (isset($this->skippedTests[__FUNCTION__])) { 222 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 223 | } 224 | 225 | $message = $this->getMessage()->withAddedHeader('content-type', 'text/html'); 226 | $message = $message->withAddedHeader('content-type', ['text/plain', 'application/json']); 227 | 228 | $headerLine = $message->getHeaderLine('content-type'); 229 | $this->assertMatchesRegexp('|text/html|', $headerLine); 230 | $this->assertMatchesRegexp('|text/plain|', $headerLine); 231 | $this->assertMatchesRegexp('|application/json|', $headerLine); 232 | } 233 | 234 | /** 235 | * Make sure we maintain headers when we add array values with keys. 236 | */ 237 | public function testWithAddedHeaderArrayValueAndKeys() 238 | { 239 | if (isset($this->skippedTests[__FUNCTION__])) { 240 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 241 | } 242 | 243 | $message = $this->getMessage()->withAddedHeader('content-type', ['foo' => 'text/html']); 244 | $message = $message->withAddedHeader('content-type', ['foo' => 'text/plain', 'bar' => 'application/json']); 245 | 246 | $headerLine = $message->getHeaderLine('content-type'); 247 | $this->assertMatchesRegexp('|text/html|', $headerLine); 248 | $this->assertMatchesRegexp('|text/plain|', $headerLine); 249 | $this->assertMatchesRegexp('|application/json|', $headerLine); 250 | } 251 | 252 | public function testWithoutHeader() 253 | { 254 | if (isset($this->skippedTests[__FUNCTION__])) { 255 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 256 | } 257 | 258 | $message = $this->getMessage()->withAddedHeader('content-type', 'text/html'); 259 | $message = $message->withAddedHeader('Age', '0'); 260 | $message = $message->withAddedHeader('X-Foo', 'bar'); 261 | 262 | $headers = $message->getHeaders(); 263 | $headerCount = count($headers); 264 | $this->assertTrue(isset($headers['Age'])); 265 | 266 | // Remove a header 267 | $message = $message->withoutHeader('age'); 268 | $headers = $message->getHeaders(); 269 | $this->assertCount($headerCount - 1, $headers); 270 | $this->assertFalse(isset($headers['Age'])); 271 | } 272 | 273 | public function testBody() 274 | { 275 | if (isset($this->skippedTests[__FUNCTION__])) { 276 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 277 | } 278 | 279 | $initialMessage = $this->getMessage(); 280 | $original = clone $initialMessage; 281 | $stream = $this->buildStream('foo'); 282 | $message = $initialMessage->withBody($stream); 283 | $this->assertNotSameObject($initialMessage, $message); 284 | $this->assertEquals($initialMessage, $original, 'Message object MUST not be mutated'); 285 | 286 | $this->assertEquals($stream, $message->getBody()); 287 | } 288 | 289 | private function assertMatchesRegexp(string $pattern, string $string, string $message = ''): void 290 | { 291 | $this->assertMatchesRegularExpression($pattern, $string, $message); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/RequestIntegrationTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class RequestIntegrationTest extends BaseTest 16 | { 17 | use MessageTrait; 18 | 19 | /** 20 | * @var array with functionName => reason 21 | */ 22 | protected $skippedTests = []; 23 | 24 | /** 25 | * @var RequestInterface 26 | */ 27 | protected $request; 28 | 29 | /** 30 | * @return RequestInterface that is used in the tests 31 | */ 32 | abstract public function createSubject(); 33 | 34 | protected function setUp(): void 35 | { 36 | $this->request = $this->createSubject(); 37 | } 38 | 39 | protected function getMessage() 40 | { 41 | return $this->request; 42 | } 43 | 44 | public function testRequestTarget() 45 | { 46 | if (isset($this->skippedTests[__FUNCTION__])) { 47 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 48 | } 49 | 50 | $original = clone $this->request; 51 | $this->assertEquals('/', $this->request->getRequestTarget()); 52 | 53 | $request = $this->request->withRequestTarget('*'); 54 | $this->assertNotSameObject($this->request, $request); 55 | $this->assertEquals($this->request, $original, 'Request object MUST not be mutated'); 56 | $this->assertEquals('*', $request->getRequestTarget()); 57 | } 58 | 59 | public function testMethod() 60 | { 61 | if (isset($this->skippedTests[__FUNCTION__])) { 62 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 63 | } 64 | 65 | $this->assertEquals('GET', $this->request->getMethod()); 66 | $original = clone $this->request; 67 | 68 | $request = $this->request->withMethod('POST'); 69 | $this->assertNotSameObject($this->request, $request); 70 | $this->assertEquals($this->request, $original, 'Request object MUST not be mutated'); 71 | $this->assertEquals('POST', $request->getMethod()); 72 | } 73 | 74 | public function testMethodIsCaseSensitive() 75 | { 76 | if (isset($this->skippedTests[__FUNCTION__])) { 77 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 78 | } 79 | 80 | $request = $this->request->withMethod('head'); 81 | $this->assertEquals('head', $request->getMethod()); 82 | } 83 | 84 | public function testMethodIsExtendable() 85 | { 86 | if (isset($this->skippedTests[__FUNCTION__])) { 87 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 88 | } 89 | 90 | $request = $this->request->withMethod('CUSTOM'); 91 | $this->assertEquals('CUSTOM', $request->getMethod()); 92 | } 93 | 94 | /** 95 | * @dataProvider getInvalidMethods 96 | */ 97 | public function testMethodWithInvalidArguments($method) 98 | { 99 | if (isset($this->skippedTests[__FUNCTION__])) { 100 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 101 | } 102 | 103 | try { 104 | $this->request->withMethod($method); 105 | $this->fail('withMethod() should have raised exception on invalid argument'); 106 | } catch (AssertionFailedError $e) { 107 | // invalid argument not caught 108 | throw $e; 109 | } catch (InvalidArgumentException|TypeError $e) { 110 | // valid 111 | $this->assertTrue($e instanceof Throwable); 112 | } catch (Throwable $e) { 113 | // invalid 114 | $this->fail(sprintf( 115 | 'Unexpected exception (%s) thrown from withMethod(); expected TypeError or InvalidArgumentException', 116 | gettype($e) 117 | )); 118 | } 119 | } 120 | 121 | public static function getInvalidMethods() 122 | { 123 | return [ 124 | 'null' => [null], 125 | 'false' => [false], 126 | 'array' => [['foo']], 127 | 'object' => [new \stdClass()], 128 | ]; 129 | } 130 | 131 | public function testUri() 132 | { 133 | if (isset($this->skippedTests[__FUNCTION__])) { 134 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 135 | } 136 | $original = clone $this->request; 137 | 138 | $this->assertInstanceOf(UriInterface::class, $this->request->getUri()); 139 | 140 | $uri = $this->buildUri('http://www.foo.com/bar'); 141 | $request = $this->request->withUri($uri); 142 | $this->assertNotSameObject($this->request, $request); 143 | $this->assertEquals($this->request, $original, 'Request object MUST not be mutated'); 144 | $this->assertEquals('www.foo.com', $request->getHeaderLine('host')); 145 | $this->assertInstanceOf(UriInterface::class, $request->getUri()); 146 | $this->assertEquals('http://www.foo.com/bar', (string) $request->getUri()); 147 | 148 | $request = $request->withUri($this->buildUri('/foobar')); 149 | $this->assertNotSameObject($this->request, $request); 150 | $this->assertEquals($this->request, $original, 'Request object MUST not be mutated'); 151 | $this->assertEquals('www.foo.com', $request->getHeaderLine('host'), 'If the URI does not contain a host component, any pre-existing Host header MUST be carried over to the returned request.'); 152 | $this->assertEquals('/foobar', (string) $request->getUri()); 153 | } 154 | 155 | public function testUriPreserveHost_NoHost_Host() 156 | { 157 | if (isset($this->skippedTests[__FUNCTION__])) { 158 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 159 | } 160 | 161 | $request = $this->request->withUri($this->buildUri('http://www.foo.com/bar'), true); 162 | $this->assertEquals('www.foo.com', $request->getHeaderLine('host')); 163 | } 164 | 165 | public function testUriPreserveHost_NoHost_NoHost() 166 | { 167 | if (isset($this->skippedTests[__FUNCTION__])) { 168 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 169 | } 170 | 171 | $host = $this->request->getHeaderLine('host'); 172 | $request = $this->request->withUri($this->buildUri('/bar'), true); 173 | $this->assertEquals($host, $request->getHeaderLine('host')); 174 | } 175 | 176 | public function testUriPreserveHost_Host_Host() 177 | { 178 | if (isset($this->skippedTests[__FUNCTION__])) { 179 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 180 | } 181 | 182 | $request = $this->request->withUri($this->buildUri('http://www.foo.com/bar')); 183 | $host = $request->getHeaderLine('host'); 184 | 185 | $request2 = $request->withUri($this->buildUri('http://www.bar.com/foo'), true); 186 | $this->assertEquals($host, $request2->getHeaderLine('host')); 187 | } 188 | 189 | /** 190 | * Tests that getRequestTarget(), when using the default behavior of 191 | * displaying the origin-form, normalizes multiple leading slashes in the 192 | * path to a single slash. This is done to prevent URL poisoning and/or XSS 193 | * issues. 194 | * 195 | * @see UriIntegrationTest::testGetPathNormalizesMultipleLeadingSlashesToSingleSlashToPreventXSS 196 | */ 197 | public function testGetRequestTargetInOriginFormNormalizesUriWithMultipleLeadingSlashesInPath() 198 | { 199 | if (isset($this->skippedTests[__FUNCTION__])) { 200 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 201 | } 202 | 203 | $url = 'http://example.org//valid///path'; 204 | $request = $this->request->withUri($this->buildUri($url)); 205 | $requestTarget = $request->getRequestTarget(); 206 | 207 | $this->assertSame('/valid///path', $requestTarget); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/ResponseIntegrationTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | abstract class ResponseIntegrationTest extends BaseTest 15 | { 16 | use MessageTrait; 17 | 18 | /** 19 | * @var array with functionName => reason 20 | */ 21 | protected $skippedTests = []; 22 | 23 | /** 24 | * @var ResponseInterface 25 | */ 26 | private $response; 27 | 28 | /** 29 | * @return ResponseInterface that is used in the tests 30 | */ 31 | abstract public function createSubject(); 32 | 33 | protected function setUp(): void 34 | { 35 | $this->response = $this->createSubject(); 36 | } 37 | 38 | protected function getMessage() 39 | { 40 | return $this->response; 41 | } 42 | 43 | public function testStatusCode() 44 | { 45 | if (isset($this->skippedTests[__FUNCTION__])) { 46 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 47 | } 48 | 49 | $original = clone $this->response; 50 | $response = $this->response->withStatus(204); 51 | $this->assertNotSameObject($this->response, $response); 52 | $this->assertEquals($this->response, $original, 'Response MUST not be mutated'); 53 | $this->assertSame(204, $response->getStatusCode()); 54 | } 55 | 56 | /** 57 | * @dataProvider getInvalidStatusCodeArguments 58 | */ 59 | public function testStatusCodeInvalidArgument($statusCode) 60 | { 61 | if (isset($this->skippedTests[__FUNCTION__])) { 62 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 63 | } 64 | 65 | try { 66 | $this->response->withStatus($statusCode); 67 | $this->fail('withStatus() should have raised exception on invalid argument'); 68 | } catch (AssertionFailedError $e) { 69 | // invalid argument not caught 70 | throw $e; 71 | } catch (InvalidArgumentException|TypeError $e) { 72 | // valid 73 | $this->assertTrue($e instanceof Throwable); 74 | } catch (Throwable $e) { 75 | // invalid 76 | $this->fail(sprintf( 77 | 'Unexpected exception (%s) thrown from withStatus(); expected TypeError or InvalidArgumentException', 78 | gettype($e) 79 | )); 80 | } 81 | } 82 | 83 | public static function getInvalidStatusCodeArguments() 84 | { 85 | return [ 86 | 'true' => [true], 87 | 'string' => ['foobar'], 88 | 'too-low' => [99], 89 | 'too-high' => [600], 90 | 'object' => [new \stdClass()], 91 | ]; 92 | } 93 | 94 | public function testReasonPhrase() 95 | { 96 | if (isset($this->skippedTests[__FUNCTION__])) { 97 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 98 | } 99 | 100 | $response = $this->response->withStatus(204, 'Foobar'); 101 | $this->assertSame(204, $response->getStatusCode()); 102 | $this->assertEquals('Foobar', $response->getReasonPhrase()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ServerRequestIntegrationTest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | abstract class ServerRequestIntegrationTest extends RequestIntegrationTest 11 | { 12 | /** 13 | * @var ServerRequestInterface 14 | */ 15 | private $serverRequest; 16 | 17 | /** 18 | * @return ServerRequestInterface that is used in the tests 19 | */ 20 | abstract public function createSubject(); 21 | 22 | protected function setUp(): void 23 | { 24 | parent::setUp(); 25 | $this->serverRequest = $this->createSubject(); 26 | } 27 | 28 | public function testGetServerParams() 29 | { 30 | if (isset($this->skippedTests[__FUNCTION__])) { 31 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 32 | } 33 | 34 | $this->assertEquals($_SERVER, $this->serverRequest->getServerParams()); 35 | } 36 | 37 | public function testGetCookieParams() 38 | { 39 | if (isset($this->skippedTests[__FUNCTION__])) { 40 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 41 | } 42 | 43 | $this->assertEquals($_COOKIE, $this->serverRequest->getCookieParams()); 44 | } 45 | 46 | public function testWithCookieParams() 47 | { 48 | if (isset($this->skippedTests[__FUNCTION__])) { 49 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 50 | } 51 | 52 | $orgCookie = $_COOKIE; 53 | $new = $this->serverRequest->withCookieParams(['foo' => 'bar']); 54 | 55 | $this->assertEquals($orgCookie, $this->serverRequest->getCookieParams(), 'Super global $_COOKIE MUST NOT change.'); 56 | $this->assertNotEquals($orgCookie, $new->getCookieParams()); 57 | 58 | $this->assertArrayHasKey('foo', $new->getCookieParams()); 59 | } 60 | 61 | public function testGetQueryParams() 62 | { 63 | if (isset($this->skippedTests[__FUNCTION__])) { 64 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 65 | } 66 | 67 | $new = $this->serverRequest->withQueryParams(['foo' => 'bar']); 68 | $this->assertEmpty($this->serverRequest->getQueryParams(), 'withQueryParams MUST be immutable'); 69 | 70 | $this->assertArrayHasKey('foo', $new->getQueryParams()); 71 | } 72 | 73 | public function testGetUploadedFiles() 74 | { 75 | if (isset($this->skippedTests[__FUNCTION__])) { 76 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 77 | } 78 | 79 | $file = $this->buildUploadableFile('foo'); 80 | $new = $this->serverRequest->withUploadedFiles([$file]); 81 | $this->assertEmpty($this->serverRequest->getUploadedFiles(), 'withUploadedFiles MUST be immutable'); 82 | 83 | $files = $new->getUploadedFiles(); 84 | $this->assertCount(1, $files); 85 | $this->assertEquals($file, $files[0]); 86 | } 87 | 88 | /** 89 | * @dataProvider validParsedBodyParams 90 | */ 91 | public function testGetParsedBody($value) 92 | { 93 | if (isset($this->skippedTests[__FUNCTION__])) { 94 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 95 | } 96 | 97 | $new = $this->serverRequest->withParsedBody($value); 98 | $this->assertNull($this->serverRequest->getParsedBody(), 'withParsedBody MUST be immutable'); 99 | $this->assertEquals($value, $new->getParsedBody()); 100 | } 101 | 102 | public static function validParsedBodyParams() 103 | { 104 | return [ 105 | [null], 106 | [new \stdClass()], 107 | [['foo' => 'bar', 'baz']], 108 | ]; 109 | } 110 | 111 | /** 112 | * @dataProvider invalidParsedBodyParams 113 | */ 114 | public function testGetParsedBodyInvalid($value) 115 | { 116 | if (isset($this->skippedTests[__FUNCTION__])) { 117 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 118 | } 119 | 120 | try { 121 | $this->serverRequest->withParsedBody($value); 122 | $this->fail('Should not be accepted'); 123 | } catch (\InvalidArgumentException $e) { 124 | $this->assertNull($this->serverRequest->getParsedBody(), 'withParsedBody MUST be immutable'); 125 | } 126 | } 127 | 128 | public static function invalidParsedBodyParams() 129 | { 130 | return [ 131 | [4711], 132 | [47.11], 133 | ['foobar'], 134 | [true], 135 | ]; 136 | } 137 | 138 | public function testGetAttributes() 139 | { 140 | if (isset($this->skippedTests[__FUNCTION__])) { 141 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 142 | } 143 | 144 | $new = $this->serverRequest->withAttribute('foo', 'bar'); 145 | $oldAttributes = $this->serverRequest->getAttributes(); 146 | $this->assertIsArray($oldAttributes, 'getAttributes MUST return an array'); 147 | $this->assertEmpty($oldAttributes, 'withAttribute MUST be immutable'); 148 | $this->assertEquals(['foo' => 'bar'], $new->getAttributes()); 149 | 150 | $new = $new->withAttribute('baz', 'biz'); 151 | $this->assertEquals(['foo' => 'bar', 'baz' => 'biz'], $new->getAttributes()); 152 | } 153 | 154 | public function testGetAttribute() 155 | { 156 | if (isset($this->skippedTests[__FUNCTION__])) { 157 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 158 | } 159 | 160 | $new = $this->serverRequest->withAttribute('foo', 'bar'); 161 | $this->assertEquals('bar', $new->getAttribute('foo')); 162 | $this->assertEquals('baz', $new->getAttribute('not found', 'baz')); 163 | $this->assertNull($new->getAttribute('not found')); 164 | } 165 | 166 | public function testWithoutAttribute() 167 | { 168 | if (isset($this->skippedTests[__FUNCTION__])) { 169 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 170 | } 171 | 172 | $with = $this->serverRequest->withAttribute('foo', 'bar'); 173 | $without = $with->withoutAttribute('foo'); 174 | 175 | $this->assertEquals('bar', $with->getAttribute('foo'), 'withoutAttribute MUST be immutable'); 176 | $this->assertNull($without->getAttribute('foo')); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/StreamIntegrationTest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | abstract class StreamIntegrationTest extends BaseTest 11 | { 12 | /** 13 | * @var array with functionName => reason 14 | */ 15 | protected $skippedTests = []; 16 | 17 | /** 18 | * @param string|resource|StreamInterface $data 19 | * 20 | * @return StreamInterface 21 | */ 22 | abstract public function createStream($data); 23 | 24 | public function testToStringReadOnlyStreams() 25 | { 26 | if (isset($this->skippedTests[__FUNCTION__])) { 27 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 28 | } 29 | 30 | $resource = fopen(__FILE__, 'r'); 31 | $stream = $this->createStream($resource); 32 | 33 | // Make sure this does not throw exception 34 | $content = (string) $stream; 35 | $this->assertNotEmpty($content, 'You MUST be able to convert a read only stream to string'); 36 | } 37 | 38 | public function testToStringRewindStreamBeforeToString() 39 | { 40 | if (isset($this->skippedTests[__FUNCTION__])) { 41 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 42 | } 43 | 44 | $resource = fopen('php://memory', 'rw'); 45 | fwrite($resource, 'abcdef'); 46 | fseek($resource, 3); 47 | $stream = $this->createStream($resource); 48 | 49 | $content = (string) $stream; 50 | $this->assertEquals('abcdef', $content); 51 | } 52 | 53 | public function testClose() 54 | { 55 | if (isset($this->skippedTests[__FUNCTION__])) { 56 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 57 | } 58 | 59 | $resource = fopen('php://memory', 'rw'); 60 | fwrite($resource, 'abcdef'); 61 | $stream = $this->createStream($resource); 62 | 63 | $this->assertTrue(is_resource($resource)); 64 | $stream->close(); 65 | $this->assertFalse(is_resource($resource)); 66 | } 67 | 68 | public function testDetach() 69 | { 70 | if (isset($this->skippedTests[__FUNCTION__])) { 71 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 72 | } 73 | 74 | $resource = fopen('php://memory', 'rw'); 75 | fwrite($resource, 'abc'); 76 | $stream = $this->createStream($resource); 77 | 78 | $this->assertEquals($resource, $stream->detach()); 79 | } 80 | 81 | public function testGetSize() 82 | { 83 | if (isset($this->skippedTests[__FUNCTION__])) { 84 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 85 | } 86 | 87 | $resource = fopen('php://memory', 'rw'); 88 | fwrite($resource, 'abc'); 89 | $stream = $this->createStream($resource); 90 | 91 | $this->assertEquals(3, $stream->getSize()); 92 | } 93 | 94 | public function testTell() 95 | { 96 | if (isset($this->skippedTests[__FUNCTION__])) { 97 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 98 | } 99 | 100 | $resource = fopen('php://memory', 'rw'); 101 | fwrite($resource, 'abcdef'); 102 | $stream = $this->createStream($resource); 103 | 104 | $this->assertSame(6, $stream->tell()); 105 | $stream->seek(0); 106 | $this->assertSame(0, $stream->tell()); 107 | $stream->seek(3); 108 | $this->assertSame(3, $stream->tell()); 109 | $stream->seek(6); 110 | $this->assertSame(6, $stream->tell()); 111 | } 112 | 113 | public function testEof() 114 | { 115 | if (isset($this->skippedTests[__FUNCTION__])) { 116 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 117 | } 118 | 119 | $resource = fopen('php://memory', 'rw'); 120 | fwrite($resource, 'abcdef'); 121 | $stream = $this->createStream($resource); 122 | 123 | $stream->seek(0); 124 | $this->assertFalse($stream->eof()); 125 | $stream->read(20); 126 | $stream->read(10); 127 | $this->assertTrue($stream->eof()); 128 | } 129 | 130 | public function testIsSeekable() 131 | { 132 | if (isset($this->skippedTests[__FUNCTION__])) { 133 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 134 | } 135 | 136 | $resource = fopen('php://memory', 'rw'); 137 | fwrite($resource, 'abcdef'); 138 | $stream = $this->createStream($resource); 139 | $this->assertTrue($stream->isSeekable()); 140 | } 141 | 142 | /** 143 | * @group internet 144 | */ 145 | public function testIsNotSeekable() 146 | { 147 | if (isset($this->skippedTests[__FUNCTION__])) { 148 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 149 | } 150 | 151 | $url = 'https://raw.githubusercontent.com/php-http/multipart-stream-builder/master/tests/Resources/httplug.png'; 152 | $resource = fopen($url, 'r'); 153 | $stream = $this->createStream($resource); 154 | $this->assertFalse($stream->isSeekable()); 155 | } 156 | 157 | public function testIsWritable() 158 | { 159 | if (isset($this->skippedTests[__FUNCTION__])) { 160 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 161 | } 162 | 163 | $resource = fopen('php://memory', 'rw'); 164 | fwrite($resource, 'abcdef'); 165 | $stream = $this->createStream($resource); 166 | $this->assertTrue($stream->isWritable()); 167 | } 168 | 169 | /** 170 | * @group internet 171 | */ 172 | public function testIsNotWritable() 173 | { 174 | if (isset($this->skippedTests[__FUNCTION__])) { 175 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 176 | } 177 | 178 | $url = 'https://raw.githubusercontent.com/php-http/multipart-stream-builder/master/tests/Resources/httplug.png'; 179 | $resource = fopen($url, 'r'); 180 | $stream = $this->createStream($resource); 181 | $this->assertFalse($stream->isWritable()); 182 | } 183 | 184 | public function testIsReadable() 185 | { 186 | if (isset($this->skippedTests[__FUNCTION__])) { 187 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 188 | } 189 | 190 | $resource = fopen('php://memory', 'rw'); 191 | fwrite($resource, 'abcdef'); 192 | $stream = $this->createStream($resource); 193 | $this->assertTrue($stream->isReadable()); 194 | } 195 | 196 | /** 197 | * @group internet 198 | */ 199 | public function testIsNotReadable() 200 | { 201 | if (isset($this->skippedTests[__FUNCTION__])) { 202 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 203 | } 204 | 205 | $url = 'https://raw.githubusercontent.com/php-http/multipart-stream-builder/master/tests/Resources/httplug.png'; 206 | $resource = fopen($url, 'r'); 207 | $stream = $this->createStream($resource); 208 | $this->assertTrue($stream->isReadable()); 209 | } 210 | 211 | public function testSeek() 212 | { 213 | if (isset($this->skippedTests[__FUNCTION__])) { 214 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 215 | } 216 | 217 | $resource = fopen('php://memory', 'rw'); 218 | fwrite($resource, 'abcdef'); 219 | $stream = $this->createStream($resource); 220 | $stream->seek(3); 221 | 222 | $this->assertEquals('def', fread($resource, 3)); 223 | } 224 | 225 | public function testRewind() 226 | { 227 | if (isset($this->skippedTests[__FUNCTION__])) { 228 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 229 | } 230 | 231 | $resource = fopen('php://memory', 'rw'); 232 | fwrite($resource, 'abcdef'); 233 | $stream = $this->createStream($resource); 234 | $stream->rewind(); 235 | 236 | $this->assertEquals('abcdef', fread($resource, 6)); 237 | } 238 | 239 | /** 240 | * @group internet 241 | */ 242 | public function testRewindNotSeekable() 243 | { 244 | if (isset($this->skippedTests[__FUNCTION__])) { 245 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 246 | } 247 | 248 | $this->expectException(\RuntimeException::class); 249 | 250 | $url = 'https://raw.githubusercontent.com/php-http/multipart-stream-builder/master/tests/Resources/httplug.png'; 251 | $resource = fopen($url, 'r'); 252 | $stream = $this->createStream($resource); 253 | $stream->rewind(); 254 | } 255 | 256 | public function testWrite() 257 | { 258 | if (isset($this->skippedTests[__FUNCTION__])) { 259 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 260 | } 261 | 262 | $resource = fopen('php://memory', 'rw'); 263 | fwrite($resource, 'abc'); 264 | $stream = $this->createStream($resource); 265 | $bytes = $stream->write('def'); 266 | 267 | $this->assertSame(3, $bytes); 268 | $this->assertEquals('abcdef', (string) $stream); 269 | } 270 | 271 | public function testRead() 272 | { 273 | if (isset($this->skippedTests[__FUNCTION__])) { 274 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 275 | } 276 | 277 | $resource = fopen('php://memory', 'rw'); 278 | fwrite($resource, 'abcdef'); 279 | $stream = $this->createStream($resource); 280 | $stream->rewind(); 281 | 282 | $data = $stream->read(3); 283 | $this->assertEquals('abc', $data); 284 | 285 | $data = $stream->read(10); 286 | $this->assertEquals('def', $data); 287 | } 288 | 289 | public function testGetContents() 290 | { 291 | if (isset($this->skippedTests[__FUNCTION__])) { 292 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 293 | } 294 | 295 | $resource = fopen('php://memory', 'rw'); 296 | fwrite($resource, 'abcdef'); 297 | $stream = $this->createStream($resource); 298 | $stream->rewind(); 299 | 300 | $stream->seek(3); 301 | $this->assertEquals('def', $stream->getContents()); 302 | $this->assertSame('', $stream->getContents()); 303 | } 304 | 305 | public function testGetContentsError() 306 | { 307 | if (isset($this->skippedTests[__FUNCTION__])) { 308 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 309 | } 310 | 311 | $resource = fopen('php://memory', 'rw'); 312 | fwrite($resource, 'abcdef'); 313 | rewind($resource); 314 | $stream = $this->createStream($resource); 315 | 316 | fclose($resource); 317 | 318 | $this->expectException(interface_exists(\Throwable::class) ? \Throwable::class : \RuntimeException::class); 319 | $stream->getContents(); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/UploadedFileIntegrationTest.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | abstract class UploadedFileIntegrationTest extends BaseTest 12 | { 13 | /** 14 | * @var array with functionName => reason 15 | */ 16 | protected $skippedTests = []; 17 | 18 | /** 19 | * @var UploadedFileInterface 20 | */ 21 | private $uploadedFile; 22 | 23 | /** 24 | * @return UploadedFileInterface that is used in the tests 25 | */ 26 | abstract public function createSubject(); 27 | 28 | protected function setUp(): void 29 | { 30 | $this->uploadedFile = $this->createSubject(); 31 | } 32 | 33 | public function testGetStream() 34 | { 35 | if (isset($this->skippedTests[__FUNCTION__])) { 36 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 37 | } 38 | 39 | $file = $this->createSubject(); 40 | 41 | $stream = $file->getStream(); 42 | $this->assertTrue($stream instanceof StreamInterface); 43 | } 44 | 45 | public function testGetStreamAfterMoveTo() 46 | { 47 | if (isset($this->skippedTests[__FUNCTION__])) { 48 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 49 | } 50 | 51 | $file = $this->createSubject(); 52 | $this->expectException(\RuntimeException::class); 53 | $file->moveTo(sys_get_temp_dir().'/'.uniqid('foo', true)); 54 | $file->getStream(); 55 | } 56 | 57 | public function testMoveToAbsolute() 58 | { 59 | if (isset($this->skippedTests[__FUNCTION__])) { 60 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 61 | } 62 | 63 | $file = $this->createSubject(); 64 | $targetPath = sys_get_temp_dir().'/'.uniqid('foo', true); 65 | 66 | $this->assertFalse(is_file($targetPath)); 67 | $file->moveTo($targetPath); 68 | $this->assertTrue(is_file($targetPath)); 69 | } 70 | 71 | public function testMoveToRelative() 72 | { 73 | if (isset($this->skippedTests[__FUNCTION__])) { 74 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 75 | } 76 | 77 | $file = $this->createSubject(); 78 | $targetPath = sys_get_temp_dir().'/'.uniqid('foo', true); 79 | 80 | $this->assertFalse(is_file($targetPath)); 81 | $file->moveTo($targetPath); 82 | $this->assertTrue(is_file($targetPath)); 83 | } 84 | 85 | public function testMoveToTwice() 86 | { 87 | if (isset($this->skippedTests[__FUNCTION__])) { 88 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 89 | } 90 | $this->expectException(\RuntimeException::class); 91 | 92 | $file = $this->createSubject(); 93 | $file->moveTo(sys_get_temp_dir().'/'.uniqid('foo', true)); 94 | $file->moveTo(sys_get_temp_dir().'/'.uniqid('foo', true)); 95 | } 96 | 97 | public function testGetSize() 98 | { 99 | if (isset($this->skippedTests[__FUNCTION__])) { 100 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 101 | } 102 | 103 | $file = $this->createSubject(); 104 | $size = $file->getSize(); 105 | if (null !== $size) { 106 | $this->assertMatchesRegularExpression('|^[0-9]+$|', (string) $size); 107 | } else { 108 | $this->assertNull($size); 109 | } 110 | } 111 | 112 | public function testGetError() 113 | { 114 | if (isset($this->skippedTests[__FUNCTION__])) { 115 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 116 | } 117 | 118 | $file = $this->createSubject(); 119 | $this->assertEquals(UPLOAD_ERR_OK, $file->getError()); 120 | } 121 | 122 | public function testGetClientFilename() 123 | { 124 | if (isset($this->skippedTests[__FUNCTION__])) { 125 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 126 | } 127 | 128 | $file = $this->createSubject(); 129 | $name = $file->getClientFilename(); 130 | if ($name) { 131 | $this->assertTrue(is_string($name)); 132 | } else { 133 | $this->assertNull($name); 134 | } 135 | } 136 | 137 | public function testGetClientMediaType() 138 | { 139 | if (isset($this->skippedTests[__FUNCTION__])) { 140 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 141 | } 142 | 143 | $file = $this->createSubject(); 144 | $type = $file->getClientMediaType(); 145 | if ($type) { 146 | $this->assertTrue(is_string($type)); 147 | } else { 148 | $this->assertNull($type); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/UriIntegrationTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | abstract class UriIntegrationTest extends BaseTest 15 | { 16 | /** 17 | * @var array with functionName => reason 18 | */ 19 | protected $skippedTests = []; 20 | 21 | /** 22 | * @param string $uri 23 | * 24 | * @return UriInterface 25 | */ 26 | abstract public function createUri($uri); 27 | 28 | public function testScheme() 29 | { 30 | if (isset($this->skippedTests[__FUNCTION__])) { 31 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 32 | } 33 | 34 | $uri = $this->createUri('/'); 35 | $this->assertSame('', $uri->getScheme()); 36 | 37 | $uri = $this->createUri('https://foo.com/'); 38 | $this->assertEquals('https', $uri->getScheme()); 39 | 40 | $newUri = $uri->withScheme('http'); 41 | $this->assertNotSameObject($uri, $newUri); 42 | $this->assertEquals('http', $newUri->getScheme()); 43 | } 44 | 45 | /** 46 | * @dataProvider getInvalidSchemaArguments 47 | */ 48 | public function testWithSchemeInvalidArguments($schema) 49 | { 50 | if (isset($this->skippedTests[__FUNCTION__])) { 51 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 52 | } 53 | 54 | try { 55 | $this->createUri('/')->withScheme($schema); 56 | $this->fail('withScheme() should have raised exception on invalid argument'); 57 | } catch (AssertionFailedError $e) { 58 | // invalid argument not caught 59 | throw $e; 60 | } catch (InvalidArgumentException|TypeError $e) { 61 | // valid 62 | $this->assertTrue($e instanceof Throwable); 63 | } catch (Throwable $e) { 64 | // invalid 65 | $this->fail(sprintf( 66 | 'Unexpected exception (%s) thrown from withScheme(); expected TypeError or InvalidArgumentException', 67 | gettype($e) 68 | )); 69 | } 70 | } 71 | 72 | public static function getInvalidSchemaArguments() 73 | { 74 | return [ 75 | [true], 76 | [['foobar']], 77 | [34], 78 | [new \stdClass()], 79 | ]; 80 | } 81 | 82 | public function testAuthority() 83 | { 84 | if (isset($this->skippedTests[__FUNCTION__])) { 85 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 86 | } 87 | 88 | $uri = $this->createUri('/'); 89 | $this->assertEquals('', $uri->getAuthority()); 90 | 91 | $uri = $this->createUri('http://foo@bar.com:80/'); 92 | $this->assertEquals('foo@bar.com', $uri->getAuthority()); 93 | 94 | $uri = $this->createUri('http://foo@bar.com:81/'); 95 | $this->assertEquals('foo@bar.com:81', $uri->getAuthority()); 96 | 97 | $uri = $this->createUri('http://user:foo@bar.com/'); 98 | $this->assertEquals('user:foo@bar.com', $uri->getAuthority()); 99 | 100 | $uri = $this->createUri('http://bar.com:81/'); 101 | $this->assertEquals('bar.com:81', $uri->getAuthority()); 102 | } 103 | 104 | public function testUserInfo() 105 | { 106 | if (isset($this->skippedTests[__FUNCTION__])) { 107 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 108 | } 109 | 110 | $uri = $this->createUri('/'); 111 | $this->assertEquals('', $uri->getUserInfo()); 112 | 113 | $uri = $this->createUri('http://user:foo@bar.com/'); 114 | $this->assertEquals('user:foo', $uri->getUserInfo()); 115 | 116 | $uri = $this->createUri('http://foo@bar.com/'); 117 | $this->assertEquals('foo', $uri->getUserInfo()); 118 | } 119 | 120 | public function testHost() 121 | { 122 | if (isset($this->skippedTests[__FUNCTION__])) { 123 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 124 | } 125 | 126 | $uri = $this->createUri('/'); 127 | $this->assertSame('', $uri->getHost()); 128 | 129 | $uri = $this->createUri('http://www.foo.com/'); 130 | $this->assertEquals('www.foo.com', $uri->getHost()); 131 | 132 | $uri = $this->createUri('http://FOOBAR.COM/'); 133 | $this->assertEquals('foobar.com', $uri->getHost()); 134 | } 135 | 136 | public function testPort() 137 | { 138 | if (isset($this->skippedTests[__FUNCTION__])) { 139 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 140 | } 141 | 142 | $uri = $this->createUri('http://www.foo.com/'); 143 | $this->assertNull($uri->getPort()); 144 | 145 | $uri = $this->createUri('http://www.foo.com:80/'); 146 | $this->assertNull($uri->getPort()); 147 | 148 | $uri = $this->createUri('https://www.foo.com:443/'); 149 | $this->assertNull($uri->getPort()); 150 | 151 | $uri = $this->createUri('http://www.foo.com:81/'); 152 | $this->assertSame(81, $uri->getPort()); 153 | } 154 | 155 | /** 156 | * @dataProvider getPaths 157 | */ 158 | public function testPath(UriInterface $uri, string $expected) 159 | { 160 | if (isset($this->skippedTests[__FUNCTION__])) { 161 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 162 | } 163 | 164 | $this->assertSame($expected, $uri->getPath()); 165 | } 166 | 167 | public static function getPaths() 168 | { 169 | $test = new static('uriprovider'); 170 | 171 | return [ 172 | [$test->createUri('http://www.foo.com/'), '/'], 173 | [$test->createUri('http://www.foo.com'), ''], 174 | [$test->createUri('foo/bar'), 'foo/bar'], 175 | [$test->createUri('http://www.foo.com/foo bar'), '/foo%20bar'], 176 | [$test->createUri('http://www.foo.com/foo%20bar'), '/foo%20bar'], 177 | [$test->createUri('http://www.foo.com/foo%2fbar'), '/foo%2fbar'], 178 | ]; 179 | } 180 | 181 | /** 182 | * @dataProvider getQueries 183 | */ 184 | public function testQuery(UriInterface $uri, string $expected) 185 | { 186 | if (isset($this->skippedTests[__FUNCTION__])) { 187 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 188 | } 189 | 190 | $this->assertSame($expected, $uri->getQuery()); 191 | } 192 | 193 | public static function getQueries() 194 | { 195 | $test = new static('uriprovider'); 196 | 197 | return [ 198 | [$test->createUri('http://www.foo.com'), ''], 199 | [$test->createUri('http://www.foo.com?'), ''], 200 | [$test->createUri('http://www.foo.com?foo=bar'), 'foo=bar'], 201 | [$test->createUri('http://www.foo.com?foo=bar%26baz'), 'foo=bar%26baz'], 202 | [$test->createUri('http://www.foo.com?foo=bar&baz=biz'), 'foo=bar&baz=biz'], 203 | ]; 204 | } 205 | 206 | /** 207 | * @dataProvider getFragments 208 | */ 209 | public function testFragment(UriInterface $uri, string $expected) 210 | { 211 | if (isset($this->skippedTests[__FUNCTION__])) { 212 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 213 | } 214 | 215 | $this->assertEquals($expected, $uri->getFragment()); 216 | } 217 | 218 | public static function getFragments() 219 | { 220 | $test = new static('uriprovider'); 221 | 222 | return [ 223 | [$test->createUri('http://www.foo.com'), ''], 224 | [$test->createUri('http://www.foo.com#'), ''], 225 | [$test->createUri('http://www.foo.com#foo'), 'foo'], 226 | [$test->createUri('http://www.foo.com#foo%20bar'), 'foo%20bar'], 227 | ]; 228 | } 229 | 230 | public function testUriModification1() 231 | { 232 | $expected = 'https://0:0@0:1/0?0#0'; 233 | $uri = $this->createUri($expected); 234 | 235 | $this->assertInstanceOf(UriInterface::class, $uri); 236 | $this->assertSame($expected, (string) $uri); 237 | } 238 | 239 | public function testUriModification2() 240 | { 241 | $expected = 'https://0:0@0:1/0?0#0'; 242 | $uri = $this 243 | ->createUri('') 244 | ->withHost('0') 245 | ->withPort(1) 246 | ->withUserInfo('0', '0') 247 | ->withScheme('https') 248 | ->withPath('/0') 249 | ->withQuery('0') 250 | ->withFragment('0') 251 | ; 252 | 253 | $this->assertInstanceOf(UriInterface::class, $uri); 254 | $this->assertSame($expected, (string) $uri); 255 | } 256 | 257 | public function testPathWithMultipleSlashes() 258 | { 259 | if (isset($this->skippedTests[__FUNCTION__])) { 260 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 261 | } 262 | 263 | $expected = 'http://example.org/valid///path'; 264 | $uri = $this->createUri($expected); 265 | 266 | $this->assertInstanceOf(UriInterface::class, $uri); 267 | $this->assertSame('/valid///path', $uri->getPath()); 268 | $this->assertSame($expected, (string) $uri); 269 | } 270 | 271 | /** 272 | * Tests that getPath() normalizes multiple leading slashes to a single 273 | * slash. This is done to ensure that when a path is used in isolation from 274 | * the authority, it will not cause URL poisoning and/or XSS issues. 275 | * 276 | * @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-3257 277 | * 278 | * @psalm-param array{expected: non-empty-string, uri: UriInterface} $test 279 | */ 280 | public function testGetPathNormalizesMultipleLeadingSlashesToSingleSlashToPreventXSS() 281 | { 282 | if (isset($this->skippedTests[__FUNCTION__])) { 283 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 284 | } 285 | 286 | $expected = 'http://example.org//valid///path'; 287 | $uri = $this->createUri($expected); 288 | 289 | $this->assertInstanceOf(UriInterface::class, $uri); 290 | $this->assertSame('/valid///path', $uri->getPath()); 291 | 292 | return [ 293 | 'expected' => $expected, 294 | 'uri' => $uri, 295 | ]; 296 | } 297 | 298 | /** 299 | * Tests that the full string representation of a URI that includes multiple 300 | * leading slashes in the path is presented verbatim (in contrast to what is 301 | * provided when calling getPath()). 302 | * 303 | * @depends testGetPathNormalizesMultipleLeadingSlashesToSingleSlashToPreventXSS 304 | * 305 | * @psalm-param array{expected: non-empty-string, uri: UriInterface} $test 306 | */ 307 | public function testStringRepresentationWithMultipleSlashes(array $test) 308 | { 309 | $this->assertSame($test['expected'], (string) $test['uri']); 310 | } 311 | 312 | /** 313 | * Tests that special chars in `userInfo` must always be URL-encoded to pass RFC3986 compliant URIs where characters 314 | * in username and password MUST NOT contain reserved characters. 315 | * 316 | * This test is taken from {@see https://github.com/guzzle/psr7/blob/3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf/tests/UriTest.php#L679-L688 guzzlehttp/psr7}. 317 | * 318 | * @see https://www.rfc-editor.org/rfc/rfc3986#appendix-A 319 | */ 320 | public function testSpecialCharsInUserInfo(): void 321 | { 322 | $uri = $this->createUri('/')->withUserInfo('foo@bar.com', 'pass#word'); 323 | self::assertSame('foo%40bar.com:pass%23word', $uri->getUserInfo()); 324 | } 325 | 326 | /** 327 | * Tests that userinfo which is already encoded is not encoded twice. 328 | * This test is taken from {@see https://github.com/guzzle/psr7/blob/3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf/tests/UriTest.php#L679-L688 guzzlehttp/psr7}. 329 | */ 330 | public function testAlreadyEncodedUserInfo(): void 331 | { 332 | $uri = $this->createUri('/')->withUserInfo('foo%40bar.com', 'pass%23word'); 333 | self::assertSame('foo%40bar.com:pass%23word', $uri->getUserInfo()); 334 | } 335 | } 336 | --------------------------------------------------------------------------------