├── .github └── workflows │ └── test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build └── .gitkeep ├── composer.json ├── doc ├── index.md ├── recording.md ├── server.md ├── start.md └── stubbing.md ├── phpunit.xml.dist ├── public └── index.php ├── src ├── Expectation.php ├── Matcher │ ├── AbstractMatcher.php │ ├── ClosureMatcher.php │ ├── ExtractorFactory.php │ ├── MatcherFactory.php │ ├── MatcherInterface.php │ ├── RegexMatcher.php │ └── StringMatcher.php ├── MockBuilder.php ├── PHPUnit │ ├── HttpMockFacade.php │ ├── HttpMockFacadeMap.php │ ├── HttpMockTrait.php │ └── ServerManager.php ├── Request │ └── UnifiedRequest.php ├── RequestCollectionFacade.php ├── RequestStorage.php ├── Response │ └── CallbackResponse.php ├── ResponseBuilder.php ├── Server.php ├── Util.php └── app.php ├── state └── .gitkeep └── tests ├── AppIntegrationTest.php ├── Fixtures └── Request.php ├── Matcher ├── ExtractorFactoryTest.php └── StringMatcherTest.php ├── MockBuilderIntegrationTest.php ├── PHPUnit ├── HttpMockMultiPHPUnitIntegrationTest.php ├── HttpMockPHPUnitIntegrationBasePathTest.php └── HttpMockPHPUnitIntegrationTest.php ├── Request └── UnifiedRequestTest.php └── RequestCollectionFacadeTest.php /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: PHP ${{ matrix.php-version }} (${{ matrix.experimental && 'experimental' || 'full support' }}) 8 | 9 | runs-on: ubuntu-18.04 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php-version: 15 | - 7.1 16 | - 7.2 17 | - 7.3 18 | - 7.4 19 | experimental: [false] 20 | 21 | continue-on-error: ${{ matrix.experimental }} 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Install PHP with extensions 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | php-version: ${{ matrix.php-version }} 30 | coverage: pcov 31 | tools: composer:v2 32 | 33 | - name: Install Composer dependencies 34 | uses: ramsey/composer-install@v1 35 | with: 36 | composer-options: --prefer-dist 37 | continue-on-error: ${{ matrix.experimental }} 38 | 39 | - name: Setup PCOV 40 | run: | 41 | composer require pcov/clobber 42 | vendor/bin/pcov clobber 43 | continue-on-error: true 44 | 45 | - name: Run Tests 46 | run: composer tests 47 | continue-on-error: ${{ matrix.experimental }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .idea/ 4 | state/*-* 5 | build/* 6 | phpunit.xml 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013 - 2017 InterNations GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Mock for PHP 2 | 3 | [![Test](https://github.com/InterNations/http-mock/actions/workflows/test.yaml/badge.svg)](https://github.com/InterNations/http-mock/actions/workflows/test.yaml) 4 | 5 | Mock HTTP requests on the server side in your PHP unit tests. 6 | 7 | HTTP Mock for PHP mocks the server side of an HTTP request to allow integration testing with the HTTP side. 8 | It uses PHP’s builtin web server to start a second process that handles the mocking. The server allows 9 | registering request matcher and responses from the client side. 10 | 11 | *BIG FAT WARNING:* software like this is inherently insecure. Only use in trusted, controlled environments. 12 | 13 | ## Usage 14 | 15 | Read the [docs](doc/index.md) 16 | -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterNations/http-mock/49f5b49d390adc13dc74168d336ceedcb7e0da09/build/.gitkeep -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "internations/http-mock", 3 | "description": "Mock HTTP requests on the server side in your PHP unit tests", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Lars Strojny", 8 | "email": "lars.strojny@internations.org" 9 | }, 10 | { 11 | "name": "Max Beutel", 12 | "email": "max.beutel@internations.org" 13 | } 14 | ], 15 | "require": { 16 | "php": "~7.1", 17 | "silex/silex": "~2.0", 18 | "guzzle/guzzle": ">=3.8", 19 | "symfony/process": "~3|~4|~5", 20 | "jeremeamia/superclosure": "~2", 21 | "lstrojny/hmmmath": ">=0.5.0" 22 | }, 23 | "require-dev": { 24 | "internations/kodierungsregelwerksammlung": "~0.23.0", 25 | "internations/testing-component": "1.0.1", 26 | "phpunit/phpunit": "^7" 27 | }, 28 | "autoload": { 29 | "psr-4": {"InterNations\\Component\\HttpMock\\": "src/"} 30 | }, 31 | "autoload-dev": { 32 | "psr-4": {"InterNations\\Component\\HttpMock\\Tests\\": "tests/"} 33 | }, 34 | "scripts": { 35 | "tests": "phpunit" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | # HTTP mock 2 | 3 | - [Getting started with HTTP Mock and PHPUnit](start.md) 4 | - [The stubbing API explained aka "listening for requests and faking HTTP responses"](stubbing.md) 5 | - [The recording API explain aka "accessing recorded request data for assertions"](recording.md) 6 | -------------------------------------------------------------------------------- /doc/recording.md: -------------------------------------------------------------------------------- 1 | # Recording API 2 | 3 | Once a SUT (system under test) has fired HTTP requests, we often want to validate that our assumption about the nature 4 | of those requests are valid. For that purpose HTTP mock stores every request for later inspection. The recorded requests 5 | are presented as an instance of `InterNations\Component\HttpMock\Request\UnifiedRequest`. 6 | 7 | ```php 8 | $this->http->mock 9 | ->when() 10 | ->methodIs('POST') 11 | ->pathIs('/resource') 12 | ->then() 13 | ->body('body') 14 | ->end(); 15 | $this->http->setUp(); 16 | 17 | // Trigger the SUTs functionality that invokes an HTTP request 18 | $this->sut->executeHttpRequest(); 19 | 20 | $request = $this->http->requests->latest(); 21 | $this->assertSame( 22 | 'application/json', 23 | (string) $request->getHeader('Content-Type'), 24 | 'Client should send application/json' 25 | ); 26 | ``` 27 | 28 | You can access the `first()`, `latest()` or `last()` request, access a specific request with `at(int $position)`, 29 | `pop()` or `shift()` a request from the stack or simply `count($this->http->requests)` to get the number of requests. 30 | -------------------------------------------------------------------------------- /doc/server.md: -------------------------------------------------------------------------------- 1 | ## The server 2 | 3 | Overview of the internal server functionality 4 | 5 | ### Setting up expectation for request recording 6 | ``` 7 | POST /_expectation 8 | { 9 | response (required): serialized Symfony response 10 | matcher (optional): serialized list of closures 11 | limiter (optional): serialized closure that limits the validity of the expectation 12 | } 13 | ``` 14 | 15 | ### Accessing latest recorded request 16 | ``` 17 | GET /_request/latest 18 | Content-Type: text/plain 19 | 20 | RECORDED REQUEST 21 | ``` 22 | 23 | ### Accessing recorded request by index 24 | ``` 25 | GET /_request/{{index}} 26 | Content-Type: text/plain 27 | 28 | RECORDED REQUEST 29 | ``` 30 | 31 | ### Shift recorded request 32 | ``` 33 | GET /_request/shift 34 | Content-Type: text/plain 35 | 36 | RECORDED REQUEST 37 | ``` 38 | 39 | ### Pop recorded request 40 | ``` 41 | GET /_request/pop 42 | Content-Type: text/plain 43 | 44 | RECORDED REQUEST 45 | ``` 46 | 47 | ### Count recorded requests 48 | ``` 49 | GET /_request/count 50 | Content-Type: text/plain 51 | 52 | REQUEST COUNT 53 | ``` 54 | 55 | ### Deleting expectations 56 | ``` 57 | DELETE /_expectation 58 | ``` 59 | 60 | ### Deleting recorded requests 61 | ``` 62 | DELETE /_request 63 | ``` 64 | 65 | ### Deleting everything 66 | ``` 67 | DELETE /_all 68 | ``` 69 | 70 | ### Introspection 71 | ``` 72 | GET /_me 73 | ``` 74 | -------------------------------------------------------------------------------- /doc/start.md: -------------------------------------------------------------------------------- 1 | # Getting started with HTTP mock 2 | 3 | HTTP mock comes out of the box with an integration with [PHPUnit](https://phpunit.de) in the shape of 4 | `InterNations\Component\HttpMock\PHPUnit\HttpMockTrait`. In order to use it, we start and stop the background HTTP 5 | server in `setUpBeforeClass()` and `tearDownAfterClass()` respectively. 6 | 7 | ```php 8 | namespace Acme\Tests; 9 | 10 | use InterNations\Component\HttpMock\PHPUnit\HttpMockTrait; 11 | 12 | class ExampleTest extends PHPUnit_Framework_TestCase 13 | { 14 | use HttpMockTrait; 15 | 16 | public static function setUpBeforeClass() 17 | { 18 | static::setUpHttpMockBeforeClass('8082', 'localhost'); 19 | } 20 | 21 | public static function tearDownAfterClass() 22 | { 23 | static::tearDownHttpMockAfterClass(); 24 | } 25 | 26 | public function setUp() 27 | { 28 | $this->setUpHttpMock(); 29 | } 30 | 31 | public function tearDown() 32 | { 33 | $this->tearDownHttpMock(); 34 | } 35 | 36 | public function testSimpleRequest() 37 | { 38 | $this->http->mock 39 | ->when() 40 | ->methodIs('GET') 41 | ->pathIs('/foo') 42 | ->then() 43 | ->body('mocked body') 44 | ->end(); 45 | $this->http->setUp(); 46 | 47 | $this->assertSame('mocked body', file_get_contents('http://localhost:8082/foo')); 48 | } 49 | 50 | public function testAccessingRecordedRequests() 51 | { 52 | $this->http->mock 53 | ->when() 54 | ->methodIs('POST') 55 | ->pathIs('/foo') 56 | ->then() 57 | ->body('mocked body') 58 | ->end(); 59 | $this->http->setUp(); 60 | 61 | $this->assertSame('mocked body', $this->http->client->post('http://localhost:8082/foo')->send()->getBody(true)); 62 | 63 | $this->assertSame('POST', $this->http->requests->latest()->getMethod()); 64 | $this->assertSame('/foo', $this->http->requests->latest()->getPath()); 65 | } 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /doc/stubbing.md: -------------------------------------------------------------------------------- 1 | # Stubbing API 2 | 3 | When we want to fake HTTP requests, one of the important part is to craft responses a client is supposed to handle. This 4 | process is called stubbing, as Gerard Meszaros explains in 5 | [xUnit patterns](http://xunitpatterns.com/Mocks,%20Fakes,%20Stubs%20and%20Dummies.html). Consequently, 6 | `internations/http-mock` should have been called `internations/http-stub` but it’s too late for that now. 7 | 8 | ## Matching 9 | 10 | What we want to do is tell the fake server: "once you get a request that matches the following criteria, send the 11 | following response". Let’s looks at a simple example: 12 | 13 | ```php 14 | $this->http->mock 15 | ->when() 16 | ->methodIs('GET') 17 | ->pathIs('/resource') 18 | ->then() 19 | ->body('response') 20 | ->end(); 21 | ``` 22 | 23 | The example above say: when we see a `GET` request asking for `/resource` respond with `response`. So far so good. 24 | What we see here is internally syntactic sugar for the following, more verbose, example using plain callbacks. 25 | 26 | ```php 27 | use Symfony\Component\HttpFoundation\Request; 28 | use Symfony\Component\HttpFoundation\Response; 29 | 30 | $this->http->mock 31 | ->when() 32 | ->callback( 33 | static function (Request $request) { 34 | return $request->getMethod() === 'GET' && $request->getPathInfo() === '/resource'; 35 | } 36 | ) 37 | ->then() 38 | ->callback( 39 | static function (Response $response) { 40 | $response->setBody('response'); 41 | } 42 | ) 43 | ->end(); 44 | ``` 45 | 46 | What we can see above is that we use standard Symfony HTTP foundation `Request` and `Response` objects. If you want to 47 | learn more about it, look at 48 | [Symfony’s documentation](https://symfony.com/doc/current/components/http_foundation/introduction.html). 49 | 50 | Let’s have a look what we can do with matching and response building shortcuts: 51 | 52 | ```php 53 | use Symfony\Component\HttpFoundation\Response; 54 | 55 | $this->http->mock 56 | ->when() 57 | ->methodIs('GET') 58 | ->pathIs('/resource') 59 | ->then() 60 | ->statusCode(Response::HTTP_NOT_FOUND) 61 | ->header('X-Custom-Header', 'Header Value') 62 | ->body('response') 63 | ->end(); 64 | ``` 65 | 66 | Additional matching methods are `queryParamExists(string $param)`, `queryParamNotExists(string $param)`, 67 | `queryParamIs(string $param, mixed $value)`, `queryParamsExist(array $params)`, `queryParamsNotExist(array $params)` 68 | and `queryParamsAre(array $paramMap)`. 69 | 70 | If you have more ideas for syntactic sugar, feel free to open a pull requests. 71 | 72 | ## Pattern matching 73 | 74 | For more advanced matching, we can use the matcher API to match against regular expressions and even callbacks. 75 | 76 | ```php 77 | $this->http->mock 78 | ->when() 79 | ->methodIs($this->http->matches->regex('/(GET|POST)/')) 80 | ->pathIs( 81 | $this->http->matches->callback( 82 | function ($path) { 83 | return substr($path, -1) === '/'; 84 | } 85 | ) 86 | ) 87 | ->then() 88 | ->body('response') 89 | ->end(); 90 | ``` 91 | 92 | ## Limiting 93 | 94 | If we need to simulate different responses for the same request based on the position, we can limit the scope of a single response 95 | a single stub. 96 | 97 | ```php 98 | $this->http->mock 99 | ->once() 100 | ->when() 101 | ->methodIs('GET') 102 | ->pathIs('/resource') 103 | ->then() 104 | ->body('response') 105 | ->end(); 106 | ``` 107 | 108 | By using `once()`, the second request will lead to a 404 HTTP Not Found, as if the request would have been undefined. 109 | Other methods to limit validity are `once()`, `twice()`, `thrice()` and `exactly(int $count)`. 110 | 111 | ## Getting different response on successive identical queries 112 | 113 | In the previous section we saw how we could make a stub at most for N queries. But it's also possible to set up different responses on 114 | successive identical queries. 115 | 116 | ```php 117 | $this->builder 118 | ->first() 119 | ->when() 120 | ->pathIs('/resource') 121 | ->methodIs('POST') 122 | ->then() 123 | ->body('called once'); 124 | $this->builder 125 | ->second() 126 | ->when() 127 | ->pathIs('/resource') 128 | ->methodIs('POST') 129 | ->then() 130 | ->body('called twice'); 131 | $this->builder 132 | ->nth(3) 133 | ->when() 134 | ->pathIs('/resource') 135 | ->methodIs('POST') 136 | ->then() 137 | ->body('called 3 times'); 138 | ``` 139 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | run(); 9 | -------------------------------------------------------------------------------- /src/Expectation.php: -------------------------------------------------------------------------------- 1 | matcherFactory = $matcherFactory; 39 | $this->responseBuilder = new ResponseBuilder($mockBuilder); 40 | $this->extractorFactory = $extractorFactory; 41 | $this->limiter = $limiter; 42 | $this->priority = $priority; 43 | } 44 | 45 | public function pathIs($matcher) 46 | { 47 | $this->appendMatcher($matcher, $this->extractorFactory->createPathExtractor()); 48 | 49 | return $this; 50 | } 51 | 52 | public function methodIs($matcher) 53 | { 54 | $this->appendMatcher($matcher, $this->extractorFactory->createMethodExtractor()); 55 | 56 | return $this; 57 | } 58 | 59 | public function queryParamIs($param, $matcher) 60 | { 61 | $this->appendMatcher($matcher, $this->extractorFactory->createParamExtractor($param)); 62 | 63 | return $this; 64 | } 65 | 66 | public function queryParamExists($param) 67 | { 68 | $this->appendMatcher(true, $this->extractorFactory->createParamExistsExtractor($param)); 69 | 70 | return $this; 71 | } 72 | 73 | public function queryParamNotExists($param) 74 | { 75 | $this->appendMatcher(false, $this->extractorFactory->createParamExistsExtractor($param)); 76 | 77 | return $this; 78 | } 79 | 80 | public function queryParamsAre(array $paramMap) 81 | { 82 | foreach ($paramMap as $param => $value) { 83 | $this->queryParamIs($param, $value); 84 | } 85 | 86 | return $this; 87 | } 88 | 89 | public function queryParamsExist(array $params) 90 | { 91 | foreach ($params as $param) { 92 | $this->queryParamExists($param); 93 | } 94 | 95 | return $this; 96 | } 97 | 98 | public function queryParamsNotExist(array $params) 99 | { 100 | foreach ($params as $param) { 101 | $this->queryParamNotExists($param); 102 | } 103 | 104 | return $this; 105 | } 106 | 107 | public function headerIs($name, $value) 108 | { 109 | $this->appendMatcher($value, $this->extractorFactory->createHeaderExtractor($name)); 110 | 111 | return $this; 112 | } 113 | 114 | public function headerExists($name) 115 | { 116 | $this->appendMatcher(true, $this->extractorFactory->createHeaderExistsExtractor($name)); 117 | 118 | return $this; 119 | } 120 | 121 | public function callback(Closure $callback) 122 | { 123 | $this->appendMatcher($this->matcherFactory->closure($callback)); 124 | 125 | return $this; 126 | } 127 | 128 | /** @return SerializableClosure[] */ 129 | public function getMatcherClosures() 130 | { 131 | $closures = []; 132 | 133 | foreach ($this->matcher as $matcher) { 134 | $closures[] = $matcher->getMatcher(); 135 | } 136 | 137 | return $closures; 138 | } 139 | 140 | public function getPriority() 141 | { 142 | return $this->priority; 143 | } 144 | 145 | public function then() 146 | { 147 | return $this->responseBuilder; 148 | } 149 | 150 | public function getResponse() 151 | { 152 | return $this->responseBuilder->getResponse(); 153 | } 154 | 155 | public function getLimiter() 156 | { 157 | return new SerializableClosure($this->limiter); 158 | } 159 | 160 | private function appendMatcher($matcher, Closure $extractor = null) 161 | { 162 | $matcher = $this->createMatcher($matcher); 163 | 164 | if ($extractor) { 165 | $matcher->setExtractor($extractor); 166 | } 167 | 168 | $this->matcher[] = $matcher; 169 | } 170 | 171 | private function createMatcher($matcher) 172 | { 173 | return $matcher instanceof MatcherInterface ? $matcher : $this->matcherFactory->str($matcher); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Matcher/AbstractMatcher.php: -------------------------------------------------------------------------------- 1 | extractor = $extractor; 15 | } 16 | 17 | protected function createExtractor() 18 | { 19 | if (!$this->extractor) { 20 | return static function (Request $request) { 21 | return $request; 22 | }; 23 | } 24 | 25 | return $this->extractor; 26 | } 27 | 28 | abstract protected function createMatcher(); 29 | 30 | public function getMatcher() 31 | { 32 | $matcher = new SerializableClosure($this->createMatcher()); 33 | $extractor = new SerializableClosure($this->createExtractor()); 34 | 35 | return new SerializableClosure( 36 | static function (Request $request) use ($matcher, $extractor) { 37 | return $matcher($extractor($request)); 38 | } 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Matcher/ClosureMatcher.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 13 | } 14 | 15 | protected function createMatcher() 16 | { 17 | return $this->closure; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Matcher/ExtractorFactory.php: -------------------------------------------------------------------------------- 1 | basePath = rtrim($basePath, '/'); 13 | } 14 | 15 | public function createPathExtractor() 16 | { 17 | $basePath = $this->basePath; 18 | 19 | return static function (Request $request) use ($basePath) { 20 | return substr_replace($request->getPathInfo(), '', 0, strlen($basePath)); 21 | }; 22 | } 23 | 24 | public function createMethodExtractor() 25 | { 26 | return static function (Request $request) { 27 | return $request->getMethod(); 28 | }; 29 | } 30 | 31 | public function createParamExtractor($param) 32 | { 33 | return static function (Request $request) use ($param) { 34 | return $request->query->get($param); 35 | }; 36 | } 37 | 38 | public function createParamExistsExtractor($param) 39 | { 40 | return static function (Request $request) use ($param) { 41 | return $request->query->has($param); 42 | }; 43 | } 44 | 45 | public function createHeaderExtractor($header) 46 | { 47 | return static function (Request $request) use ($header) { 48 | return $request->headers->get($header); 49 | }; 50 | } 51 | 52 | public function createHeaderExistsExtractor($header) 53 | { 54 | return static function (Request $request) use ($header) { 55 | return $request->headers->has($header); 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Matcher/MatcherFactory.php: -------------------------------------------------------------------------------- 1 | regex = $regex; 11 | } 12 | 13 | protected function createMatcher() 14 | { 15 | $regex = $this->regex; 16 | 17 | return static function ($value) use ($regex) { 18 | return (bool) preg_match($regex, $value); 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Matcher/StringMatcher.php: -------------------------------------------------------------------------------- 1 | string = $string; 11 | } 12 | 13 | protected function createMatcher() 14 | { 15 | $string = $this->string; 16 | 17 | return static function ($value) use ($string) { 18 | return (string) $string === (string) $value; 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/MockBuilder.php: -------------------------------------------------------------------------------- 1 | matcherFactory = $matcherFactory; 32 | $this->extractorFactory = $extractorFactory; 33 | $this->any(); 34 | } 35 | 36 | public function once() 37 | { 38 | return $this->exactly(1); 39 | } 40 | 41 | public function twice() 42 | { 43 | return $this->exactly(2); 44 | } 45 | 46 | public function thrice() 47 | { 48 | return $this->exactly(3); 49 | } 50 | 51 | public function exactly($times) 52 | { 53 | $this->limiter = static function ($runs) use ($times) { 54 | return $runs < $times; 55 | }; 56 | $this->priority = self::PRIORITY_EXACTLY; 57 | 58 | return $this; 59 | } 60 | 61 | public function first() 62 | { 63 | return $this->nth(1); 64 | } 65 | 66 | public function second() 67 | { 68 | return $this->nth(2); 69 | } 70 | 71 | public function third() 72 | { 73 | return $this->nth(3); 74 | } 75 | 76 | public function nth($position) 77 | { 78 | $this->limiter = static function ($runs) use ($position) { 79 | return $runs === ($position - 1); 80 | }; 81 | $this->priority = $position * self::PRIORITY_NTH; 82 | 83 | return $this; 84 | } 85 | 86 | public function any() 87 | { 88 | $this->limiter = static function () { 89 | return true; 90 | }; 91 | $this->priority = self::PRIORITY_ANY; 92 | 93 | return $this; 94 | } 95 | 96 | /** @return Expectation */ 97 | public function when() 98 | { 99 | $this->expectations[] = new Expectation( 100 | $this, 101 | $this->matcherFactory, 102 | $this->extractorFactory, 103 | $this->limiter, 104 | $this->priority 105 | ); 106 | 107 | $this->any(); 108 | 109 | return end($this->expectations); 110 | } 111 | 112 | public function flushExpectations() 113 | { 114 | $expectations = $this->expectations; 115 | $this->expectations = []; 116 | 117 | usort( 118 | $expectations, 119 | static function (Expectation $left, Expectation $right): int { 120 | return $left->getPriority() <=> $right->getPriority(); 121 | } 122 | ); 123 | 124 | return $expectations; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/PHPUnit/HttpMockFacade.php: -------------------------------------------------------------------------------- 1 | start(); 30 | $this->services['server'] = $server; 31 | $this->basePath = $basePath; 32 | } 33 | 34 | public static function getProperties() 35 | { 36 | return ['server', 'matches', 'mock', 'requests', 'client']; 37 | } 38 | 39 | public function setUp() 40 | { 41 | $this->server->setUp($this->mock->flushExpectations()); 42 | } 43 | 44 | public function __get($property) 45 | { 46 | if (isset($this->services[$property])) { 47 | return $this->services[$property]; 48 | } 49 | 50 | return $this->services[$property] = $this->createService($property); 51 | } 52 | 53 | private function createService($property) 54 | { 55 | switch ($property) { 56 | case 'matches': 57 | return new MatcherFactory(); 58 | break; 59 | 60 | case 'mock': 61 | return new MockBuilder($this->matches, new ExtractorFactory($this->basePath)); 62 | break; 63 | 64 | case 'client': 65 | return $this->server->getClient(); 66 | break; 67 | 68 | case 'requests': 69 | return new RequestCollectionFacade($this->client); 70 | break; 71 | 72 | default: 73 | throw new RuntimeException(sprintf('Invalid property "%s" read', $property)); 74 | break; 75 | } 76 | } 77 | 78 | public function __clone() 79 | { 80 | $this->server->clean(); 81 | } 82 | 83 | public function each(callable $callback) 84 | { 85 | $callback($this); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/PHPUnit/HttpMockFacadeMap.php: -------------------------------------------------------------------------------- 1 | facadeMap = $facadeMap; 17 | } 18 | 19 | public function offsetGet($offset) 20 | { 21 | if (!$this->offsetExists($offset)) { 22 | throw new OutOfBoundsException(sprintf('No named facade "%s" configured', $offset)); 23 | } 24 | 25 | return $this->facadeMap[$offset]; 26 | } 27 | 28 | public function offsetExists($offset) 29 | { 30 | return isset($this->facadeMap[$offset]); 31 | } 32 | 33 | public function offsetSet($offset, $value) 34 | { 35 | throw new BadMethodCallException(__METHOD__); 36 | } 37 | 38 | public function offsetUnset($offset) 39 | { 40 | throw new BadMethodCallException(__METHOD__); 41 | } 42 | 43 | public function __clone() 44 | { 45 | $this->facadeMap = array_map( 46 | static function (HttpMockFacade $facade) { 47 | return clone $facade; 48 | }, 49 | $this->facadeMap 50 | ); 51 | } 52 | 53 | public function each(callable $callback) 54 | { 55 | array_map($callback, $this->facadeMap); 56 | } 57 | 58 | public function __get($property) 59 | { 60 | if (in_array($property, HttpMockFacade::getProperties(), true)) { 61 | throw new OutOfBoundsException( 62 | sprintf( 63 | 'Tried to access facade property "%1$s" on facade map. First select one of the facades from ' 64 | . 'the map. Defined facades: "%2$s", try $this->http[\'%s\']->%1$s->…', 65 | $property, 66 | implode('", "', array_keys($this->facadeMap)), 67 | current(array_keys($this->facadeMap)) 68 | ) 69 | ); 70 | } 71 | 72 | throw new OutOfBoundsException( 73 | sprintf( 74 | 'Tried to access property "%1$s". This is a map of facades, try $this->http[\'%1$s\'] instead.', 75 | $property 76 | ) 77 | ); 78 | } 79 | 80 | public function all() 81 | { 82 | return $this->facadeMap; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/PHPUnit/HttpMockTrait.php: -------------------------------------------------------------------------------- 1 | $facade] + static::$staticHttp->all()); 33 | } else { 34 | static::$staticHttp = new HttpMockFacadeMap([$name => $facade]); 35 | } 36 | 37 | ServerManager::getInstance()->add($facade->server); 38 | } 39 | 40 | protected function setUpHttpMock() 41 | { 42 | static::assertHttpMockSetup(); 43 | 44 | $this->http = clone static::$staticHttp; 45 | } 46 | 47 | protected static function assertHttpMockSetup() 48 | { 49 | if (!static::$staticHttp) { 50 | static::fail( 51 | sprintf( 52 | 'Static HTTP mock facade not present. Did you forget to invoke static::setUpHttpMockBeforeClass()' 53 | . ' in %s::setUpBeforeClass()?', 54 | get_called_class() 55 | ) 56 | ); 57 | } 58 | } 59 | 60 | protected function tearDownHttpMock() 61 | { 62 | if (!$this->http) { 63 | return; 64 | } 65 | 66 | $http = $this->http; 67 | $this->http = null; 68 | $http->each( 69 | function (HttpMockFacade $facade) { 70 | $this->assertSame( 71 | '', 72 | (string) $facade->server->getIncrementalErrorOutput(), 73 | 'HTTP mock server standard error output should be empty' 74 | ); 75 | } 76 | ); 77 | } 78 | 79 | protected static function tearDownHttpMockAfterClass() 80 | { 81 | static::$staticHttp->each( 82 | static function (HttpMockFacade $facade) { 83 | $facade->server->stop(); 84 | ServerManager::getInstance()->remove($facade->server); 85 | } 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/PHPUnit/ServerManager.php: -------------------------------------------------------------------------------- 1 | servers->attach($server); 31 | } 32 | 33 | public function remove(Server $server) 34 | { 35 | $this->servers->detach($server); 36 | } 37 | 38 | public function cleanup() 39 | { 40 | foreach ($this->servers as $server) { 41 | $server->stop(); 42 | } 43 | } 44 | 45 | private function __construct() 46 | { 47 | $this->servers = new SplObjectStorage(); 48 | register_shutdown_function([$this, 'cleanup']); 49 | } 50 | 51 | private function __clone() 52 | { 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Request/UnifiedRequest.php: -------------------------------------------------------------------------------- 1 | wrapped = $wrapped; 29 | $this->init($params); 30 | } 31 | 32 | /** 33 | * Get the user agent of the request 34 | * 35 | * @return string 36 | */ 37 | public function getUserAgent() 38 | { 39 | return $this->userAgent; 40 | } 41 | 42 | /** 43 | * Get the body of the request if set 44 | * 45 | * @return EntityBodyInterface|null 46 | */ 47 | public function getBody() 48 | { 49 | return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); 50 | } 51 | 52 | /** 53 | * Get a POST field from the request 54 | * 55 | * @param string $field Field to retrieve 56 | * 57 | * @return mixed|null 58 | */ 59 | public function getPostField($field) 60 | { 61 | return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); 62 | } 63 | 64 | /** 65 | * Get the post fields that will be used in the request 66 | * 67 | * @return QueryString 68 | */ 69 | public function getPostFields() 70 | { 71 | return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); 72 | } 73 | 74 | /** 75 | * Returns an associative array of POST field names to PostFileInterface objects 76 | * 77 | * @return array 78 | */ 79 | public function getPostFiles() 80 | { 81 | return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); 82 | } 83 | 84 | /** 85 | * Get a POST file from the request 86 | * 87 | * @param string $fieldName POST fields to retrieve 88 | * 89 | * @return array|null Returns an array wrapping an array of PostFileInterface objects 90 | */ 91 | public function getPostFile($fieldName) 92 | { 93 | return $this->invokeWrappedIfEntityEnclosed(__FUNCTION__, func_get_args()); 94 | } 95 | 96 | /** 97 | * Get application and plugin specific parameters set on the message. 98 | * 99 | * @return Collection 100 | */ 101 | public function getParams() 102 | { 103 | return $this->wrapped->getParams(); 104 | } 105 | 106 | /** 107 | * Retrieve an HTTP header by name. Performs a case-insensitive search of all headers. 108 | * 109 | * @param string $header Header to retrieve. 110 | * 111 | * @return Header|null Returns NULL if no matching header is found. 112 | * Returns a Header object if found. 113 | */ 114 | public function getHeader($header) 115 | { 116 | return $this->wrapped->getHeader($header); 117 | } 118 | 119 | /** 120 | * Get all headers as a collection 121 | * 122 | * @return HeaderCollection 123 | */ 124 | public function getHeaders() 125 | { 126 | return $this->wrapped->getHeaders(); 127 | } 128 | 129 | /** 130 | * Get an array of message header lines 131 | * 132 | * @return array 133 | */ 134 | public function getHeaderLines() 135 | { 136 | return $this->wrapped->getHeaderLines(); 137 | } 138 | 139 | /** 140 | * Check if the specified header is present. 141 | * 142 | * @param string $header The header to check. 143 | * 144 | * @return bool Returns TRUE or FALSE if the header is present 145 | */ 146 | public function hasHeader($header) 147 | { 148 | return $this->wrapped->hasHeader($header); 149 | } 150 | 151 | /** 152 | * Get the raw message headers as a string 153 | * 154 | * @return string 155 | */ 156 | public function getRawHeaders() 157 | { 158 | return $this->wrapped->getRawHeaders(); 159 | } 160 | 161 | /** 162 | * Get the collection of key value pairs that will be used as the query 163 | * string in the request 164 | * 165 | * @return QueryString 166 | */ 167 | public function getQuery() 168 | { 169 | return $this->wrapped->getQuery(); 170 | } 171 | 172 | /** 173 | * Get the HTTP method of the request 174 | * 175 | * @return string 176 | */ 177 | public function getMethod() 178 | { 179 | return $this->wrapped->getMethod(); 180 | } 181 | 182 | /** 183 | * Get the URI scheme of the request (http, https, ftp, etc) 184 | * 185 | * @return string 186 | */ 187 | public function getScheme() 188 | { 189 | return $this->wrapped->getScheme(); 190 | } 191 | 192 | /** 193 | * Get the host of the request 194 | * 195 | * @return string 196 | */ 197 | public function getHost() 198 | { 199 | return $this->wrapped->getHost(); 200 | } 201 | 202 | /** 203 | * Get the HTTP protocol version of the request 204 | * 205 | * @return string 206 | */ 207 | public function getProtocolVersion() 208 | { 209 | return $this->wrapped->getProtocolVersion(); 210 | } 211 | 212 | /** 213 | * Get the path of the request (e.g. '/', '/index.html') 214 | * 215 | * @return string 216 | */ 217 | public function getPath() 218 | { 219 | return $this->wrapped->getPath(); 220 | } 221 | 222 | /** 223 | * Get the port that the request will be sent on if it has been set 224 | * 225 | * @return int|null 226 | */ 227 | public function getPort() 228 | { 229 | return $this->wrapped->getPort(); 230 | } 231 | 232 | /** 233 | * Get the username to pass in the URL if set 234 | * 235 | * @return string|null 236 | */ 237 | public function getUsername() 238 | { 239 | return $this->wrapped->getUsername(); 240 | } 241 | 242 | /** 243 | * Get the password to pass in the URL if set 244 | * 245 | * @return string|null 246 | */ 247 | public function getPassword() 248 | { 249 | return $this->wrapped->getPassword(); 250 | } 251 | 252 | /** 253 | * Get the full URL of the request (e.g. 'http://www.guzzle-project.com/') 254 | * scheme://username:password@domain:port/path?query_string#fragment 255 | * 256 | * @param bool $asObject Set to TRUE to retrieve the URL as a clone of the URL object owned by the request. 257 | * 258 | * @return string|Url 259 | */ 260 | public function getUrl($asObject = false) 261 | { 262 | return $this->wrapped->getUrl($asObject); 263 | } 264 | 265 | /** 266 | * Get an array of Cookies 267 | * 268 | * @return array 269 | */ 270 | public function getCookies() 271 | { 272 | return $this->wrapped->getCookies(); 273 | } 274 | 275 | /** 276 | * Get a cookie value by name 277 | * 278 | * @param string $name Cookie to retrieve 279 | * 280 | * @return null|string 281 | */ 282 | public function getCookie($name) 283 | { 284 | return $this->wrapped->getCookie($name); 285 | } 286 | 287 | protected function invokeWrappedIfEntityEnclosed($method, array $params = []) 288 | { 289 | if (!$this->wrapped instanceof EntityEnclosingRequestInterface) { 290 | throw new BadMethodCallException( 291 | sprintf( 292 | 'Cannot call method "%s" on a request that does not enclose an entity.' 293 | . ' Did you expect a POST/PUT request instead of %s %s?', 294 | $method, 295 | $this->wrapped->getMethod(), 296 | $this->wrapped->getPath() 297 | ) 298 | ); 299 | } 300 | 301 | return call_user_func_array([$this->wrapped, $method], $params); 302 | } 303 | 304 | private function init(array $params) 305 | { 306 | foreach ($params as $property => $value) { 307 | if (property_exists($this, $property)) { 308 | $this->{$property} = $value; 309 | } 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/RequestCollectionFacade.php: -------------------------------------------------------------------------------- 1 | client = $client; 20 | } 21 | 22 | /** 23 | * @return UnifiedRequest 24 | */ 25 | public function latest() 26 | { 27 | return $this->getRecordedRequest('/_request/last'); 28 | } 29 | 30 | /** 31 | * @return UnifiedRequest 32 | */ 33 | public function last() 34 | { 35 | return $this->getRecordedRequest('/_request/last'); 36 | } 37 | 38 | /** 39 | * @return UnifiedRequest 40 | */ 41 | public function first() 42 | { 43 | return $this->getRecordedRequest('/_request/first'); 44 | } 45 | 46 | /** 47 | * @param int $position 48 | * @return UnifiedRequest 49 | */ 50 | public function at($position) 51 | { 52 | return $this->getRecordedRequest('/_request/' . $position); 53 | } 54 | 55 | /** 56 | * @return UnifiedRequest 57 | */ 58 | public function pop() 59 | { 60 | return $this->deleteRecordedRequest('/_request/last'); 61 | } 62 | 63 | /** 64 | * @return UnifiedRequest 65 | */ 66 | public function shift() 67 | { 68 | return $this->deleteRecordedRequest('/_request/first'); 69 | } 70 | 71 | public function count() 72 | { 73 | $response = $this->client 74 | ->get('/_request/count') 75 | ->send(); 76 | 77 | return (int) $response->getBody(true); 78 | } 79 | 80 | /** 81 | * @param Response $response 82 | * @param string $path 83 | * @throws UnexpectedValueException 84 | * @return UnifiedRequest 85 | */ 86 | private function parseRequestFromResponse(Response $response, $path) 87 | { 88 | try { 89 | $requestInfo = Util::deserialize($response->getBody()); 90 | } catch (UnexpectedValueException $e) { 91 | throw new UnexpectedValueException( 92 | sprintf('Cannot deserialize response from "%s": "%s"', $path, $response->getBody()), 93 | null, 94 | $e 95 | ); 96 | } 97 | 98 | $request = RequestFactory::getInstance()->fromMessage($requestInfo['request']); 99 | $params = $this->configureRequest( 100 | $request, 101 | $requestInfo['server'], 102 | isset($requestInfo['enclosure']) ? $requestInfo['enclosure'] : [] 103 | ); 104 | 105 | return new UnifiedRequest($request, $params); 106 | } 107 | 108 | private function configureRequest(RequestInterface $request, array $server, array $enclosure) 109 | { 110 | if (isset($server['HTTP_HOST'])) { 111 | $request->setHost($server['HTTP_HOST']); 112 | } 113 | 114 | if (isset($server['HTTP_PORT'])) { 115 | $request->setPort($server['HTTP_PORT']); 116 | } 117 | 118 | if (isset($server['PHP_AUTH_USER'])) { 119 | $request->setAuth($server['PHP_AUTH_USER'], isset($server['PHP_AUTH_PW']) ? $server['PHP_AUTH_PW'] : null); 120 | } 121 | 122 | $params = []; 123 | 124 | if (isset($server['HTTP_USER_AGENT'])) { 125 | $params['userAgent'] = $server['HTTP_USER_AGENT']; 126 | } 127 | 128 | if ($request instanceof EntityEnclosingRequestInterface) { 129 | $request->addPostFields($enclosure); 130 | } 131 | 132 | return $params; 133 | } 134 | 135 | private function getRecordedRequest($path) 136 | { 137 | $response = $this->client 138 | ->get($path) 139 | ->send(); 140 | 141 | return $this->parseResponse($response, $path); 142 | } 143 | 144 | private function deleteRecordedRequest($path) 145 | { 146 | $response = $this->client 147 | ->delete($path) 148 | ->send(); 149 | 150 | return $this->parseResponse($response, $path); 151 | } 152 | 153 | private function parseResponse(Response $response, $path) 154 | { 155 | $statusCode = $response->getStatusCode(); 156 | 157 | if ($statusCode !== 200) { 158 | throw new UnexpectedValueException( 159 | sprintf('Expected status code 200 from "%s", got %d', $path, $statusCode) 160 | ); 161 | } 162 | 163 | $contentType = $response->hasHeader('content-type') 164 | ? $response->getContentType() 165 | : ''; 166 | 167 | if (substr($contentType, 0, 10) !== 'text/plain') { 168 | throw new UnexpectedValueException( 169 | sprintf('Expected content type "text/plain" from "%s", got "%s"', $path, $contentType) 170 | ); 171 | } 172 | 173 | return $this->parseRequestFromResponse($response, $path); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/RequestStorage.php: -------------------------------------------------------------------------------- 1 | pid = $pid; 15 | $this->directory = $directory; 16 | } 17 | 18 | public function store(Request $request, $name, $data) 19 | { 20 | file_put_contents($this->getFileName($request, $name), serialize($data)); 21 | } 22 | 23 | public function read(Request $request, $name) 24 | { 25 | $fileName = $this->getFileName($request, $name); 26 | 27 | if (!file_exists($fileName)) { 28 | return []; 29 | } 30 | 31 | return Util::deserialize(file_get_contents($fileName)); 32 | } 33 | 34 | public function append(Request $request, $name, $data) 35 | { 36 | $list = $this->read($request, $name); 37 | $list[] = $data; 38 | $this->store($request, $name, $list); 39 | } 40 | 41 | public function prepend(Request $request, $name, $data) 42 | { 43 | $list = $this->read($request, $name); 44 | array_unshift($list, $data); 45 | $this->store($request, $name, $list); 46 | } 47 | 48 | private function getFileName(Request $request, $name) 49 | { 50 | return $this->directory . $this->pid . '-' . $name . '-' . $request->server->get('SERVER_PORT'); 51 | } 52 | 53 | public function clear(Request $request, $name) 54 | { 55 | $fileName = $this->getFileName($request, $name); 56 | 57 | if (file_exists($fileName)) { 58 | unlink($fileName); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Response/CallbackResponse.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 13 | } 14 | 15 | public function sendCallback() 16 | { 17 | if ($this->callback) { 18 | $callback = $this->callback; 19 | $callback($this); 20 | } 21 | } 22 | 23 | public function send() 24 | { 25 | $this->sendCallback(); 26 | parent::send(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ResponseBuilder.php: -------------------------------------------------------------------------------- 1 | mockBuilder = $mockBuilder; 19 | $this->response = new CallbackResponse(); 20 | } 21 | 22 | public function statusCode($statusCode) 23 | { 24 | $this->response->setStatusCode($statusCode); 25 | 26 | return $this; 27 | } 28 | 29 | public function body($body) 30 | { 31 | $this->response->setContent($body); 32 | 33 | return $this; 34 | } 35 | 36 | public function callback(Closure $callback) 37 | { 38 | $this->response->setCallback(new SerializableClosure($callback)); 39 | 40 | return $this; 41 | } 42 | 43 | public function header($header, $value) 44 | { 45 | $this->response->headers->set($header, $value); 46 | 47 | return $this; 48 | } 49 | 50 | public function end() 51 | { 52 | return $this->mockBuilder; 53 | } 54 | 55 | public function getResponse() 56 | { 57 | return $this->response; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | port = $port; 22 | $this->host = $host; 23 | $packageRoot = __DIR__ . '/../'; 24 | $command = [ 25 | 'php', 26 | '-dalways_populate_raw_post_data=-1', 27 | '-derror_log=', 28 | '-S=' . $this->getConnectionString(), 29 | '-t=public/', 30 | $packageRoot . 'public/index.php', 31 | ]; 32 | 33 | parent::__construct($command, $packageRoot); 34 | $this->setTimeout(null); 35 | } 36 | 37 | public function start(callable $callback = null, array $env = []) 38 | { 39 | parent::start($callback, $env); 40 | 41 | $this->pollWait(); 42 | } 43 | 44 | public function stop($timeout = 10, $signal = null) 45 | { 46 | return parent::stop($timeout, $signal); 47 | } 48 | 49 | public function getClient() 50 | { 51 | return $this->client ?: $this->client = $this->createClient(); 52 | } 53 | 54 | private function createClient() 55 | { 56 | $client = new Client($this->getBaseUrl()); 57 | $client->getEventDispatcher()->addListener( 58 | 'request.error', 59 | static function (Event $event) { 60 | $event->stopPropagation(); 61 | } 62 | ); 63 | 64 | return $client; 65 | } 66 | 67 | public function getBaseUrl() 68 | { 69 | return sprintf('http://%s', $this->getConnectionString()); 70 | } 71 | 72 | public function getConnectionString() 73 | { 74 | return sprintf('%s:%d', $this->host, $this->port); 75 | } 76 | 77 | /** 78 | * @param Expectation[] $expectations 79 | * @throws RuntimeException 80 | */ 81 | public function setUp(array $expectations) 82 | { 83 | /** @var Expectation $expectation */ 84 | foreach ($expectations as $expectation) { 85 | $response = $this->getClient()->post( 86 | '/_expectation', 87 | null, 88 | [ 89 | 'matcher' => serialize($expectation->getMatcherClosures()), 90 | 'limiter' => serialize($expectation->getLimiter()), 91 | 'response' => serialize($expectation->getResponse()), 92 | ] 93 | )->send(); 94 | 95 | if ($response->getStatusCode() !== 201) { 96 | throw new RuntimeException('Could not set up expectations'); 97 | } 98 | } 99 | } 100 | 101 | public function clean() 102 | { 103 | if (!$this->isRunning()) { 104 | $this->start(); 105 | } 106 | 107 | $this->getClient()->delete('/_all')->send(); 108 | } 109 | 110 | private function pollWait() 111 | { 112 | foreach (FibonacciFactory::sequence(50000, 10000) as $sleepTime) { 113 | try { 114 | usleep($sleepTime); 115 | $this->getClient()->head('/_me')->send(); 116 | break; 117 | } catch (CurlException $e) { 118 | continue; 119 | } 120 | } 121 | } 122 | 123 | public function getIncrementalErrorOutput() 124 | { 125 | return self::cleanErrorOutput(parent::getIncrementalErrorOutput()); 126 | } 127 | 128 | public function getErrorOutput() 129 | { 130 | return self::cleanErrorOutput(parent::getErrorOutput()); 131 | } 132 | 133 | private static function cleanErrorOutput($output) 134 | { 135 | if (!trim($output)) { 136 | return ''; 137 | } 138 | 139 | $errorLines = []; 140 | 141 | foreach (explode(PHP_EOL, $output) as $line) { 142 | if (!$line) { 143 | continue; 144 | } 145 | 146 | if (!self::stringEndsWithAny($line, ['Accepted', 'Closing', ' started'])) { 147 | $errorLines[] = $line; 148 | } 149 | } 150 | 151 | return $errorLines ? implode(PHP_EOL, $errorLines) : ''; 152 | } 153 | 154 | private static function stringEndsWithAny($haystack, array $needles) 155 | { 156 | foreach ($needles as $needle) { 157 | if (substr($haystack, (-1 * strlen($needle))) === $needle) { 158 | return true; 159 | } 160 | } 161 | 162 | return false; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | delete( 38 | '/_expectation', 39 | static function (Request $request) use ($app) { 40 | $app['storage']->clear($request, 'expectations'); 41 | 42 | return new Response('', Response::HTTP_OK); 43 | } 44 | ); 45 | 46 | $app->post( 47 | '/_expectation', 48 | static function (Request $request) use ($app) { 49 | 50 | $matcher = []; 51 | 52 | if ($request->request->has('matcher')) { 53 | $matcher = Util::silentDeserialize($request->request->get('matcher')); 54 | $validator = static function ($closure) { 55 | return is_callable($closure); 56 | }; 57 | 58 | if (!is_array($matcher) || count(array_filter($matcher, $validator)) !== count($matcher)) { 59 | return new Response( 60 | 'POST data key "matcher" must be a serialized list of closures', 61 | Response::HTTP_EXPECTATION_FAILED 62 | ); 63 | } 64 | } 65 | 66 | if (!$request->request->has('response')) { 67 | return new Response('POST data key "response" not found in POST data', Response::HTTP_EXPECTATION_FAILED); 68 | } 69 | 70 | $response = Util::silentDeserialize($request->request->get('response')); 71 | 72 | if (!$response instanceof Response) { 73 | return new Response( 74 | 'POST data key "response" must be a serialized Symfony response', 75 | Response::HTTP_EXPECTATION_FAILED 76 | ); 77 | } 78 | 79 | $limiter = null; 80 | 81 | if ($request->request->has('limiter')) { 82 | $limiter = Util::silentDeserialize($request->request->get('limiter')); 83 | 84 | if (!is_callable($limiter)) { 85 | return new Response( 86 | 'POST data key "limiter" must be a serialized closure', 87 | Response::HTTP_EXPECTATION_FAILED 88 | ); 89 | } 90 | } 91 | 92 | // Fix issue with silex default error handling 93 | $response->headers->set('X-Status-Code', $response->getStatusCode()); 94 | 95 | $app['storage']->prepend( 96 | $request, 97 | 'expectations', 98 | ['matcher' => $matcher, 'response' => $response, 'limiter' => $limiter, 'runs' => 0] 99 | ); 100 | 101 | return new Response('', Response::HTTP_CREATED); 102 | } 103 | ); 104 | 105 | $app->error( 106 | static function (Exception $e, Request $request, $code, GetResponseForExceptionEvent $event = null) use ($app) { 107 | if ($e instanceof NotFoundHttpException) { 108 | if (method_exists($event, 'allowCustomResponseCode')) { 109 | $event->allowCustomResponseCode(); 110 | } 111 | 112 | $app['storage']->append( 113 | $request, 114 | 'requests', 115 | serialize( 116 | [ 117 | 'server' => $request->server->all(), 118 | 'request' => (string) $request, 119 | 'enclosure' => $request->request->all(), 120 | ] 121 | ) 122 | ); 123 | 124 | $notFoundResponse = new Response('No matching expectation found', Response::HTTP_NOT_FOUND); 125 | 126 | $expectations = $app['storage']->read($request, 'expectations'); 127 | 128 | foreach ($expectations as $pos => $expectation) { 129 | foreach ($expectation['matcher'] as $matcher) { 130 | if (!$matcher($request)) { 131 | continue 2; 132 | } 133 | } 134 | 135 | $applicable = !isset($expectation['limiter']) || $expectation['limiter']($expectation['runs']); 136 | 137 | ++$expectations[$pos]['runs']; 138 | $app['storage']->store($request, 'expectations', $expectations); 139 | 140 | if (!$applicable) { 141 | $notFoundResponse = new Response('Expectation not met', Response::HTTP_GONE); 142 | continue; 143 | } 144 | 145 | return $expectation['response']; 146 | } 147 | 148 | return $notFoundResponse; 149 | } 150 | 151 | return new Response('Server error: ' . $e->getMessage(), $code); 152 | } 153 | ); 154 | 155 | $app->get( 156 | '/_request/count', 157 | static function (Request $request) use ($app) { 158 | return count($app['storage']->read($request, 'requests')); 159 | } 160 | ); 161 | 162 | $app->get( 163 | '/_request/{index}', 164 | static function (Request $request, $index) use ($app) { 165 | $requestData = $app['storage']->read($request, 'requests'); 166 | 167 | if (!isset($requestData[$index])) { 168 | return new Response('Index ' . $index . ' not found', Response::HTTP_NOT_FOUND); 169 | } 170 | 171 | return new Response($requestData[$index], Response::HTTP_OK, ['Content-Type' => 'text/plain']); 172 | } 173 | )->assert('index', '\d+'); 174 | 175 | $app->delete( 176 | '/_request/{action}', 177 | static function (Request $request, $action) use ($app) { 178 | $requestData = $app['storage']->read($request, 'requests'); 179 | $fn = 'array_' . ($action === 'last' ? 'pop' : 'shift'); 180 | $requestString = $fn($requestData); 181 | $app['storage']->store($request, 'requests', $requestData); 182 | 183 | if (!$requestString) { 184 | return new Response($action . ' not possible', Response::HTTP_NOT_FOUND); 185 | } 186 | 187 | return new Response($requestString, Response::HTTP_OK, ['Content-Type' => 'text/plain']); 188 | } 189 | )->assert('index', '(last|first)'); 190 | 191 | $app->get( 192 | '/_request/{action}', 193 | static function (Request $request, $action) use ($app) { 194 | $requestData = $app['storage']->read($request, 'requests'); 195 | $fn = 'array_' . ($action === 'last' ? 'pop' : 'shift'); 196 | $requestString = $fn($requestData); 197 | 198 | if (!$requestString) { 199 | return new Response($action . ' not available', Response::HTTP_NOT_FOUND); 200 | } 201 | 202 | return new Response($requestString, Response::HTTP_OK, ['Content-Type' => 'text/plain']); 203 | } 204 | )->assert('index', '(last|first)'); 205 | 206 | $app->delete( 207 | '/_request', 208 | static function (Request $request) use ($app) { 209 | $app['storage']->store($request, 'requests', []); 210 | 211 | return new Response('', Response::HTTP_OK); 212 | } 213 | ); 214 | 215 | $app->delete( 216 | '/_all', 217 | static function (Request $request) use ($app) { 218 | $app['storage']->store($request, 'requests', []); 219 | $app['storage']->store($request, 'expectations', []); 220 | 221 | return new Response('', Response::HTTP_OK); 222 | } 223 | ); 224 | 225 | $app->get( 226 | '/_me', 227 | static function () { 228 | return new Response('O RLY?', Response::HTTP_I_AM_A_TEAPOT, ['Content-Type' => 'text/plain']); 229 | } 230 | ); 231 | 232 | return $app; 233 | -------------------------------------------------------------------------------- /state/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterNations/http-mock/49f5b49d390adc13dc74168d336ceedcb7e0da09/state/.gitkeep -------------------------------------------------------------------------------- /tests/AppIntegrationTest.php: -------------------------------------------------------------------------------- 1 | start(); 35 | } 36 | 37 | public static function tearDownAfterClass() 38 | { 39 | static::assertSame('', (string) static::$server1->getOutput(), (string) static::$server1->getOutput()); 40 | static::assertSame('', (string) static::$server1->getErrorOutput(), (string) static::$server1->getErrorOutput()); 41 | static::$server1->stop(); 42 | } 43 | 44 | public function setUp() 45 | { 46 | static::$server1->clean(); 47 | $this->client = static::$server1->getClient(); 48 | } 49 | 50 | public function testSimpleUseCase() 51 | { 52 | $response = $this->client->post( 53 | '/_expectation', 54 | null, 55 | $this->createExpectationParams( 56 | [ 57 | static function ($request) { 58 | return $request instanceof Request; 59 | } 60 | ], 61 | new Response('fake body', 200) 62 | ) 63 | )->send(); 64 | $this->assertSame('', (string) $response->getBody()); 65 | $this->assertSame(201, $response->getStatusCode()); 66 | 67 | $response = $this->client->post('/foobar', ['X-Special' => 1], ['post' => 'data'])->send(); 68 | 69 | $this->assertSame(200, $response->getStatusCode()); 70 | $this->assertSame('fake body', (string) $response->getBody()); 71 | 72 | $response = $this->client->get('/_request/latest')->send(); 73 | 74 | /** @var EntityEnclosingRequest $request */ 75 | $request = $this->parseRequestFromResponse($response); 76 | $this->assertSame('1', (string) $request->getHeader('X-Special')); 77 | $this->assertSame('post=data', (string) $request->getBody()); 78 | } 79 | 80 | public function testRecording() 81 | { 82 | $this->client->delete('/_all')->send(); 83 | 84 | $this->assertSame(404, $this->client->get('/_request/latest')->send()->getStatusCode()); 85 | $this->assertSame(404, $this->client->get('/_request/0')->send()->getStatusCode()); 86 | $this->assertSame(404, $this->client->get('/_request/first')->send()->getStatusCode()); 87 | $this->assertSame(404, $this->client->get('/_request/last')->send()->getStatusCode()); 88 | 89 | $this->client->get('/req/0')->send(); 90 | $this->client->get('/req/1')->send(); 91 | $this->client->get('/req/2')->send(); 92 | $this->client->get('/req/3')->send(); 93 | 94 | $this->assertSame( 95 | '/req/3', 96 | $this->parseRequestFromResponse($this->client->get('/_request/last')->send())->getPath() 97 | ); 98 | $this->assertSame( 99 | '/req/0', 100 | $this->parseRequestFromResponse($this->client->get('/_request/0')->send())->getPath() 101 | ); 102 | $this->assertSame( 103 | '/req/1', 104 | $this->parseRequestFromResponse($this->client->get('/_request/1')->send())->getPath() 105 | ); 106 | $this->assertSame( 107 | '/req/2', 108 | $this->parseRequestFromResponse($this->client->get('/_request/2')->send())->getPath() 109 | ); 110 | $this->assertSame( 111 | '/req/3', 112 | $this->parseRequestFromResponse($this->client->get('/_request/3')->send())->getPath() 113 | ); 114 | $this->assertSame(404, $this->client->get('/_request/4')->send()->getStatusCode()); 115 | 116 | $this->assertSame( 117 | '/req/3', 118 | $this->parseRequestFromResponse($this->client->delete('/_request/last')->send())->getPath() 119 | ); 120 | $this->assertSame( 121 | '/req/0', 122 | $this->parseRequestFromResponse($this->client->delete('/_request/first')->send())->getPath() 123 | ); 124 | $this->assertSame( 125 | '/req/1', 126 | $this->parseRequestFromResponse($this->client->get('/_request/0')->send())->getPath() 127 | ); 128 | $this->assertSame( 129 | '/req/2', 130 | $this->parseRequestFromResponse($this->client->get('/_request/1')->send())->getPath() 131 | ); 132 | $this->assertSame(404, $this->client->get('/_request/2')->send()->getStatusCode()); 133 | } 134 | 135 | public function testErrorHandling() 136 | { 137 | $this->client->delete('/_all')->send(); 138 | 139 | $response = $this->client->post('/_expectation', null, ['matcher' => ''])->send(); 140 | $this->assertSame(417, $response->getStatusCode()); 141 | $this->assertSame('POST data key "matcher" must be a serialized list of closures', (string) $response->getBody()); 142 | 143 | $response = $this->client->post('/_expectation', null, ['matcher' => ['foo']])->send(); 144 | $this->assertSame(417, $response->getStatusCode()); 145 | $this->assertSame('POST data key "matcher" must be a serialized list of closures', (string) $response->getBody()); 146 | 147 | $response = $this->client->post('/_expectation', null, [])->send(); 148 | $this->assertSame(417, $response->getStatusCode()); 149 | $this->assertSame('POST data key "response" not found in POST data', (string) $response->getBody()); 150 | 151 | $response = $this->client->post('/_expectation', null, ['response' => ''])->send(); 152 | $this->assertSame(417, $response->getStatusCode()); 153 | $this->assertSame('POST data key "response" must be a serialized Symfony response', (string) $response->getBody()); 154 | 155 | $response = $this->client->post('/_expectation', null, ['response' => serialize(new Response()), 'limiter' => 'foo'])->send(); 156 | $this->assertSame(417, $response->getStatusCode()); 157 | $this->assertSame('POST data key "limiter" must be a serialized closure', (string) $response->getBody()); 158 | } 159 | 160 | public function testServerParamsAreRecorded() 161 | { 162 | $this->client 163 | ->setUserAgent('CUSTOM UA') 164 | ->get('/foo') 165 | ->setAuth('username', 'password') 166 | ->setProtocolVersion('1.0') 167 | ->send(); 168 | 169 | $latestRequest = unserialize($this->client->get('/_request/latest')->send()->getBody()); 170 | 171 | $this->assertSame(HTTP_MOCK_HOST, $latestRequest['server']['SERVER_NAME']); 172 | $this->assertSame(HTTP_MOCK_PORT, $latestRequest['server']['SERVER_PORT']); 173 | $this->assertSame('username', $latestRequest['server']['PHP_AUTH_USER']); 174 | $this->assertSame('password', $latestRequest['server']['PHP_AUTH_PW']); 175 | $this->assertSame('HTTP/1.0', $latestRequest['server']['SERVER_PROTOCOL']); 176 | $this->assertSame('CUSTOM UA', $latestRequest['server']['HTTP_USER_AGENT']); 177 | } 178 | 179 | public function testNewestExpectationsAreFirstEvaluated() 180 | { 181 | $this->client->post( 182 | '/_expectation', 183 | null, 184 | $this->createExpectationParams( 185 | [ 186 | static function ($request) { 187 | return $request instanceof Request; 188 | } 189 | ], 190 | new Response('first', 200) 191 | ) 192 | )->send(); 193 | $this->assertSame('first', $this->client->get('/')->send()->getBody(true)); 194 | 195 | $this->client->post( 196 | '/_expectation', 197 | null, 198 | $this->createExpectationParams( 199 | [ 200 | static function ($request) { 201 | return $request instanceof Request; 202 | } 203 | ], 204 | new Response('second', 200) 205 | ) 206 | )->send(); 207 | $this->assertSame('second', $this->client->get('/')->send()->getBody(true)); 208 | } 209 | 210 | public function testServerLogsAreNotInErrorOutput() 211 | { 212 | $this->client->delete('/_all'); 213 | 214 | $expectedServerErrorOutput = '[404]: (null) / - No such file or directory'; 215 | 216 | self::$server1->addErrorOutput('PHP 7.4.2 Development Server (http://localhost:8086) started' . PHP_EOL); 217 | self::$server1->addErrorOutput('Accepted' . PHP_EOL); 218 | self::$server1->addErrorOutput($expectedServerErrorOutput . PHP_EOL); 219 | self::$server1->addErrorOutput('Closing' . PHP_EOL); 220 | 221 | $actualServerErrorOutput = self::$server1->getErrorOutput(); 222 | 223 | $this->assertEquals($expectedServerErrorOutput, $actualServerErrorOutput); 224 | 225 | self::$server1->clearErrorOutput(); 226 | } 227 | 228 | private function parseRequestFromResponse(GuzzleResponse $response) 229 | { 230 | $body = unserialize($response->getBody()); 231 | 232 | return RequestFactory::getInstance()->fromMessage($body['request']); 233 | } 234 | 235 | private function createExpectationParams(array $closures, Response $response) 236 | { 237 | foreach ($closures as $index => $closure) { 238 | $closures[$index] = new SerializableClosure($closure); 239 | } 240 | 241 | return [ 242 | 'matcher' => serialize($closures), 243 | 'response' => serialize($response), 244 | ]; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /tests/Fixtures/Request.php: -------------------------------------------------------------------------------- 1 | requestUri = $requestUri; 11 | } 12 | 13 | public function setContent($content) 14 | { 15 | $this->content = $content; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Matcher/ExtractorFactoryTest.php: -------------------------------------------------------------------------------- 1 | extractorFactory = new ExtractorFactory(); 20 | $this->request = $this->createMock('Symfony\Component\HttpFoundation\Request'); 21 | } 22 | 23 | public function testGetMethod() 24 | { 25 | $this->request 26 | ->expects($this->once()) 27 | ->method('getMethod') 28 | ->will($this->returnValue('POST')); 29 | 30 | $extractor = $this->extractorFactory->createMethodExtractor(); 31 | $this->assertSame('POST', $extractor($this->request)); 32 | } 33 | 34 | public function testGetPath() 35 | { 36 | $this->request 37 | ->expects($this->once()) 38 | ->method('getPathInfo') 39 | ->will($this->returnValue('/foo/bar')); 40 | 41 | $extractor = $this->extractorFactory->createPathExtractor(); 42 | $this->assertSame('/foo/bar', $extractor($this->request)); 43 | } 44 | 45 | public function testGetPathWithBasePath() 46 | { 47 | $this->request 48 | ->expects($this->once()) 49 | ->method('getPathInfo') 50 | ->will($this->returnValue('/foo/bar')); 51 | 52 | $extractorFactory = new ExtractorFactory('/foo'); 53 | 54 | $extractor = $extractorFactory->createPathExtractor(); 55 | $this->assertSame('/bar', $extractor($this->request)); 56 | } 57 | 58 | public function testGetPathWithBasePathTrailingSlash() 59 | { 60 | $this->request 61 | ->expects($this->once()) 62 | ->method('getPathInfo') 63 | ->will($this->returnValue('/foo/bar')); 64 | 65 | $extractorFactory = new ExtractorFactory('/foo/'); 66 | 67 | $extractor = $extractorFactory->createPathExtractor(); 68 | $this->assertSame('/bar', $extractor($this->request)); 69 | } 70 | 71 | public function testGetPathWithBasePathThatDoesNotMatch() 72 | { 73 | $this->request 74 | ->expects($this->once()) 75 | ->method('getPathInfo') 76 | ->will($this->returnValue('/bar')); 77 | 78 | $extractorFactory = new ExtractorFactory('/foo'); 79 | 80 | $extractor = $extractorFactory->createPathExtractor(); 81 | $this->assertSame('', $extractor($this->request)); 82 | } 83 | 84 | public function testGetHeaderWithExistingHeader() 85 | { 86 | $request = new Request( 87 | [], 88 | [], 89 | [], 90 | [], 91 | [], 92 | ['HTTP_CONTENT_TYPE' => 'application/json'] 93 | ); 94 | 95 | $extractorFactory = new ExtractorFactory('/foo'); 96 | 97 | $extractor = $extractorFactory->createHeaderExtractor('content-type'); 98 | $this->assertSame('application/json', $extractor($request)); 99 | } 100 | 101 | public function testGetHeaderWithNonExistingHeader() 102 | { 103 | $request = new Request( 104 | [], 105 | [], 106 | [], 107 | [], 108 | [], 109 | ['HTTP_X_FOO' => 'bar'] 110 | ); 111 | 112 | $extractorFactory = new ExtractorFactory('/foo'); 113 | 114 | $extractor = $extractorFactory->createHeaderExtractor('content-type'); 115 | $this->assertNull($extractor($request)); 116 | } 117 | 118 | public function testHeaderExistsWithExistingHeader() 119 | { 120 | $request = new Request( 121 | [], 122 | [], 123 | [], 124 | [], 125 | [], 126 | ['HTTP_CONTENT_TYPE' => 'application/json'] 127 | ); 128 | 129 | $extractorFactory = new ExtractorFactory('/foo'); 130 | 131 | $extractor = $extractorFactory->createHeaderExistsExtractor('content-type'); 132 | $this->assertTrue($extractor($request)); 133 | } 134 | 135 | public function testHeaderExistsWithNonExistingHeader() 136 | { 137 | $request = new Request( 138 | [], 139 | [], 140 | [], 141 | [], 142 | [], 143 | ['HTTP_X_FOO' => 'bar'] 144 | ); 145 | 146 | $extractorFactory = new ExtractorFactory('/foo'); 147 | 148 | $extractor = $extractorFactory->createHeaderExistsExtractor('content-type'); 149 | $this->assertFalse($extractor($request)); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tests/Matcher/StringMatcherTest.php: -------------------------------------------------------------------------------- 1 | setExtractor(static function() { 14 | return 0; 15 | }); 16 | self::assertTrue($matcher->getMatcher()(new Request())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/MockBuilderIntegrationTest.php: -------------------------------------------------------------------------------- 1 | matches = new MatcherFactory(); 33 | $this->builder = new MockBuilder($this->matches, new ExtractorFactory()); 34 | $this->server = new Server(HTTP_MOCK_PORT, HTTP_MOCK_HOST); 35 | $this->server->start(); 36 | $this->server->clean(); 37 | } 38 | 39 | public function tearDown() 40 | { 41 | $this->server->stop(); 42 | } 43 | 44 | public function testCreateExpectation() 45 | { 46 | $builder = $this->builder 47 | ->when() 48 | ->pathIs('/foo') 49 | ->methodIs($this->matches->regex('/POST/')) 50 | ->callback(static function (Request $request) { 51 | error_log('CLOSURE MATCHER: ' . $request->getMethod() . ' ' . $request->getPathInfo()); 52 | return true; 53 | }) 54 | ->then() 55 | ->statusCode(401) 56 | ->body('response body') 57 | ->header('X-Foo', 'Bar') 58 | ->end(); 59 | 60 | $this->assertSame($this->builder, $builder); 61 | 62 | $expectations = $this->builder->flushExpectations(); 63 | 64 | $this->assertCount(1, $expectations); 65 | /** @var Expectation $expectation */ 66 | $expectation = current($expectations); 67 | 68 | $request = new TestRequest(); 69 | $request->setMethod('POST'); 70 | $request->setRequestUri('/foo'); 71 | 72 | $run = 0; 73 | $oldValue = ini_set('error_log', '/dev/null'); 74 | foreach ($expectation->getMatcherClosures() as $closure) { 75 | $this->assertTrue($closure($request)); 76 | 77 | $unserializedClosure = unserialize(serialize($closure)); 78 | $this->assertTrue($unserializedClosure($request)); 79 | 80 | $run++; 81 | } 82 | ini_set('error_log', $oldValue); 83 | $this->assertSame(3, $run); 84 | 85 | $expectation->getResponse()->setDate(new DateTime('2012-11-10 09:08:07', new DateTimeZone('UTC'))); 86 | $response = "HTTP/1.0 401 Unauthorized\r\nCache-Control: no-cache, private\r\nDate: Sat, 10 Nov 2012 09:08:07 GMT\r\nX-Foo: Bar\r\n\r\nresponse body"; 87 | $this->assertSame($response, (string)$expectation->getResponse()); 88 | 89 | 90 | $this->server->setUp($expectations); 91 | 92 | $client = $this->server->getClient(); 93 | 94 | $this->assertSame('response body', (string) $client->post('/foo')->send()->getBody()); 95 | 96 | $this->assertContains('CLOSURE MATCHER: POST /foo', $this->server->getErrorOutput()); 97 | } 98 | 99 | public function testCreateTwoExpectationsAfterEachOther() 100 | { 101 | $this->builder 102 | ->when() 103 | ->pathIs('/post-resource-1') 104 | ->methodIs('POST') 105 | ->then() 106 | ->statusCode(200) 107 | ->body('POST 1') 108 | ->end(); 109 | $this->server->setUp($this->builder->flushExpectations()); 110 | 111 | $this->builder 112 | ->when() 113 | ->pathIs('/post-resource-2') 114 | ->methodIs($this->matches->regex('/POST/')) 115 | ->then() 116 | ->statusCode(200) 117 | ->body('POST 2') 118 | ->end(); 119 | $this->server->setUp($this->builder->flushExpectations()); 120 | 121 | $this->assertSame('POST 1', (string) $this->server->getClient()->post('/post-resource-1')->send()->getBody()); 122 | $this->assertSame('POST 2', (string) $this->server->getClient()->post('/post-resource-2')->send()->getBody()); 123 | $this->assertSame('POST 1', (string) $this->server->getClient()->post('/post-resource-1')->send()->getBody()); 124 | $this->assertSame('POST 2', (string) $this->server->getClient()->post('/post-resource-2')->send()->getBody()); 125 | } 126 | 127 | public function testCreateSuccessiveExpectationsOnSameWhen() 128 | { 129 | $this->builder 130 | ->first() 131 | ->when() 132 | ->pathIs('/resource') 133 | ->methodIs('POST') 134 | ->then() 135 | ->body('called once'); 136 | $this->builder 137 | ->second() 138 | ->when() 139 | ->pathIs('/resource') 140 | ->methodIs('POST') 141 | ->then() 142 | ->body('called twice'); 143 | $this->builder 144 | ->nth(3) 145 | ->when() 146 | ->pathIs('/resource') 147 | ->methodIs('POST') 148 | ->then() 149 | ->body('called 3 times'); 150 | 151 | $this->server->setUp($this->builder->flushExpectations()); 152 | 153 | $this->assertSame('called once', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 154 | $this->assertSame('called twice', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 155 | $this->assertSame('called 3 times', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 156 | } 157 | 158 | public function testCreateSuccessiveExpectationsWithAny() 159 | { 160 | $this->builder 161 | ->first() 162 | ->when() 163 | ->pathIs('/resource') 164 | ->methodIs('POST') 165 | ->then() 166 | ->body('1'); 167 | $this->builder 168 | ->second() 169 | ->when() 170 | ->pathIs('/resource') 171 | ->methodIs('POST') 172 | ->then() 173 | ->body('2'); 174 | $this->builder 175 | ->any() 176 | ->when() 177 | ->pathIs('/resource') 178 | ->methodIs('POST') 179 | ->then() 180 | ->body('any'); 181 | 182 | $this->server->setUp($this->builder->flushExpectations()); 183 | 184 | $this->assertSame('1', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 185 | $this->assertSame('2', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 186 | $this->assertSame('any', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 187 | } 188 | 189 | public function testCreateSuccessiveExpectationsInUnexpectedOrder() 190 | { 191 | $this->builder 192 | ->second() 193 | ->when() 194 | ->pathIs('/resource') 195 | ->methodIs('POST') 196 | ->then() 197 | ->body('2'); 198 | $this->builder 199 | ->first() 200 | ->when() 201 | ->pathIs('/resource') 202 | ->methodIs('POST') 203 | ->then() 204 | ->body('1'); 205 | 206 | $this->server->setUp($this->builder->flushExpectations()); 207 | 208 | $this->assertSame('1', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 209 | $this->assertSame('2', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 210 | } 211 | 212 | public function testCreateSuccessiveExpectationsWithOnce() 213 | { 214 | $this->builder 215 | ->first() 216 | ->when() 217 | ->pathIs('/resource') 218 | ->methodIs('POST') 219 | ->then() 220 | ->body('1'); 221 | $this->builder 222 | ->second() 223 | ->when() 224 | ->pathIs('/resource') 225 | ->methodIs('POST') 226 | ->then() 227 | ->body('2'); 228 | $this->builder 229 | ->twice() 230 | ->when() 231 | ->pathIs('/resource') 232 | ->methodIs('POST') 233 | ->then() 234 | ->body('twice'); 235 | 236 | $this->server->setUp($this->builder->flushExpectations()); 237 | 238 | $this->assertSame('1', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 239 | $this->assertSame('2', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 240 | $this->assertSame('twice', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 241 | $this->assertSame('twice', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 242 | $this->assertSame('Expectation not met', (string) $this->server->getClient()->post('/resource')->send()->getBody()); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /tests/PHPUnit/HttpMockMultiPHPUnitIntegrationTest.php: -------------------------------------------------------------------------------- 1 | setUpHttpMock(); 28 | } 29 | 30 | public function tearDown() 31 | { 32 | $this->tearDownHttpMock(); 33 | } 34 | 35 | public static function getPaths() 36 | { 37 | return [ 38 | [ 39 | '/foo', 40 | '/bar', 41 | ] 42 | ]; 43 | } 44 | 45 | /** @dataProvider getPaths */ 46 | public function testSimpleRequest($path) 47 | { 48 | $this->http['firstNamedServer']->mock 49 | ->when() 50 | ->pathIs($path) 51 | ->then() 52 | ->body($path . ' body') 53 | ->end(); 54 | $this->http['firstNamedServer']->setUp(); 55 | 56 | $this->assertSame($path . ' body', (string) $this->http['firstNamedServer']->client->get($path)->send()->getBody()); 57 | 58 | $request = $this->http['firstNamedServer']->requests->latest(); 59 | $this->assertSame('GET', $request->getMethod()); 60 | $this->assertSame($path, $request->getPath()); 61 | 62 | $request = $this->http['firstNamedServer']->requests->last(); 63 | $this->assertSame('GET', $request->getMethod()); 64 | $this->assertSame($path, $request->getPath()); 65 | 66 | $request = $this->http['firstNamedServer']->requests->first(); 67 | $this->assertSame('GET', $request->getMethod()); 68 | $this->assertSame($path, $request->getPath()); 69 | 70 | $request = $this->http['firstNamedServer']->requests->at(0); 71 | $this->assertSame('GET', $request->getMethod()); 72 | $this->assertSame($path, $request->getPath()); 73 | 74 | $request = $this->http['firstNamedServer']->requests->pop(); 75 | $this->assertSame('GET', $request->getMethod()); 76 | $this->assertSame($path, $request->getPath()); 77 | 78 | $this->assertSame($path . ' body', (string) $this->http['firstNamedServer']->client->get($path)->send()->getBody()); 79 | 80 | $request = $this->http['firstNamedServer']->requests->shift(); 81 | $this->assertSame('GET', $request->getMethod()); 82 | $this->assertSame($path, $request->getPath()); 83 | 84 | $this->expectException('UnexpectedValueException'); 85 | 86 | $this->expectExceptionMessage('Expected status code 200 from "/_request/last", got 404'); 87 | $this->http['firstNamedServer']->requests->pop(); 88 | } 89 | 90 | public function testErrorLogOutput() 91 | { 92 | $this->http['firstNamedServer']->mock 93 | ->when() 94 | ->callback(static function () {error_log('error output');}) 95 | ->then() 96 | ->end(); 97 | $this->http['firstNamedServer']->setUp(); 98 | 99 | $this->http['firstNamedServer']->client->get('/foo')->send(); 100 | 101 | // Should fail during tear down as we have an error_log() on the server side 102 | try { 103 | $this->tearDown(); 104 | $this->fail('Exception expected'); 105 | } catch (\Exception $e) { 106 | $this->assertContains('HTTP mock server standard error output should be empty', $e->getMessage()); 107 | } 108 | } 109 | 110 | public function testFailedRequest() 111 | { 112 | $response = $this->http['firstNamedServer']->client->get('/foo')->send(); 113 | $this->assertSame(404, $response->getStatusCode()); 114 | $this->assertSame('No matching expectation found', (string) $response->getBody()); 115 | } 116 | 117 | public function testStopServer() 118 | { 119 | $this->http['firstNamedServer']->server->stop(); 120 | } 121 | 122 | /** @depends testStopServer */ 123 | public function testHttpServerIsRestartedIfATestStopsIt() 124 | { 125 | $response = $this->http['firstNamedServer']->client->get('/')->send(); 126 | $this->assertSame(404, $response->getStatusCode()); 127 | } 128 | 129 | public function testLimitDurationOfAResponse() 130 | { 131 | $this->http['firstNamedServer']->mock 132 | ->once() 133 | ->when() 134 | ->methodIs('POST') 135 | ->then() 136 | ->body('POST METHOD') 137 | ->end(); 138 | $this->http['firstNamedServer']->setUp(); 139 | $firstResponse = $this->http['firstNamedServer']->client->post('/')->send(); 140 | $this->assertSame(200, $firstResponse->getStatusCode()); 141 | $secondResponse = $this->http['firstNamedServer']->client->post('/')->send(); 142 | $this->assertSame(410, $secondResponse->getStatusCode()); 143 | $this->assertSame('Expectation not met', $secondResponse->getBody(true)); 144 | 145 | $this->http['firstNamedServer']->mock 146 | ->exactly(2) 147 | ->when() 148 | ->methodIs('POST') 149 | ->then() 150 | ->body('POST METHOD') 151 | ->end(); 152 | $this->http['firstNamedServer']->setUp(); 153 | $firstResponse = $this->http['firstNamedServer']->client->post('/')->send(); 154 | $this->assertSame(200, $firstResponse->getStatusCode()); 155 | $secondResponse = $this->http['firstNamedServer']->client->post('/')->send(); 156 | $this->assertSame(200, $secondResponse->getStatusCode()); 157 | $thirdResponse = $this->http['firstNamedServer']->client->post('/')->send(); 158 | $this->assertSame(410, $thirdResponse->getStatusCode()); 159 | $this->assertSame('Expectation not met', $thirdResponse->getBody(true)); 160 | 161 | $this->http['firstNamedServer']->mock 162 | ->any() 163 | ->when() 164 | ->methodIs('POST') 165 | ->then() 166 | ->body('POST METHOD') 167 | ->end(); 168 | $this->http['firstNamedServer']->setUp(); 169 | $firstResponse = $this->http['firstNamedServer']->client->post('/')->send(); 170 | $this->assertSame(200, $firstResponse->getStatusCode()); 171 | $secondResponse = $this->http['firstNamedServer']->client->post('/')->send(); 172 | $this->assertSame(200, $secondResponse->getStatusCode()); 173 | $thirdResponse = $this->http['firstNamedServer']->client->post('/')->send(); 174 | $this->assertSame(200, $thirdResponse->getStatusCode()); 175 | } 176 | 177 | public function testCallbackOnResponse() 178 | { 179 | $this->http['firstNamedServer']->mock 180 | ->when() 181 | ->methodIs('POST') 182 | ->then() 183 | ->callback(static function(Response $response) {$response->setContent('CALLBACK');}) 184 | ->end(); 185 | $this->http['firstNamedServer']->setUp(); 186 | $this->assertSame('CALLBACK', $this->http['firstNamedServer']->client->post('/')->send()->getBody(true)); 187 | } 188 | 189 | public function testComplexResponse() 190 | { 191 | $this->http['firstNamedServer']->mock 192 | ->when() 193 | ->methodIs('POST') 194 | ->then() 195 | ->body('BODY') 196 | ->statusCode(201) 197 | ->header('X-Foo', 'Bar') 198 | ->end(); 199 | $this->http['firstNamedServer']->setUp(); 200 | $response = $this->http['firstNamedServer']->client 201 | ->post('/', ['x-client-header' => 'header-value'], ['post-key' => 'post-value'])->send(); 202 | $this->assertSame('BODY', $response->getBody(true)); 203 | $this->assertSame(201, $response->getStatusCode()); 204 | $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); 205 | $this->assertSame('post-value', $this->http['firstNamedServer']->requests->latest()->getPostField('post-key')); 206 | } 207 | 208 | public function testPutRequest() 209 | { 210 | $this->http['firstNamedServer']->mock 211 | ->when() 212 | ->methodIs('PUT') 213 | ->then() 214 | ->body('BODY') 215 | ->statusCode(201) 216 | ->header('X-Foo', 'Bar') 217 | ->end(); 218 | $this->http['firstNamedServer']->setUp(); 219 | $response = $this->http['firstNamedServer']->client 220 | ->put('/', ['x-client-header' => 'header-value'], ['put-key' => 'put-value'])->send(); 221 | $this->assertSame('BODY', $response->getBody(true)); 222 | $this->assertSame(201, $response->getStatusCode()); 223 | $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); 224 | $this->assertSame('put-value', $this->http['firstNamedServer']->requests->latest()->getPostField('put-key')); 225 | } 226 | 227 | public function testPostRequest() 228 | { 229 | $this->http['firstNamedServer']->mock 230 | ->when() 231 | ->methodIs('POST') 232 | ->then() 233 | ->body('BODY') 234 | ->statusCode(201) 235 | ->header('X-Foo', 'Bar') 236 | ->end(); 237 | $this->http['firstNamedServer']->setUp(); 238 | $response = $this->http['firstNamedServer']->client 239 | ->post('/', ['x-client-header' => 'header-value'], ['post-key' => 'post-value'])->send(); 240 | $this->assertSame('BODY', $response->getBody(true)); 241 | $this->assertSame(201, $response->getStatusCode()); 242 | $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); 243 | $this->assertSame('post-value', $this->http['firstNamedServer']->requests->latest()->getPostField('post-key')); 244 | } 245 | 246 | public function testFatalError() 247 | { 248 | if (version_compare(PHP_VERSION, '7.0', '<')) { 249 | $this->markTestSkipped('Comment in to test if fatal errors are properly handled'); 250 | } 251 | 252 | $this->expectException('Error'); 253 | 254 | $this->expectExceptionMessage('Cannot instantiate abstract class'); 255 | new TestCase(); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /tests/PHPUnit/HttpMockPHPUnitIntegrationBasePathTest.php: -------------------------------------------------------------------------------- 1 | setUpHttpMock(); 25 | } 26 | 27 | public function tearDown() 28 | { 29 | $this->tearDownHttpMock(); 30 | } 31 | 32 | public function testSimpleRequest() 33 | { 34 | $this->http->mock 35 | ->when() 36 | ->pathIs('/foo') 37 | ->then() 38 | ->body('/foo' . ' body') 39 | ->end(); 40 | $this->http->setUp(); 41 | 42 | $this->assertSame('/foo body', (string) $this->http->client->get('/custom-base-path/foo')->send()->getBody()); 43 | 44 | $request = $this->http->requests->latest(); 45 | $this->assertSame('GET', $request->getMethod()); 46 | $this->assertSame('/custom-base-path/foo', $request->getPath()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/PHPUnit/HttpMockPHPUnitIntegrationTest.php: -------------------------------------------------------------------------------- 1 | setUpHttpMock(); 28 | } 29 | 30 | public function tearDown() 31 | { 32 | $this->tearDownHttpMock(); 33 | } 34 | 35 | public static function getPaths() 36 | { 37 | return [ 38 | [ 39 | '/foo', 40 | '/bar', 41 | ] 42 | ]; 43 | } 44 | 45 | /** @dataProvider getPaths */ 46 | public function testSimpleRequest($path) 47 | { 48 | $this->http->mock 49 | ->when() 50 | ->pathIs($path) 51 | ->then() 52 | ->body($path . ' body') 53 | ->end(); 54 | $this->http->setUp(); 55 | 56 | $this->assertSame($path . ' body', (string) $this->http->client->get($path)->send()->getBody()); 57 | 58 | $request = $this->http->requests->latest(); 59 | $this->assertSame('GET', $request->getMethod()); 60 | $this->assertSame($path, $request->getPath()); 61 | 62 | $request = $this->http->requests->last(); 63 | $this->assertSame('GET', $request->getMethod()); 64 | $this->assertSame($path, $request->getPath()); 65 | 66 | $request = $this->http->requests->first(); 67 | $this->assertSame('GET', $request->getMethod()); 68 | $this->assertSame($path, $request->getPath()); 69 | 70 | $request = $this->http->requests->at(0); 71 | $this->assertSame('GET', $request->getMethod()); 72 | $this->assertSame($path, $request->getPath()); 73 | 74 | $request = $this->http->requests->pop(); 75 | $this->assertSame('GET', $request->getMethod()); 76 | $this->assertSame($path, $request->getPath()); 77 | 78 | $this->assertSame($path . ' body', (string) $this->http->client->get($path)->send()->getBody()); 79 | 80 | $request = $this->http->requests->shift(); 81 | $this->assertSame('GET', $request->getMethod()); 82 | $this->assertSame($path, $request->getPath()); 83 | 84 | $this->expectException('UnexpectedValueException'); 85 | 86 | $this->expectExceptionMessage('Expected status code 200 from "/_request/last", got 404'); 87 | $this->http->requests->pop(); 88 | } 89 | 90 | public function testErrorLogOutput() 91 | { 92 | $this->http->mock 93 | ->when() 94 | ->callback(static function () {error_log('error output');}) 95 | ->then() 96 | ->end(); 97 | $this->http->setUp(); 98 | 99 | $this->http->client->get('/foo')->send(); 100 | 101 | // Should fail during tear down as we have an error_log() on the server side 102 | try { 103 | $this->tearDown(); 104 | $this->fail('Exception expected'); 105 | } catch (\Exception $e) { 106 | $this->assertContains('HTTP mock server standard error output should be empty', $e->getMessage()); 107 | } 108 | } 109 | 110 | public function testFailedRequest() 111 | { 112 | $response = $this->http->client->get('/foo')->send(); 113 | $this->assertSame(404, $response->getStatusCode()); 114 | $this->assertSame('No matching expectation found', (string) $response->getBody()); 115 | } 116 | 117 | public function testStopServer() 118 | { 119 | $this->http->server->stop(); 120 | } 121 | 122 | /** @depends testStopServer */ 123 | public function testHttpServerIsRestartedIfATestStopsIt() 124 | { 125 | $response = $this->http->client->get('/')->send(); 126 | $this->assertSame(404, $response->getStatusCode()); 127 | } 128 | 129 | public function testLimitDurationOfAResponse() 130 | { 131 | $this->http->mock 132 | ->once() 133 | ->when() 134 | ->methodIs('POST') 135 | ->then() 136 | ->body('POST METHOD') 137 | ->end(); 138 | $this->http->setUp(); 139 | $firstResponse = $this->http->client->post('/')->send(); 140 | $this->assertSame(200, $firstResponse->getStatusCode()); 141 | $secondResponse = $this->http->client->post('/')->send(); 142 | $this->assertSame(410, $secondResponse->getStatusCode()); 143 | $this->assertSame('Expectation not met', $secondResponse->getBody(true)); 144 | 145 | $this->http->mock 146 | ->exactly(2) 147 | ->when() 148 | ->methodIs('POST') 149 | ->then() 150 | ->body('POST METHOD') 151 | ->end(); 152 | $this->http->setUp(); 153 | $firstResponse = $this->http->client->post('/')->send(); 154 | $this->assertSame(200, $firstResponse->getStatusCode()); 155 | $secondResponse = $this->http->client->post('/')->send(); 156 | $this->assertSame(200, $secondResponse->getStatusCode()); 157 | $thirdResponse = $this->http->client->post('/')->send(); 158 | $this->assertSame(410, $thirdResponse->getStatusCode()); 159 | $this->assertSame('Expectation not met', $thirdResponse->getBody(true)); 160 | 161 | $this->http->mock 162 | ->any() 163 | ->when() 164 | ->methodIs('POST') 165 | ->then() 166 | ->body('POST METHOD') 167 | ->end(); 168 | $this->http->setUp(); 169 | $firstResponse = $this->http->client->post('/')->send(); 170 | $this->assertSame(200, $firstResponse->getStatusCode()); 171 | $secondResponse = $this->http->client->post('/')->send(); 172 | $this->assertSame(200, $secondResponse->getStatusCode()); 173 | $thirdResponse = $this->http->client->post('/')->send(); 174 | $this->assertSame(200, $thirdResponse->getStatusCode()); 175 | } 176 | 177 | public function testCallbackOnResponse() 178 | { 179 | $this->http->mock 180 | ->when() 181 | ->methodIs('POST') 182 | ->then() 183 | ->callback(static function(Response $response) {$response->setContent('CALLBACK');}) 184 | ->end(); 185 | $this->http->setUp(); 186 | $this->assertSame('CALLBACK', $this->http->client->post('/')->send()->getBody(true)); 187 | } 188 | 189 | public function testComplexResponse() 190 | { 191 | $this->http->mock 192 | ->when() 193 | ->methodIs('POST') 194 | ->then() 195 | ->statusCode(201) 196 | ->header('X-Foo', 'Bar') 197 | ->body('BODY') 198 | ->end(); 199 | $this->http->setUp(); 200 | $response = $this->http->client 201 | ->post('/', ['x-client-header' => 'header-value'], ['post-key' => 'post-value'])->send(); 202 | $this->assertSame('BODY', $response->getBody(true)); 203 | $this->assertSame(201, $response->getStatusCode()); 204 | $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); 205 | $this->assertSame('post-value', $this->http->requests->latest()->getPostField('post-key')); 206 | } 207 | 208 | public function testPutRequest() 209 | { 210 | $this->http->mock 211 | ->when() 212 | ->methodIs('PUT') 213 | ->then() 214 | ->body('BODY') 215 | ->statusCode(201) 216 | ->header('X-Foo', 'Bar') 217 | ->end(); 218 | $this->http->setUp(); 219 | $response = $this->http->client 220 | ->put('/', ['x-client-header' => 'header-value'], ['put-key' => 'put-value'])->send(); 221 | $this->assertSame('BODY', $response->getBody(true)); 222 | $this->assertSame(201, $response->getStatusCode()); 223 | $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); 224 | $this->assertSame('put-value', $this->http->requests->latest()->getPostField('put-key')); 225 | } 226 | 227 | public function testPostRequest() 228 | { 229 | $this->http->mock 230 | ->when() 231 | ->methodIs('POST') 232 | ->then() 233 | ->body('BODY') 234 | ->statusCode(201) 235 | ->header('X-Foo', 'Bar') 236 | ->end(); 237 | $this->http->setUp(); 238 | $response = $this->http->client 239 | ->post('/', ['x-client-header' => 'header-value'], ['post-key' => 'post-value'])->send(); 240 | $this->assertSame('BODY', $response->getBody(true)); 241 | $this->assertSame(201, $response->getStatusCode()); 242 | $this->assertSame('Bar', (string) $response->getHeader('X-Foo')); 243 | $this->assertSame('post-value', $this->http->requests->latest()->getPostField('post-key')); 244 | } 245 | 246 | public function testCountRequests() 247 | { 248 | $this->http->mock 249 | ->when() 250 | ->pathIs('/resource') 251 | ->then() 252 | ->body('resource body') 253 | ->end(); 254 | $this->http->setUp(); 255 | 256 | $this->assertCount(0, $this->http->requests); 257 | $this->assertSame('resource body', (string) $this->http->client->get('/resource')->send()->getBody()); 258 | $this->assertCount(1, $this->http->requests); 259 | } 260 | 261 | public function testMatchQueryString() 262 | { 263 | $this->http->mock 264 | ->when() 265 | ->callback( 266 | function (Request $request) { 267 | return $request->query->has('key1'); 268 | } 269 | ) 270 | ->methodIs('GET') 271 | ->then() 272 | ->body('query string') 273 | ->end(); 274 | $this->http->setUp(); 275 | 276 | $this->assertSame('query string', (string) $this->http->client->get('/?key1=')->send()->getBody()); 277 | 278 | $this->assertSame(Response::HTTP_NOT_FOUND, $this->http->client->get('/')->send()->getStatusCode()); 279 | $this->assertSame(Response::HTTP_NOT_FOUND, $this->http->client->post('/')->send()->getStatusCode()); 280 | } 281 | 282 | public function testMatchRegex() 283 | { 284 | $this->http->mock 285 | ->when() 286 | ->methodIs($this->http->matches->regex('/(GET|POST)/')) 287 | ->then() 288 | ->body('response') 289 | ->end(); 290 | $this->http->setUp(); 291 | 292 | $this->assertSame('response', (string) $this->http->client->get('/')->send()->getBody()); 293 | $this->assertSame('response', (string) $this->http->client->get('/')->send()->getBody()); 294 | } 295 | 296 | public function testMatchQueryParams() 297 | { 298 | $this->http->mock 299 | ->when() 300 | ->queryParamExists('p1') 301 | ->queryParamIs('p2', 'v2') 302 | ->queryParamNotExists('p3') 303 | ->queryParamsExist(['p4']) 304 | ->queryParamsAre(['p5' => 'v5', 'p6' => 'v6']) 305 | ->queryParamsNotExist(['p7']) 306 | ->then() 307 | ->body('response') 308 | ->end(); 309 | $this->http->setUp(); 310 | 311 | $this->assertSame( 312 | 'response', 313 | (string) $this->http->client->get('/?p1=&p2=v2&p4=any&p5=v5&p6=v6')->send()->getBody() 314 | ); 315 | $this->assertSame( 316 | Response::HTTP_NOT_FOUND, 317 | $this->http->client->get('/?p1=&p2=v2&p3=foo')->send()->getStatusCode() 318 | ); 319 | $this->assertSame( 320 | Response::HTTP_NOT_FOUND, 321 | $this->http->client->get('/?p1=')->send()->getStatusCode() 322 | ); 323 | $this->assertSame( 324 | Response::HTTP_NOT_FOUND, 325 | $this->http->client->get('/?p3=foo')->send()->getStatusCode() 326 | ); 327 | } 328 | 329 | public function testFatalError() 330 | { 331 | if (version_compare(PHP_VERSION, '7.0', '<')) { 332 | $this->markTestSkipped('Comment in to test if fatal errors are properly handled'); 333 | } 334 | 335 | $this->expectException('Error'); 336 | 337 | $this->expectExceptionMessage('Cannot instantiate abstract class'); 338 | new TestCase(); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /tests/Request/UnifiedRequestTest.php: -------------------------------------------------------------------------------- 1 | wrappedRequest = $this->createMock('Guzzle\Http\Message\RequestInterface'); 27 | $this->wrappedEntityEnclosingRequest = $this->createMock('Guzzle\Http\Message\EntityEnclosingRequestInterface'); 28 | $this->unifiedRequest = new UnifiedRequest($this->wrappedRequest); 29 | $this->unifiedEnclosingEntityRequest = new UnifiedRequest($this->wrappedEntityEnclosingRequest); 30 | } 31 | 32 | public static function provideMethods() 33 | { 34 | return [ 35 | ['getParams'], 36 | ['getHeaders'], 37 | ['getHeaderLines'], 38 | ['getRawHeaders'], 39 | ['getQuery'], 40 | ['getMethod'], 41 | ['getScheme'], 42 | ['getHost'], 43 | ['getProtocolVersion'], 44 | ['getPath'], 45 | ['getPort'], 46 | ['getUsername'], 47 | ['getPassword'], 48 | ['getUrl'], 49 | ['getCookies'], 50 | ['getHeader', ['header']], 51 | ['hasHeader', ['header']], 52 | ['getUrl', [false]], 53 | ['getUrl', [true]], 54 | ['getCookie', ['cookieName']], 55 | ]; 56 | } 57 | 58 | public static function provideEntityEnclosingInterfaceMethods() 59 | { 60 | return [ 61 | ['getBody'], 62 | ['getPostField', ['postField']], 63 | ['getPostFields'], 64 | ['getPostFiles'], 65 | ['getPostFile', ['fileName']], 66 | ]; 67 | } 68 | 69 | /** @dataProvider provideMethods */ 70 | public function testMethodsFromRequestInterface($method, array $params = []) 71 | { 72 | $this->wrappedRequest 73 | ->expects($this->once()) 74 | ->method($method) 75 | ->will($this->returnValue('REQ')) 76 | ->with(...$params); 77 | $this->assertSame('REQ', call_user_func_array([$this->unifiedRequest, $method], $params)); 78 | 79 | 80 | $this->wrappedEntityEnclosingRequest 81 | ->expects($this->once()) 82 | ->method($method) 83 | ->will($this->returnValue('ENTITY_ENCL_REQ')) 84 | ->with(...$params); 85 | $this->assertSame( 86 | 'ENTITY_ENCL_REQ', 87 | call_user_func_array([$this->unifiedEnclosingEntityRequest, $method], $params) 88 | ); 89 | } 90 | 91 | /** @dataProvider provideEntityEnclosingInterfaceMethods */ 92 | public function testEntityEnclosingInterfaceMethods($method, array $params = []) 93 | { 94 | $this->wrappedEntityEnclosingRequest 95 | ->expects($this->once()) 96 | ->method($method) 97 | ->will($this->returnValue('ENTITY_ENCL_REQ')) 98 | ->with(...$params); 99 | 100 | $this->assertSame( 101 | 'ENTITY_ENCL_REQ', 102 | call_user_func_array([$this->unifiedEnclosingEntityRequest, $method], $params) 103 | ); 104 | 105 | $this->wrappedRequest 106 | ->expects($this->any()) 107 | ->method('getMethod') 108 | ->will($this->returnValue('METHOD')); 109 | $this->wrappedRequest 110 | ->expects($this->any()) 111 | ->method('getPath') 112 | ->will($this->returnValue('/foo')); 113 | 114 | $this->expectException('BadMethodCallException'); 115 | 116 | $this->expectExceptionMessage( 117 | 118 | sprintf( 119 | 'Cannot call method "%s" on a request that does not enclose an entity. Did you expect a POST/PUT request instead of METHOD /foo?', 120 | $method 121 | ) 122 | 123 | ); 124 | call_user_func_array([$this->unifiedRequest, $method], $params); 125 | } 126 | 127 | public function testUserAgent() 128 | { 129 | $this->assertNull($this->unifiedRequest->getUserAgent()); 130 | 131 | $unifiedRequest = new UnifiedRequest($this->wrappedRequest, ['userAgent' => 'UA']); 132 | $this->assertSame('UA', $unifiedRequest->getUserAgent()); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/RequestCollectionFacadeTest.php: -------------------------------------------------------------------------------- 1 | client = $this->createMock('Guzzle\Http\ClientInterface'); 26 | $this->facade = new RequestCollectionFacade($this->client); 27 | $this->request = new Request('GET', '/_request/last'); 28 | $this->request->setClient($this->client); 29 | } 30 | 31 | public static function provideMethodAndUrls() 32 | { 33 | return [ 34 | ['latest', '/_request/last'], 35 | ['first', '/_request/first'], 36 | ['last', '/_request/last'], 37 | ['at', '/_request/0', [0]], 38 | ['shift', '/_request/first', [], 'delete'], 39 | ['pop', '/_request/last', [], 'delete'], 40 | ]; 41 | } 42 | 43 | /** @dataProvider provideMethodAndUrls */ 44 | public function testRequestingLatestRequest($method, $path, array $args = [], $httpMethod = 'get') 45 | { 46 | $this->mockClient($path, $this->createSimpleResponse(), $httpMethod); 47 | 48 | $request = call_user_func_array([$this->facade, $method], $args); 49 | 50 | $this->assertSame('POST', $request->getMethod()); 51 | $this->assertSame('/foo', $request->getPath()); 52 | $this->assertSame('RECORDED=1', (string) $request->getBody()); 53 | } 54 | 55 | /** @dataProvider provideMethodAndUrls */ 56 | public function testRequestLatestResponseWithHttpAuth($method, $path, array $args = [], $httpMethod = 'get') 57 | { 58 | $this->mockClient($path, $this->createComplexResponse(), $httpMethod); 59 | 60 | $request = call_user_func_array([$this->facade, $method], $args); 61 | 62 | $this->assertSame('POST', $request->getMethod()); 63 | $this->assertSame('/foo', $request->getPath()); 64 | $this->assertSame('RECORDED=1', (string) $request->getBody()); 65 | $this->assertSame('host', $request->getHost()); 66 | $this->assertSame(1234, $request->getPort()); 67 | $this->assertSame('username', $request->getUsername()); 68 | $this->assertSame('password', $request->getPassword()); 69 | $this->assertSame('CUSTOM UA', $request->getUserAgent()); 70 | } 71 | 72 | /** @dataProvider provideMethodAndUrls */ 73 | public function testRequestResponse_InvalidStatusCode($method, $path, array $args = [], $httpMethod = 'get') 74 | { 75 | $this->mockClient($path, $this->createResponseWithInvalidStatusCode(), $httpMethod); 76 | 77 | $this->expectException('UnexpectedValueException'); 78 | 79 | $this->expectExceptionMessage('Expected status code 200 from "' . $path . '", got 404'); 80 | call_user_func_array([$this->facade, $method], $args); 81 | } 82 | 83 | /** @dataProvider provideMethodAndUrls */ 84 | public function testRequestResponse_EmptyContentType($method, $path, array $args = [], $httpMethod = 'get') 85 | { 86 | $this->mockClient($path, $this->createResponseWithEmptyContentType(), $httpMethod); 87 | 88 | $this->expectException('UnexpectedValueException'); 89 | 90 | $this->expectExceptionMessage('Expected content type "text/plain" from "' . $path . '", got ""'); 91 | call_user_func_array([$this->facade, $method], $args); 92 | } 93 | 94 | /** @dataProvider provideMethodAndUrls */ 95 | public function testRequestResponse_InvalidContentType($method, $path, array $args = [], $httpMethod = 'get') 96 | { 97 | $this->mockClient($path, $this->createResponseWithInvalidContentType(), $httpMethod); 98 | 99 | $this->expectException('UnexpectedValueException'); 100 | 101 | $this->expectExceptionMessage('Expected content type "text/plain" from "' . $path . '", got "text/html"'); 102 | call_user_func_array([$this->facade, $method], $args); 103 | } 104 | 105 | /** @dataProvider provideMethodAndUrls */ 106 | public function testRequestResponse_DeserializationError($method, $path, array $args = [], $httpMethod = 'get') 107 | { 108 | $this->mockClient($path, $this->createResponseThatCannotBeDeserialized(), $httpMethod); 109 | 110 | $this->expectException('UnexpectedValueException'); 111 | 112 | $this->expectExceptionMessage('Cannot deserialize response from "' . $path . '": "invalid response"'); 113 | call_user_func_array([$this->facade, $method], $args); 114 | } 115 | 116 | private function mockClient($path, Response $response, $method) 117 | { 118 | $this->client 119 | ->expects($this->once()) 120 | ->method($method) 121 | ->with($path) 122 | ->will($this->returnValue($this->request)); 123 | 124 | $this->client 125 | ->expects($this->once()) 126 | ->method('send') 127 | ->with($this->request) 128 | ->will($this->returnValue($response)); 129 | } 130 | 131 | private function createSimpleResponse() 132 | { 133 | $recordedRequest = new TestRequest(); 134 | $recordedRequest->setMethod('POST'); 135 | $recordedRequest->setRequestUri('/foo'); 136 | $recordedRequest->setContent('RECORDED=1'); 137 | 138 | return new Response( 139 | '200', 140 | ['Content-Type' => 'text/plain'], 141 | serialize( 142 | [ 143 | 'server' => [], 144 | 'request' => (string) $recordedRequest, 145 | ] 146 | ) 147 | ); 148 | } 149 | 150 | private function createComplexResponse() 151 | { 152 | $recordedRequest = new TestRequest(); 153 | $recordedRequest->setMethod('POST'); 154 | $recordedRequest->setRequestUri('/foo'); 155 | $recordedRequest->setContent('RECORDED=1'); 156 | $recordedRequest->headers->set('Php-Auth-User', 'ignored'); 157 | $recordedRequest->headers->set('Php-Auth-Pw', 'ignored'); 158 | $recordedRequest->headers->set('User-Agent', 'ignored'); 159 | 160 | return new Response( 161 | '200', 162 | ['Content-Type' => 'text/plain; charset=UTF-8'], 163 | serialize( 164 | [ 165 | 'server' => [ 166 | 'HTTP_HOST' => 'host', 167 | 'HTTP_PORT' => 1234, 168 | 'PHP_AUTH_USER' => 'username', 169 | 'PHP_AUTH_PW' => 'password', 170 | 'HTTP_USER_AGENT' => 'CUSTOM UA', 171 | ], 172 | 'request' => (string) $recordedRequest, 173 | ] 174 | ) 175 | ); 176 | } 177 | 178 | private function createResponseWithInvalidStatusCode() 179 | { 180 | return new Response(404); 181 | } 182 | 183 | private function createResponseWithInvalidContentType() 184 | { 185 | return new Response(200, ['Content-Type' => 'text/html']); 186 | } 187 | 188 | private function createResponseWithEmptyContentType() 189 | { 190 | return new Response(200, []); 191 | } 192 | 193 | private function createResponseThatCannotBeDeserialized() 194 | { 195 | return new Response(200, ['Content-Type' => 'text/plain'], 'invalid response'); 196 | } 197 | } 198 | --------------------------------------------------------------------------------