├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── cover.yml │ └── lint.yml ├── .gitignore ├── .phan └── config.php ├── .php-cs-fixer.dist.php ├── LICENSE.md ├── README.md ├── composer.json ├── docs └── docs.md ├── example ├── basic.php ├── delayed.php ├── methods.php ├── multi.php ├── notfound.php ├── phpunit.php └── simple.php ├── mddoc.xml ├── phpcs.xml.dist ├── phpstan.neon ├── phpunit.xml.dist ├── server └── server.php ├── src ├── DelayedResponse.php ├── Exceptions │ ├── RuntimeException.php │ └── ServerException.php ├── InitializingResponseInterface.php ├── InternalServer.php ├── MockWebServer.php ├── MultiResponseInterface.php ├── RequestInfo.php ├── Response.php ├── ResponseByMethod.php ├── ResponseInterface.php ├── ResponseStack.php └── Responses │ ├── DefaultResponse.php │ └── NotFoundResponse.php └── test ├── DelayedResponseTest.php ├── Integration ├── InternalServer_IntegrationTest.php ├── Mock │ └── ExampleInitializingResponse.php ├── MockWebServer_ChangedDefault_IntegrationTest.php ├── MockWebServer_GetRequestByOffset_IntegrationTest.php └── MockWebServer_IntegrationTest.php ├── InternalServerTest.php ├── Regression └── MockWebServer_RegressionTest.php └── ResponseStackTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.php] 2 | charset = utf-8 3 | 4 | indent_style = tab 5 | indent_size = 4 6 | 7 | end_of_line = lf 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: monthly 13 | time: "11:00" 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - pull_request 3 | - push 4 | 5 | name: CI 6 | 7 | jobs: 8 | test: 9 | name: Tests 10 | 11 | strategy: 12 | matrix: 13 | operating-system: [ubuntu-latest, windows-latest] 14 | php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] 15 | 16 | runs-on: ${{ matrix.operating-system }} 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Install PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php-versions }} 26 | extensions: sockets, json, curl 27 | 28 | - name: Install dependencies with composer 29 | run: composer install 30 | 31 | - name: Test with phpunit 32 | run: vendor/bin/phpunit 33 | 34 | - name: Install PHPStan if PHP >= 7.4 35 | if: startsWith(matrix.php-versions, '7.4') || startsWith(matrix.php-versions, '8.') 36 | run: | 37 | composer require --dev phpstan/phpstan:2.1.17 38 | vendor/bin/phpstan 39 | -------------------------------------------------------------------------------- /.github/workflows/cover.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | name: Coveralls 7 | 8 | jobs: 9 | run: 10 | name: Tests 11 | 12 | strategy: 13 | matrix: 14 | operating-system: [ ubuntu-latest ] 15 | php-versions: [ '8.3' ] 16 | 17 | runs-on: ${{ matrix.operating-system }} 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Install PHP 24 | uses: shivammathur/setup-php@v2 25 | with: 26 | php-version: ${{ matrix.php-versions }} 27 | extensions: sockets, json, curl 28 | 29 | - name: Install dependencies with composer 30 | run: composer install 31 | 32 | - name: Run tests 33 | run: XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml 34 | 35 | - name: Upload coverage results to Coveralls 36 | env: 37 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | run: | 39 | composer global require php-coveralls/php-coveralls 40 | php-coveralls --coverage_clover=build/logs/clover.xml -v 41 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - pull_request 3 | - push 4 | 5 | name: Lint 6 | 7 | jobs: 8 | run: 9 | name: Linters 10 | 11 | strategy: 12 | matrix: 13 | operating-system: [ubuntu-latest] 14 | php-versions: ['8.3'] 15 | 16 | runs-on: ${{ matrix.operating-system }} 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Install PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php-versions }} 26 | extensions: sockets, json, curl 27 | tools: phan 28 | 29 | - name: Install dependencies with composer 30 | run: composer install 31 | 32 | - name: PHPCS 33 | run: vendor/bin/phpcs 34 | 35 | - name: phan 36 | run: phan --no-progress-bar 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | /coverage.xml 4 | *.cache 5 | -------------------------------------------------------------------------------- /.phan/config.php: -------------------------------------------------------------------------------- 1 | '7.1', 15 | 16 | // A list of directories that should be parsed for class and 17 | // method information. After excluding the directories 18 | // defined in exclude_analysis_directory_list, the remaining 19 | // files will be statically analyzed for errors. 20 | // 21 | // Thus, both first-party and third-party code being used by 22 | // your application should be included in this list. 23 | 'directory_list' => [ 24 | 'src', 25 | 'vendor/ralouphie', 26 | ], 27 | 28 | "exclude_file_list" => [ 29 | ], 30 | 31 | // "exclude_file_regex" => "@\.html\.php$@", 32 | 33 | // A directory list that defines files that will be excluded 34 | // from static analysis, but whose class and method 35 | // information should be included. 36 | // 37 | // Generally, you'll want to include the directories for 38 | // third-party code (such as "vendor/") in this list. 39 | // 40 | // n.b.: If you'd like to parse but not analyze 3rd 41 | // party code, directories containing that code 42 | // should be added to the `directory_list` as 43 | // to `exclude_analysis_directory_list`. 44 | "exclude_analysis_directory_list" => [ 45 | 'vendor/', 46 | ], 47 | 48 | 'plugin_config' => [ 49 | 'infer_pure_methods' => true, 50 | ], 51 | 52 | // A list of plugin files to execute. 53 | // See https://github.com/phan/phan/tree/master/.phan/plugins for even more. 54 | // (Pass these in as relative paths. 55 | // Base names without extensions such as 'AlwaysReturnPlugin' 56 | // can be used to refer to a plugin that is bundled with Phan) 57 | 'plugins' => [ 58 | // checks if a function, closure or method unconditionally returns. 59 | 60 | // can also be written as 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php' 61 | 'AlwaysReturnPlugin', 62 | // Checks for syntactically unreachable statements in 63 | // the global scope or function bodies. 64 | 'UnreachableCodePlugin', 65 | 'DollarDollarPlugin', 66 | 'DuplicateExpressionPlugin', 67 | 'DuplicateArrayKeyPlugin', 68 | 'PregRegexCheckerPlugin', 69 | 'PrintfCheckerPlugin', 70 | 'PHPUnitNotDeadCodePlugin', 71 | 'LoopVariableReusePlugin', 72 | 'UseReturnValuePlugin', 73 | 'RedundantAssignmentPlugin', 74 | 'InvalidVariableIssetPlugin', 75 | ], 76 | 77 | // Add any issue types (such as 'PhanUndeclaredMethod') 78 | // to this black-list to inhibit them from being reported. 79 | 'suppress_issue_types' => [ 80 | 'PhanUnusedPublicMethodParameter', 81 | ], 82 | 83 | 'unused_variable_detection' => true, 84 | ]; 85 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | files() 5 | ->in(__DIR__ . '/src') 6 | ->in(__DIR__ . '/test') 7 | ->in(__DIR__ . '/example') 8 | ->name('*.php'); 9 | 10 | $finder->files()->append([__DIR__ . 'composer/bin/mddoc']); 11 | 12 | return (new PhpCsFixer\Config) 13 | ->setUsingCache(true) 14 | ->setIndent("\t") 15 | ->setLineEnding("\n") 16 | //->setUsingLinter(false) 17 | ->setRiskyAllowed(true) 18 | ->setRules( 19 | [ 20 | '@PHPUnit60Migration:risky' => true, 21 | 'php_unit_test_case_static_method_calls' => [ 22 | 'call_type' => 'this', 23 | ], 24 | 25 | 'concat_space' => [ 26 | 'spacing' => 'one', 27 | ], 28 | 29 | 'visibility_required' => true, 30 | 'indentation_type' => true, 31 | 'no_useless_return' => true, 32 | 33 | 'switch_case_space' => true, 34 | 'switch_case_semicolon_to_colon' => true, 35 | 36 | 'array_syntax' => [ 'syntax' => 'short' ], 37 | 'list_syntax' => [ 'syntax' => 'short' ], 38 | 39 | 'no_leading_import_slash' => true, 40 | 'no_leading_namespace_whitespace' => true, 41 | 42 | 'no_whitespace_in_blank_line' => true, 43 | 44 | 'phpdoc_add_missing_param_annotation' => [ 'only_untyped' => true, ], 45 | 'phpdoc_indent' => true, 46 | 47 | 'phpdoc_no_alias_tag' => true, 48 | 'phpdoc_no_package' => true, 49 | 'phpdoc_no_useless_inheritdoc' => true, 50 | 51 | 'phpdoc_order' => true, 52 | 'phpdoc_scalar' => true, 53 | 'phpdoc_single_line_var_spacing' => true, 54 | 55 | 'phpdoc_var_annotation_correct_order' => true, 56 | 57 | 'phpdoc_trim' => true, 58 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 59 | 60 | 'phpdoc_types' => true, 61 | 'phpdoc_types_order' => [ 62 | 'null_adjustment' => 'always_last', 63 | 'sort_algorithm' => 'alpha', 64 | ], 65 | 66 | 'phpdoc_align' => [ 67 | 'align' => 'vertical', 68 | 'tags' => [ 'param' ], 69 | ], 70 | 71 | 'phpdoc_line_span' => [ 72 | 'const' => 'single', 73 | 'method' => 'multi', 74 | 'property' => 'single', 75 | ], 76 | 77 | 'short_scalar_cast' => true, 78 | 79 | 'standardize_not_equals' => true, 80 | 'ternary_operator_spaces' => true, 81 | 'no_spaces_after_function_name' => true, 82 | 'no_unneeded_control_parentheses' => true, 83 | 84 | 'return_type_declaration' => [ 85 | 'space_before' => 'one', 86 | ], 87 | 88 | 'single_line_after_imports' => true, 89 | 'single_blank_line_before_namespace' => true, 90 | 'blank_line_after_namespace' => true, 91 | 'single_blank_line_at_eof' => true, 92 | 'ternary_to_null_coalescing' => true, 93 | 'whitespace_after_comma_in_array' => true, 94 | 95 | 'cast_spaces' => [ 'space' => 'none' ], 96 | 97 | 'encoding' => true, 98 | 99 | 'space_after_semicolon' => [ 100 | 'remove_in_empty_for_expressions' => true, 101 | ], 102 | 103 | 'align_multiline_comment' => [ 104 | 'comment_type' => 'phpdocs_like', 105 | ], 106 | 107 | 'blank_line_before_statement' => [ 108 | 'statements' => [ 'continue', 'try', 'switch', 'exit', 'throw', 'return', 'do' ], 109 | ], 110 | 111 | 'no_superfluous_phpdoc_tags' => [ 112 | 'remove_inheritdoc' => true, 113 | ], 114 | 'no_superfluous_elseif' => true, 115 | 116 | 'no_useless_else' => true, 117 | 118 | 'combine_consecutive_issets' => true, 119 | 'escape_implicit_backslashes' => true, 120 | 'explicit_indirect_variable' => true, 121 | 'heredoc_to_nowdoc' => true, 122 | 123 | 124 | 'no_singleline_whitespace_before_semicolons' => true, 125 | 'no_null_property_initialization' => true, 126 | 'no_whitespace_before_comma_in_array' => true, 127 | 128 | 'no_empty_phpdoc' => true, 129 | 'no_empty_statement' => true, 130 | 'no_empty_comment' => true, 131 | 'no_extra_blank_lines' => true, 132 | 'no_blank_lines_after_phpdoc' => true, 133 | 134 | 'no_spaces_around_offset' => [ 135 | 'positions' => [ 'outside' ], 136 | ], 137 | 138 | 'return_assignment' => true, 139 | 'lowercase_static_reference' => true, 140 | 141 | 'method_chaining_indentation' => true, 142 | 'method_argument_space' => [ 143 | 'on_multiline' => 'ignore', // at least until they fix it 144 | 'keep_multiple_spaces_after_comma' => true, 145 | ], 146 | 147 | 'multiline_comment_opening_closing' => true, 148 | 149 | 'include' => true, 150 | 'elseif' => true, 151 | 152 | 'simple_to_complex_string_variable' => true, 153 | 154 | 'global_namespace_import' => [ 155 | 'import_classes' => false, 156 | 'import_constants' => false, 157 | 'import_functions' => false, 158 | ], 159 | 160 | 'trailing_comma_in_multiline' => true, 161 | 'single_line_comment_style' => true, 162 | 163 | 'is_null' => true, 164 | 'yoda_style' => [ 165 | 'equal' => false, 166 | 'identical' => false, 167 | 'less_and_greater' => null, 168 | ], 169 | ] 170 | ) 171 | ->setFinder($finder); 172 | 173 | 174 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright (c) 2017 Jesse G. Donat 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mock Web Server 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/donatj/mock-webserver/version)](https://packagist.org/packages/donatj/mock-webserver) 4 | [![License](https://poser.pugx.org/donatj/mock-webserver/license)](https://packagist.org/packages/donatj/mock-webserver) 5 | [![ci.yml](https://github.com/donatj/mock-webserver/actions/workflows/ci.yml/badge.svg)](https://github.com/donatj/mock-webserver/actions/workflows/ci.yml) 6 | 7 | 8 | Simple, easy to use Mock Web Server for PHP unit testing. Gets along simply with PHPUnit and other unit testing frameworks. 9 | 10 | Unit testing HTTP requests can be difficult, especially in cases where injecting a request library is difficult or not ideal. This helps greatly simplify the process. 11 | 12 | Mock Web Server creates a local Web Server you can make predefined requests against. 13 | 14 | 15 | ## Documentation 16 | 17 | [See: docs/docs.md](docs/docs.md) 18 | 19 | ## Requirements 20 | 21 | - **php**: >=7.1 22 | - **ext-sockets**: * 23 | - **ext-json**: * 24 | - **ralouphie/getallheaders**: ~2.0 || ~3.0 25 | 26 | ## Installing 27 | 28 | Install the latest version with: 29 | 30 | ```bash 31 | composer require --dev 'donatj/mock-webserver' 32 | ``` 33 | 34 | ## Examples 35 | 36 | ### Basic Usage 37 | 38 | The following example shows the most basic usage. If you do not define a path, the server will simply bounce a JSON body describing the request back to you. 39 | 40 | ```php 41 | start(); 49 | 50 | $url = $server->getServerRoot() . '/endpoint?get=foobar'; 51 | 52 | echo "Requesting: $url\n\n"; 53 | echo file_get_contents($url); 54 | 55 | ``` 56 | 57 | Outputs: 58 | 59 | ``` 60 | Requesting: http://127.0.0.1:61874/endpoint?get=foobar 61 | 62 | { 63 | "_GET": { 64 | "get": "foobar" 65 | }, 66 | "_POST": [], 67 | "_FILES": [], 68 | "_COOKIE": [], 69 | "HEADERS": { 70 | "Host": "127.0.0.1:61874", 71 | "Connection": "close" 72 | }, 73 | "METHOD": "GET", 74 | "INPUT": "", 75 | "PARSED_INPUT": [], 76 | "REQUEST_URI": "\/endpoint?get=foobar", 77 | "PARSED_REQUEST_URI": { 78 | "path": "\/endpoint", 79 | "query": "get=foobar" 80 | } 81 | } 82 | ``` 83 | 84 | ### Simple 85 | 86 | ```php 87 | start(); 96 | 97 | // We define the server's response to requests of the /definedPath endpoint 98 | $url = $server->setResponseOfPath( 99 | '/definedPath', 100 | new Response( 101 | 'This is our http body response', 102 | [ 'Cache-Control' => 'no-cache' ], 103 | 200 104 | ) 105 | ); 106 | 107 | echo "Requesting: $url\n\n"; 108 | 109 | $content = file_get_contents($url); 110 | 111 | // $http_response_header is a little known variable magically defined 112 | // in the current scope by file_get_contents with the response headers 113 | echo implode("\n", $http_response_header) . "\n\n"; 114 | echo $content . "\n"; 115 | 116 | ``` 117 | 118 | Outputs: 119 | 120 | ``` 121 | Requesting: http://127.0.0.1:61874/definedPath 122 | 123 | HTTP/1.1 200 OK 124 | Host: 127.0.0.1:61874 125 | Date: Tue, 31 Aug 2021 19:50:15 GMT 126 | Connection: close 127 | X-Powered-By: PHP/7.3.25 128 | Cache-Control: no-cache 129 | Content-type: text/html; charset=UTF-8 130 | 131 | This is our http body response 132 | ``` 133 | 134 | ### Change Default Response 135 | 136 | ```php 137 | start(); 146 | 147 | // The default response is donatj\MockWebServer\Responses\DefaultResponse 148 | // which returns an HTTP 200 and a descriptive JSON payload. 149 | // 150 | // Change the default response to donatj\MockWebServer\Responses\NotFoundResponse 151 | // to get a standard 404. 152 | // 153 | // Any other response may be specified as default as well. 154 | $server->setDefaultResponse(new NotFoundResponse); 155 | 156 | $content = file_get_contents($server->getServerRoot() . '/PageDoesNotExist', false, stream_context_create([ 157 | 'http' => [ 'ignore_errors' => true ], // allow reading 404s 158 | ])); 159 | 160 | // $http_response_header is a little known variable magically defined 161 | // in the current scope by file_get_contents with the response headers 162 | echo implode("\n", $http_response_header) . "\n\n"; 163 | echo $content . "\n"; 164 | 165 | ``` 166 | 167 | Outputs: 168 | 169 | ``` 170 | HTTP/1.1 404 Not Found 171 | Host: 127.0.0.1:61874 172 | Date: Tue, 31 Aug 2021 19:50:15 GMT 173 | Connection: close 174 | X-Powered-By: PHP/7.3.25 175 | Content-type: text/html; charset=UTF-8 176 | 177 | VND.DonatStudios.MockWebServer: Resource '/PageDoesNotExist' not found! 178 | 179 | ``` 180 | 181 | ### PHPUnit 182 | 183 | ```php 184 | start(); 197 | } 198 | 199 | public function testGetParams() : void { 200 | $result = file_get_contents(self::$server->getServerRoot() . '/autoEndpoint?foo=bar'); 201 | $decoded = json_decode($result, true); 202 | $this->assertSame('bar', $decoded['_GET']['foo']); 203 | } 204 | 205 | public function testGetSetPath() : void { 206 | // $url = http://127.0.0.1:61874/definedEndPoint 207 | $url = self::$server->setResponseOfPath('/definedEndPoint', new Response('foo bar content')); 208 | $result = file_get_contents($url); 209 | $this->assertSame('foo bar content', $result); 210 | } 211 | 212 | public static function tearDownAfterClass() : void { 213 | // stopping the web server during tear down allows us to reuse the port for later tests 214 | self::$server->stop(); 215 | } 216 | 217 | } 218 | 219 | ``` 220 | 221 | ### Delayed Response Usage 222 | 223 | By default responses will happen instantly. If you're looking to test timeouts, the DelayedResponse response wrapper may be useful. 224 | 225 | ```php 226 | start(); 236 | 237 | $response = new Response( 238 | 'This is our http body response', 239 | [ 'Cache-Control' => 'no-cache' ], 240 | 200 241 | ); 242 | 243 | // Wrap the response in a DelayedResponse object, which will delay the response 244 | $delayedResponse = new DelayedResponse( 245 | $response, 246 | 100000 // sets a delay of 100000 microseconds (.1 seconds) before returning the response 247 | ); 248 | 249 | $realtimeUrl = $server->setResponseOfPath('/realtime', $response); 250 | $delayedUrl = $server->setResponseOfPath('/delayed', $delayedResponse); 251 | 252 | echo "Requesting: $realtimeUrl\n\n"; 253 | 254 | // This request will run as quickly as possible 255 | $start = microtime(true); 256 | file_get_contents($realtimeUrl); 257 | echo "Realtime Request took: " . (microtime(true) - $start) . " seconds\n\n"; 258 | 259 | echo "Requesting: $delayedUrl\n\n"; 260 | 261 | // The request will take the delayed time + the time it takes to make and transfer the request 262 | $start = microtime(true); 263 | file_get_contents($delayedUrl); 264 | echo "Delayed Request took: " . (microtime(true) - $start) . " seconds\n\n"; 265 | 266 | ``` 267 | 268 | Outputs: 269 | 270 | ``` 271 | Requesting: http://127.0.0.1:61874/realtime 272 | 273 | Realtime Request took: 0.015669107437134 seconds 274 | 275 | Requesting: http://127.0.0.1:61874/delayed 276 | 277 | Delayed Request took: 0.10729098320007 seconds 278 | 279 | ``` 280 | 281 | ## Multiple Responses from the Same Endpoint 282 | 283 | ### Response Stack 284 | 285 | If you need an ordered set of responses, that can be done using the ResponseStack. 286 | 287 | ```php 288 | start(); 298 | 299 | // We define the servers response to requests of the /definedPath endpoint 300 | $url = $server->setResponseOfPath( 301 | '/definedPath', 302 | new ResponseStack( 303 | new Response("Response One"), 304 | new Response("Response Two") 305 | ) 306 | ); 307 | 308 | echo "Requesting: $url\n\n"; 309 | 310 | $contentOne = file_get_contents($url); 311 | $contentTwo = file_get_contents($url); 312 | // This third request is expected to 404 which will error if errors are not ignored 313 | $contentThree = file_get_contents($url, false, stream_context_create([ 'http' => [ 'ignore_errors' => true ] ])); 314 | 315 | // $http_response_header is a little known variable magically defined 316 | // in the current scope by file_get_contents with the response headers 317 | echo $contentOne . "\n"; 318 | echo $contentTwo . "\n"; 319 | echo $contentThree . "\n"; 320 | 321 | ``` 322 | 323 | Outputs: 324 | 325 | ``` 326 | Requesting: http://127.0.0.1:61874/definedPath 327 | 328 | Response One 329 | Response Two 330 | Past the end of the ResponseStack 331 | ``` 332 | 333 | ### Response by Method 334 | 335 | If you need to vary responses to a single endpoint by method, you can do that using the ResponseByMethod response object. 336 | 337 | ```php 338 | start(); 348 | 349 | // Create a response for both a POST and GET request to the same URL 350 | 351 | $response = new ResponseByMethod([ 352 | ResponseByMethod::METHOD_GET => new Response("This is our http GET response"), 353 | ResponseByMethod::METHOD_POST => new Response("This is our http POST response", [], 201), 354 | ]); 355 | 356 | $url = $server->setResponseOfPath('/foo/bar', $response); 357 | 358 | foreach( [ ResponseByMethod::METHOD_GET, ResponseByMethod::METHOD_POST ] as $method ) { 359 | echo "$method request to $url:\n"; 360 | 361 | $context = stream_context_create([ 'http' => [ 'method' => $method ] ]); 362 | $content = file_get_contents($url, false, $context); 363 | 364 | echo $content . "\n\n"; 365 | } 366 | 367 | ``` 368 | 369 | Outputs: 370 | 371 | ``` 372 | GET request to http://127.0.0.1:61874/foo/bar: 373 | This is our http GET response 374 | 375 | POST request to http://127.0.0.1:61874/foo/bar: 376 | This is our http POST response 377 | 378 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "donatj/mock-webserver", 3 | "description": "Simple mock web server for unit testing", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "webserver", "mock", "testing", "unit testing", "dev", "http", "phpunit" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Jesse G. Donat", 12 | "email": "donatj@gmail.com", 13 | "homepage": "https://donatstudios.com", 14 | "role": "Lead" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=7.1", 19 | "ext-sockets": "*", 20 | "ext-json": "*", 21 | "ralouphie/getallheaders": "~2.0 || ~3.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "donatj\\MockWebServer\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Test\\": "test/" 31 | } 32 | }, 33 | "require-dev": { 34 | "donatj/drop": "^1.0", 35 | "phpunit/phpunit": "~7|~9", 36 | "friendsofphp/php-cs-fixer": "^3.1", 37 | "squizlabs/php_codesniffer": "^3.6", 38 | "corpus/coding-standard": "^0.6.0 || ^0.9.0", 39 | "ext-curl": "*" 40 | }, 41 | "config": { 42 | "allow-plugins": { 43 | "dealerdirect/phpcodesniffer-composer-installer": true 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/docs.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Class: donatj\MockWebServer\MockWebServer 4 | 5 | ```php 6 | __construct 18 | 19 | ```php 20 | function __construct([ int $port = 0 [, string $host = '127.0.0.1']]) 21 | ``` 22 | 23 | TestWebServer constructor. 24 | 25 | #### Parameters: 26 | 27 | - ***int*** `$port` - Network port to run on 28 | - ***string*** `$host` - Listening hostname 29 | 30 | --- 31 | 32 | ### Method: MockWebServer->start 33 | 34 | ```php 35 | function start() : void 36 | ``` 37 | 38 | Start the Web Server on the selected port and host 39 | 40 | --- 41 | 42 | ### Method: MockWebServer->isRunning 43 | 44 | ```php 45 | function isRunning() : bool 46 | ``` 47 | 48 | Is the Web Server currently running? 49 | 50 | --- 51 | 52 | ### Method: MockWebServer->stop 53 | 54 | ```php 55 | function stop() : void 56 | ``` 57 | 58 | Stop the Web Server 59 | 60 | --- 61 | 62 | ### Method: MockWebServer->getServerRoot 63 | 64 | ```php 65 | function getServerRoot() : string 66 | ``` 67 | 68 | Get the HTTP root of the webserver 69 | e.g.: http://127.0.0.1:8123 70 | 71 | --- 72 | 73 | ### Method: MockWebServer->getUrlOfResponse 74 | 75 | ```php 76 | function getUrlOfResponse(\donatj\MockWebServer\ResponseInterface $response) : string 77 | ``` 78 | 79 | Get a URL providing the specified response. 80 | 81 | #### Returns: 82 | 83 | - ***string*** - URL where response can be found 84 | 85 | --- 86 | 87 | ### Method: MockWebServer->setResponseOfPath 88 | 89 | ```php 90 | function setResponseOfPath(string $path, \donatj\MockWebServer\ResponseInterface $response) : string 91 | ``` 92 | 93 | Set a specified path to provide a specific response 94 | 95 | --- 96 | 97 | ### Method: MockWebServer->setDefaultResponse 98 | 99 | ```php 100 | function setDefaultResponse(\donatj\MockWebServer\ResponseInterface $response) : void 101 | ``` 102 | 103 | Override the default server response, e.g. Fallback or 404 104 | 105 | --- 106 | 107 | ### Method: MockWebServer->getLastRequest 108 | 109 | ```php 110 | function getLastRequest() : ?\donatj\MockWebServer\RequestInfo 111 | ``` 112 | 113 | Get the previous requests associated request data. 114 | 115 | --- 116 | 117 | ### Method: MockWebServer->getRequestByOffset 118 | 119 | ```php 120 | function getRequestByOffset(int $offset) : ?\donatj\MockWebServer\RequestInfo 121 | ``` 122 | 123 | Get request by offset 124 | 125 | If offset is non-negative, the request will be the index from the start of the server. 126 | If offset is negative, the request will be that from the end of the requests. 127 | 128 | --- 129 | 130 | ### Method: MockWebServer->getHost 131 | 132 | ```php 133 | function getHost() : string 134 | ``` 135 | 136 | Get the host of the server. 137 | 138 | --- 139 | 140 | ### Method: MockWebServer->getPort 141 | 142 | ```php 143 | function getPort() : int 144 | ``` 145 | 146 | Get the port the network server is to be ran on. 147 | 148 | ## Class: donatj\MockWebServer\Response 149 | 150 | ### Method: Response->__construct 151 | 152 | ```php 153 | function __construct(string $body [, array $headers = [] [, int $status = 200]]) 154 | ``` 155 | 156 | Response constructor. 157 | 158 | ## Class: donatj\MockWebServer\ResponseStack 159 | 160 | ResponseStack is used to store multiple responses for a request issued by the server in order. 161 | 162 | When the stack is empty, the server will return a customizable response defaulting to a 404. 163 | 164 | ### Method: ResponseStack->__construct 165 | 166 | ```php 167 | function __construct(\donatj\MockWebServer\ResponseInterface ...$responses) 168 | ``` 169 | 170 | ResponseStack constructor. 171 | 172 | Accepts a variable number of ResponseInterface objects 173 | 174 | --- 175 | 176 | ### Method: ResponseStack->getPastEndResponse 177 | 178 | ```php 179 | function getPastEndResponse() : \donatj\MockWebServer\ResponseInterface 180 | ``` 181 | 182 | Gets the response returned when the stack is exhausted. 183 | 184 | --- 185 | 186 | ### Method: ResponseStack->setPastEndResponse 187 | 188 | ```php 189 | function setPastEndResponse(\donatj\MockWebServer\ResponseInterface $pastEndResponse) : void 190 | ``` 191 | 192 | Set the response to return when the stack is exhausted. 193 | 194 | ## Class: donatj\MockWebServer\ResponseByMethod 195 | 196 | ResponseByMethod is used to vary the response to a request by the called HTTP Method. 197 | 198 | ```php 199 | __construct 215 | 216 | ```php 217 | function __construct([ array $responses = [] [, ?\donatj\MockWebServer\ResponseInterface $defaultResponse = null]]) 218 | ``` 219 | 220 | MethodResponse constructor. 221 | 222 | #### Parameters: 223 | 224 | - ***array*** `$responses` - A map of responses keyed by their method. 225 | - ***\donatj\MockWebServer\ResponseInterface*** | ***null*** `$defaultResponse` - The fallthrough response to return if a response for a given 226 | method is not found. If this is not defined the server will 227 | return an HTTP 501 error. 228 | 229 | --- 230 | 231 | ### Method: ResponseByMethod->setMethodResponse 232 | 233 | ```php 234 | function setMethodResponse(string $method, \donatj\MockWebServer\ResponseInterface $response) : void 235 | ``` 236 | 237 | Set the Response for the Given Method 238 | 239 | ## Class: donatj\MockWebServer\DelayedResponse 240 | 241 | DelayedResponse wraps a response, causing it when called to be delayed by a specified number of microseconds. 242 | 243 | This is useful for simulating slow responses and testing timeouts. 244 | 245 | ### Method: DelayedResponse->__construct 246 | 247 | ```php 248 | function __construct(\donatj\MockWebServer\ResponseInterface $response, int $delay [, ?callable $usleep = null]) 249 | ``` 250 | 251 | #### Parameters: 252 | 253 | - ***int*** `$delay` - Microseconds to delay the response 254 | 255 | ## Built-In Responses 256 | 257 | ### Class: donatj\MockWebServer\Responses\DefaultResponse 258 | 259 | The Built-In Default Response. 260 | 261 | Results in an HTTP 200 with a JSON encoded version of the incoming Request 262 | 263 | ### Class: donatj\MockWebServer\Responses\NotFoundResponse 264 | 265 | Basic Built-In 404 Response -------------------------------------------------------------------------------- /example/basic.php: -------------------------------------------------------------------------------- 1 | start(); 9 | 10 | $url = $server->getServerRoot() . '/endpoint?get=foobar'; 11 | 12 | echo "Requesting: $url\n\n"; 13 | echo file_get_contents($url); 14 | -------------------------------------------------------------------------------- /example/delayed.php: -------------------------------------------------------------------------------- 1 | start(); 11 | 12 | $response = new Response( 13 | 'This is our http body response', 14 | [ 'Cache-Control' => 'no-cache' ], 15 | 200 16 | ); 17 | 18 | // Wrap the response in a DelayedResponse object, which will delay the response 19 | $delayedResponse = new DelayedResponse( 20 | $response, 21 | 100000 // sets a delay of 100000 microseconds (.1 seconds) before returning the response 22 | ); 23 | 24 | $realtimeUrl = $server->setResponseOfPath('/realtime', $response); 25 | $delayedUrl = $server->setResponseOfPath('/delayed', $delayedResponse); 26 | 27 | echo "Requesting: $realtimeUrl\n\n"; 28 | 29 | // This request will run as quickly as possible 30 | $start = microtime(true); 31 | file_get_contents($realtimeUrl); 32 | echo "Realtime Request took: " . (microtime(true) - $start) . " seconds\n\n"; 33 | 34 | echo "Requesting: $delayedUrl\n\n"; 35 | 36 | // The request will take the delayed time + the time it takes to make and transfer the request 37 | $start = microtime(true); 38 | file_get_contents($delayedUrl); 39 | echo "Delayed Request took: " . (microtime(true) - $start) . " seconds\n\n"; 40 | -------------------------------------------------------------------------------- /example/methods.php: -------------------------------------------------------------------------------- 1 | start(); 11 | 12 | // Create a response for both a POST and GET request to the same URL 13 | 14 | $response = new ResponseByMethod([ 15 | ResponseByMethod::METHOD_GET => new Response("This is our http GET response"), 16 | ResponseByMethod::METHOD_POST => new Response("This is our http POST response", [], 201), 17 | ]); 18 | 19 | $url = $server->setResponseOfPath('/foo/bar', $response); 20 | 21 | foreach( [ ResponseByMethod::METHOD_GET, ResponseByMethod::METHOD_POST ] as $method ) { 22 | echo "$method request to $url:\n"; 23 | 24 | $context = stream_context_create([ 'http' => [ 'method' => $method ] ]); 25 | $content = file_get_contents($url, false, $context); 26 | 27 | echo $content . "\n\n"; 28 | } 29 | -------------------------------------------------------------------------------- /example/multi.php: -------------------------------------------------------------------------------- 1 | start(); 11 | 12 | // We define the servers response to requests of the /definedPath endpoint 13 | $url = $server->setResponseOfPath( 14 | '/definedPath', 15 | new ResponseStack( 16 | new Response("Response One"), 17 | new Response("Response Two") 18 | ) 19 | ); 20 | 21 | echo "Requesting: $url\n\n"; 22 | 23 | $contentOne = file_get_contents($url); 24 | $contentTwo = file_get_contents($url); 25 | // This third request is expected to 404 which will error if errors are not ignored 26 | $contentThree = file_get_contents($url, false, stream_context_create([ 'http' => [ 'ignore_errors' => true ] ])); 27 | 28 | // $http_response_header is a little known variable magically defined 29 | // in the current scope by file_get_contents with the response headers 30 | echo $contentOne . "\n"; 31 | echo $contentTwo . "\n"; 32 | echo $contentThree . "\n"; 33 | -------------------------------------------------------------------------------- /example/notfound.php: -------------------------------------------------------------------------------- 1 | start(); 10 | 11 | // The default response is donatj\MockWebServer\Responses\DefaultResponse 12 | // which returns an HTTP 200 and a descriptive JSON payload. 13 | // 14 | // Change the default response to donatj\MockWebServer\Responses\NotFoundResponse 15 | // to get a standard 404. 16 | // 17 | // Any other response may be specified as default as well. 18 | $server->setDefaultResponse(new NotFoundResponse); 19 | 20 | $content = file_get_contents($server->getServerRoot() . '/PageDoesNotExist', false, stream_context_create([ 21 | 'http' => [ 'ignore_errors' => true ], // allow reading 404s 22 | ])); 23 | 24 | // $http_response_header is a little known variable magically defined 25 | // in the current scope by file_get_contents with the response headers 26 | echo implode("\n", $http_response_header) . "\n\n"; 27 | echo $content . "\n"; 28 | -------------------------------------------------------------------------------- /example/phpunit.php: -------------------------------------------------------------------------------- 1 | start(); 14 | } 15 | 16 | public function testGetParams() : void { 17 | $result = file_get_contents(self::$server->getServerRoot() . '/autoEndpoint?foo=bar'); 18 | $decoded = json_decode($result, true); 19 | $this->assertSame('bar', $decoded['_GET']['foo']); 20 | } 21 | 22 | public function testGetSetPath() : void { 23 | // $url = http://127.0.0.1:8123/definedEndPoint 24 | $url = self::$server->setResponseOfPath('/definedEndPoint', new Response('foo bar content')); 25 | $result = file_get_contents($url); 26 | $this->assertSame('foo bar content', $result); 27 | } 28 | 29 | public static function tearDownAfterClass() : void { 30 | // stopping the web server during tear down allows us to reuse the port for later tests 31 | self::$server->stop(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /example/simple.php: -------------------------------------------------------------------------------- 1 | start(); 10 | 11 | // We define the server's response to requests of the /definedPath endpoint 12 | $url = $server->setResponseOfPath( 13 | '/definedPath', 14 | new Response( 15 | 'This is our http body response', 16 | [ 'Cache-Control' => 'no-cache' ], 17 | 200 18 | ) 19 | ); 20 | 21 | echo "Requesting: $url\n\n"; 22 | 23 | $content = file_get_contents($url); 24 | 25 | // $http_response_header is a little known variable magically defined 26 | // in the current scope by file_get_contents with the response headers 27 | echo implode("\n", $http_response_header) . "\n\n"; 28 | echo $content . "\n"; 29 | -------------------------------------------------------------------------------- /mddoc.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 |
5 | 6 | 7 | 8 | 14 |
15 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 | 39 | 40 | 41 |
42 |
43 | 44 | 45 | Outputs: 46 | 47 |
48 |
49 | 50 | Outputs: 51 | 52 |
53 |
54 | 55 | Outputs: 56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 | 64 | Outputs: 65 | 66 | 67 | 68 | 69 | 70 |
71 |
72 |
73 |
74 | If you need an ordered set of responses, that can be done using the ResponseStack. 75 | 76 | Outputs: 77 | 78 |
79 |
80 | If you need to vary responses to a single endpoint by method, you can do that using the ResponseByMethod response object. 81 | 82 | Outputs: 83 | 84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | src 4 | test 5 | example 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 9 3 | paths: 4 | - src 5 | - server 6 | phpVersion: 70100 7 | ignoreErrors: 8 | - 9 | identifier: missingType.iterableValue 10 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | test 5 | 6 | 7 | 8 | 9 | ./src 10 | ./server 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/server.php: -------------------------------------------------------------------------------- 1 | response = $response; 28 | $this->delay = $delay; 29 | 30 | $this->usleep = '\\usleep'; 31 | if( $usleep ) { 32 | $this->usleep = $usleep; 33 | } 34 | } 35 | 36 | public function getRef() : string { 37 | return md5('delayed.' . $this->response->getRef()); 38 | } 39 | 40 | public function initialize( RequestInfo $request ) : void { 41 | ($this->usleep)($this->delay); 42 | } 43 | 44 | public function getBody( RequestInfo $request ) : string { 45 | return $this->response->getBody($request); 46 | } 47 | 48 | public function getHeaders( RequestInfo $request ) : array { 49 | return $this->response->getHeaders($request); 50 | } 51 | 52 | public function getStatus( RequestInfo $request ) : int { 53 | return $this->response->getStatus($request); 54 | } 55 | 56 | public function next() : bool { 57 | if( $this->response instanceof MultiResponseInterface ) { 58 | return $this->response->next(); 59 | } 60 | 61 | return false; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/Exceptions/RuntimeException.php: -------------------------------------------------------------------------------- 1 | tmpPath = $tmpPath; 42 | 43 | $count = self::incrementRequestCounter($this->tmpPath); 44 | $this->logRequest($request, $count); 45 | 46 | $this->request = $request; 47 | $this->header = $header; 48 | $this->httpResponseCode = $httpResponseCode; 49 | } 50 | 51 | /** 52 | * @internal 53 | */ 54 | public static function incrementRequestCounter( string $tmpPath, ?int $int = null ) : int { 55 | $countFile = $tmpPath . DIRECTORY_SEPARATOR . MockWebServer::REQUEST_COUNT_FILE; 56 | 57 | if( $int === null ) { 58 | $newInt = file_get_contents($countFile); 59 | if( !is_string($newInt) ) { 60 | throw new ServerException('failed to fetch request count'); 61 | } 62 | 63 | $int = (int)$newInt + 1; 64 | } 65 | 66 | file_put_contents($countFile, (string)$int); 67 | 68 | return (int)$int; 69 | } 70 | 71 | private function logRequest( RequestInfo $request, int $count ) : void { 72 | $reqStr = serialize($request); 73 | file_put_contents($this->tmpPath . DIRECTORY_SEPARATOR . MockWebServer::LAST_REQUEST_FILE, $reqStr); 74 | file_put_contents($this->tmpPath . DIRECTORY_SEPARATOR . 'request.' . $count, $reqStr); 75 | } 76 | 77 | /** 78 | * @internal 79 | */ 80 | public static function aliasPath( string $tmpPath, string $path ) : string { 81 | $path = '/' . ltrim($path, '/'); 82 | 83 | return sprintf('%s%salias.%s', 84 | $tmpPath, 85 | DIRECTORY_SEPARATOR, 86 | md5($path) 87 | ); 88 | } 89 | 90 | private function responseForRef( string $ref ) : ?ResponseInterface { 91 | $path = $this->tmpPath . DIRECTORY_SEPARATOR . $ref; 92 | if( !is_readable($path) ) { 93 | return null; 94 | } 95 | 96 | $content = file_get_contents($path); 97 | if( $content === false ) { 98 | throw new ServerException('failed to read response content'); 99 | } 100 | 101 | $response = unserialize($content); 102 | if( !$response instanceof ResponseInterface ) { 103 | throw new ServerException('invalid serialized response'); 104 | } 105 | 106 | return $response; 107 | } 108 | 109 | public function __invoke() : void { 110 | $ref = $this->getRefForUri($this->request->getParsedUri()['path']); 111 | 112 | if( $ref !== null ) { 113 | $response = $this->responseForRef($ref); 114 | if( $response ) { 115 | $this->sendResponse($response); 116 | 117 | return; 118 | } 119 | 120 | $this->sendResponse(new NotFoundResponse); 121 | 122 | return; 123 | } 124 | 125 | $response = $this->responseForRef(self::DEFAULT_REF); 126 | if( $response ) { 127 | $this->sendResponse($response); 128 | 129 | return; 130 | } 131 | 132 | $this->sendResponse(new DefaultResponse); 133 | } 134 | 135 | protected function sendResponse( ResponseInterface $response ) : void { 136 | if( $response instanceof InitializingResponseInterface ) { 137 | $response->initialize($this->request); 138 | } 139 | 140 | ($this->httpResponseCode)($response->getStatus($this->request)); 141 | 142 | foreach( $response->getHeaders($this->request) as $key => $header ) { 143 | if( is_int($key) ) { 144 | ($this->header)($header); 145 | } else { 146 | ($this->header)("{$key}: {$header}"); 147 | } 148 | } 149 | 150 | echo $response->getBody($this->request); 151 | 152 | if( $response instanceof MultiResponseInterface ) { 153 | $response->next(); 154 | self::storeResponse($this->tmpPath, $response); 155 | } 156 | } 157 | 158 | protected function getRefForUri( string $uriPath ) : ?string { 159 | $aliasPath = self::aliasPath($this->tmpPath, $uriPath); 160 | 161 | if( file_exists($aliasPath) ) { 162 | if( $path = file_get_contents($aliasPath) ) { 163 | return $path; 164 | } 165 | } elseif( preg_match('%^/' . preg_quote(MockWebServer::VND, '%') . '/([0-9a-fA-F]{32})$%', $uriPath, $matches) ) { 166 | return $matches[1]; 167 | } 168 | 169 | return null; 170 | } 171 | 172 | public static function getPathOfRef( string $ref ) : string { 173 | return '/' . MockWebServer::VND . '/' . $ref; 174 | } 175 | 176 | /** 177 | * @internal 178 | */ 179 | public static function storeResponse( string $tmpPath, ResponseInterface $response ) : string { 180 | $ref = $response->getRef(); 181 | self::storeRef($response, $tmpPath, $ref); 182 | 183 | return $ref; 184 | } 185 | 186 | /** 187 | * @internal 188 | */ 189 | public static function storeDefaultResponse( string $tmpPath, ResponseInterface $response ) : void { 190 | self::storeRef($response, $tmpPath, self::DEFAULT_REF); 191 | } 192 | 193 | private static function storeRef( ResponseInterface $response, string $tmpPath, string $ref ) : void { 194 | $content = serialize($response); 195 | 196 | if( !file_put_contents($tmpPath . DIRECTORY_SEPARATOR . $ref, $content) ) { 197 | throw new Exceptions\RuntimeException('Failed to write temporary content'); 198 | } 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/MockWebServer.php: -------------------------------------------------------------------------------- 1 | host = $host; 47 | $this->port = $port; 48 | if( $this->port === 0 ) { 49 | $this->port = $this->findOpenPort(); 50 | } 51 | 52 | $this->tmpDir = $this->getTmpDir(); 53 | } 54 | 55 | /** 56 | * Start the Web Server on the selected port and host 57 | */ 58 | public function start() : void { 59 | if( $this->isRunning() ) { 60 | return; 61 | } 62 | 63 | $script = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'server' . DIRECTORY_SEPARATOR . 'server.php'; 64 | 65 | $stdout = tempnam(sys_get_temp_dir(), 'mockserv-stdout-'); 66 | $cmd = sprintf("php -S %s:%d %s", $this->host, $this->port, escapeshellarg($script)); 67 | 68 | if( !putenv(self::TMP_ENV . '=' . $this->tmpDir) ) { 69 | throw new Exceptions\RuntimeException('Unable to put environmental variable'); 70 | } 71 | 72 | $fullCmd = sprintf('%s > %s 2>&1', 73 | $cmd, 74 | $stdout 75 | ); 76 | 77 | InternalServer::incrementRequestCounter($this->tmpDir, 0); 78 | 79 | [ $this->process, $this->descriptors ] = $this->startServer($fullCmd); 80 | 81 | for( $i = 0; $i <= 20; $i++ ) { 82 | usleep(100000); 83 | 84 | $open = @fsockopen($this->host, $this->port); 85 | if( is_resource($open) ) { 86 | fclose($open); 87 | break; 88 | } 89 | } 90 | 91 | if( !$this->isRunning() ) { 92 | throw new Exceptions\ServerException("Failed to start server. Is something already running on port {$this->port}?"); 93 | } 94 | 95 | register_shutdown_function(function () { 96 | if( $this->isRunning() ) { 97 | $this->stop(); 98 | } 99 | }); 100 | } 101 | 102 | /** 103 | * Is the Web Server currently running? 104 | */ 105 | public function isRunning() : bool { 106 | if( !is_resource($this->process) ) { 107 | return false; 108 | } 109 | 110 | $processStatus = proc_get_status($this->process); 111 | 112 | if( !$processStatus ) { 113 | return false; 114 | } 115 | 116 | return $processStatus['running']; 117 | } 118 | 119 | /** 120 | * Stop the Web Server 121 | */ 122 | public function stop() : void { 123 | if( $this->isRunning() ) { 124 | proc_terminate($this->process); 125 | 126 | $attempts = 0; 127 | while( $this->isRunning() ) { 128 | if( ++$attempts > 1000 ) { 129 | throw new Exceptions\ServerException('Failed to stop server.'); 130 | } 131 | 132 | usleep(10000); 133 | } 134 | } 135 | 136 | foreach( $this->descriptors as $descriptor ) { 137 | @fclose($descriptor); 138 | } 139 | 140 | $this->descriptors = []; 141 | } 142 | 143 | /** 144 | * Get the HTTP root of the webserver 145 | * e.g.: http://127.0.0.1:8123 146 | */ 147 | public function getServerRoot() : string { 148 | return "http://{$this->host}:{$this->port}"; 149 | } 150 | 151 | /** 152 | * Get a URL providing the specified response. 153 | * 154 | * @return string URL where response can be found 155 | */ 156 | public function getUrlOfResponse( ResponseInterface $response ) : string { 157 | $ref = InternalServer::storeResponse($this->tmpDir, $response); 158 | 159 | return $this->getServerRoot() . InternalServer::getPathOfRef($ref); 160 | } 161 | 162 | /** 163 | * Set a specified path to provide a specific response 164 | */ 165 | public function setResponseOfPath( string $path, ResponseInterface $response ) : string { 166 | $ref = InternalServer::storeResponse($this->tmpDir, $response); 167 | 168 | $aliasPath = InternalServer::aliasPath($this->tmpDir, $path); 169 | 170 | if( !file_put_contents($aliasPath, $ref) ) { 171 | throw new \RuntimeException('Failed to store path alias'); 172 | } 173 | 174 | return $this->getServerRoot() . $path; 175 | } 176 | 177 | /** 178 | * Override the default server response, e.g. Fallback or 404 179 | */ 180 | public function setDefaultResponse( ResponseInterface $response ) : void { 181 | InternalServer::storeDefaultResponse($this->tmpDir, $response); 182 | } 183 | 184 | /** 185 | * @internal 186 | */ 187 | private function getTmpDir() : string { 188 | $tmpDir = sys_get_temp_dir() ?: '/tmp'; 189 | if( !is_dir($tmpDir) || !is_writable($tmpDir) ) { 190 | throw new \RuntimeException('Unable to find system tmp directory'); 191 | } 192 | 193 | $tmpPath = $tmpDir . DIRECTORY_SEPARATOR . 'MockWebServer'; 194 | if( !is_dir($tmpPath) ) { 195 | if( !mkdir($tmpPath) && !is_dir($tmpPath) ) { 196 | throw new \RuntimeException(sprintf('Directory "%s" was not created', $tmpPath)); 197 | } 198 | } 199 | 200 | $tmpPath .= DIRECTORY_SEPARATOR . $this->port; 201 | if( !is_dir($tmpPath) ) { 202 | if( !mkdir($tmpPath) && !is_dir($tmpPath) ) { 203 | throw new \RuntimeException(sprintf('Directory "%s" was not created', $tmpPath)); 204 | } 205 | } 206 | 207 | $tmpPath .= DIRECTORY_SEPARATOR . md5(microtime(true) . ':' . rand(0, 100000)); 208 | if( !is_dir($tmpPath) ) { 209 | if( !mkdir($tmpPath) && !is_dir($tmpPath) ) { 210 | throw new \RuntimeException(sprintf('Directory "%s" was not created', $tmpPath)); 211 | } 212 | } 213 | 214 | return $tmpPath; 215 | } 216 | 217 | /** 218 | * Get the previous requests associated request data. 219 | */ 220 | public function getLastRequest() : ?RequestInfo { 221 | $path = $this->tmpDir . DIRECTORY_SEPARATOR . self::LAST_REQUEST_FILE; 222 | if( file_exists($path) ) { 223 | $content = file_get_contents($path); 224 | if( $content === false ) { 225 | throw new RuntimeException('failed to read last request'); 226 | } 227 | 228 | $data = @unserialize($content); 229 | if( $data instanceof RequestInfo ) { 230 | return $data; 231 | } 232 | } 233 | 234 | return null; 235 | } 236 | 237 | /** 238 | * Get request by offset 239 | * 240 | * If offset is non-negative, the request will be the index from the start of the server. 241 | * If offset is negative, the request will be that from the end of the requests. 242 | */ 243 | public function getRequestByOffset( int $offset ) : ?RequestInfo { 244 | $reqs = glob($this->tmpDir . DIRECTORY_SEPARATOR . 'request.*') ?: []; 245 | natsort($reqs); 246 | 247 | $item = array_slice($reqs, $offset, 1); 248 | if( !$item ) { 249 | return null; 250 | } 251 | 252 | $path = reset($item); 253 | if( !$path ) { 254 | return null; 255 | } 256 | 257 | $content = file_get_contents($path); 258 | if( $content === false ) { 259 | throw new RuntimeException("failed to read request from '{$path}'"); 260 | } 261 | 262 | $data = @unserialize($content); 263 | if( $data instanceof RequestInfo ) { 264 | return $data; 265 | } 266 | 267 | return null; 268 | } 269 | 270 | /** 271 | * Get the host of the server. 272 | */ 273 | public function getHost() : string { 274 | return $this->host; 275 | } 276 | 277 | /** 278 | * Get the port the network server is to be ran on. 279 | */ 280 | public function getPort() : int { 281 | return $this->port; 282 | } 283 | 284 | /** 285 | * Let the OS find an open port for you. 286 | */ 287 | private function findOpenPort() : int { 288 | $sock = socket_create(AF_INET, SOCK_STREAM, 0); 289 | if( $sock === false ) { 290 | throw new RuntimeException('Failed to create socket'); 291 | } 292 | 293 | // Bind the socket to an address/port 294 | if( !socket_bind($sock, $this->getHost(), 0) ) { 295 | throw new RuntimeException('Could not bind to address'); 296 | } 297 | 298 | socket_getsockname($sock, $checkAddress, $checkPort); 299 | socket_close($sock); 300 | 301 | if( $checkPort > 0 ) { 302 | return $checkPort; 303 | } 304 | 305 | throw new RuntimeException('Failed to find open port'); 306 | } 307 | 308 | private function isWindowsPlatform() : bool { 309 | return defined('PHP_WINDOWS_VERSION_MAJOR'); 310 | } 311 | 312 | /** 313 | * @return array{resource,array{resource,resource,resource}} 314 | */ 315 | private function startServer( string $fullCmd ) : array { 316 | if( !$this->isWindowsPlatform() ) { 317 | // We need to prefix exec to get the correct process http://php.net/manual/ru/function.proc-get-status.php#93382 318 | $fullCmd = 'exec ' . $fullCmd; 319 | } 320 | 321 | $pipes = []; 322 | $env = null; 323 | $cwd = null; 324 | 325 | $stdoutf = tempnam(sys_get_temp_dir(), 'MockWebServer.stdout'); 326 | if( $stdoutf === false ) { 327 | throw new RuntimeException('error creating stdout temp file'); 328 | } 329 | 330 | $stderrf = tempnam(sys_get_temp_dir(), 'MockWebServer.stderr'); 331 | if( $stderrf === false ) { 332 | throw new RuntimeException('error creating stderr temp file'); 333 | } 334 | 335 | $stdin = fopen('php://stdin', 'rb'); 336 | if( $stdin === false ) { 337 | throw new RuntimeException('error opening stdin'); 338 | } 339 | 340 | $stdout = fopen($stdoutf, 'ab'); 341 | if( $stdout === false ) { 342 | throw new RuntimeException('error opening stdout'); 343 | } 344 | 345 | $stderr = fopen($stderrf, 'ab'); 346 | if( $stderr === false ) { 347 | throw new RuntimeException('error opening stderr'); 348 | } 349 | 350 | $descriptorSpec = [ $stdin, $stdout, $stderr ]; 351 | 352 | $process = proc_open($fullCmd, $descriptorSpec, $pipes, $cwd, $env, [ 353 | 'suppress_errors' => false, 354 | 'bypass_shell' => true, 355 | ]); 356 | 357 | if( $process === false ) { 358 | throw new Exceptions\ServerException('Error starting server'); 359 | } 360 | 361 | return [ $process, $descriptorSpec ]; 362 | } 363 | 364 | } 365 | -------------------------------------------------------------------------------- /src/MultiResponseInterface.php: -------------------------------------------------------------------------------- 1 | server = $server; 50 | $this->get = $get; 51 | $this->post = $post; 52 | $this->files = $files; 53 | $this->cookie = $cookie; 54 | $this->HEADERS = $HEADERS; 55 | $this->INPUT = $INPUT; 56 | 57 | parse_str($INPUT, $PARSED_INPUT); 58 | $this->PARSED_INPUT = $PARSED_INPUT; 59 | 60 | if( !isset($server['REQUEST_URI']) ) { 61 | throw new RuntimeException('REQUEST_URI not set'); 62 | } 63 | 64 | if( !isset($server['REQUEST_METHOD']) ) { 65 | throw new RuntimeException('REQUEST_METHOD not set'); 66 | } 67 | 68 | $parsedUrl = parse_url($server['REQUEST_URI']); 69 | if( $parsedUrl === false ) { 70 | throw new RuntimeException('Failed to parse REQUEST_URI: ' . $server['REQUEST_URI']); 71 | } 72 | 73 | $this->parsedUri = $parsedUrl; 74 | } 75 | 76 | /** 77 | * Specify data which should be serialized to JSON 78 | */ 79 | public function jsonSerialize() : array { 80 | return [ 81 | self::JSON_KEY_GET => $this->get, 82 | self::JSON_KEY_POST => $this->post, 83 | self::JSON_KEY_FILES => $this->files, 84 | self::JSON_KEY_COOKIE => $this->cookie, 85 | self::JSON_KEY_HEADERS => $this->HEADERS, 86 | self::JSON_KEY_METHOD => $this->getRequestMethod(), 87 | self::JSON_KEY_INPUT => $this->INPUT, 88 | self::JSON_KEY_PARSED_INPUT => $this->PARSED_INPUT, 89 | self::JSON_KEY_REQUEST_URI => $this->getRequestUri(), 90 | 91 | self::JSON_KEY_PARSED_REQUEST_URI => $this->parsedUri, 92 | ]; 93 | } 94 | 95 | /** 96 | * @return array 97 | */ 98 | public function getParsedUri() { 99 | return $this->parsedUri; 100 | } 101 | 102 | public function getRequestUri() : string { 103 | return $this->server['REQUEST_URI']; 104 | } 105 | 106 | public function getRequestMethod() : string { 107 | return $this->server['REQUEST_METHOD']; 108 | } 109 | 110 | public function getServer() : array { 111 | return $this->server; 112 | } 113 | 114 | public function getGet() : array { 115 | return $this->get; 116 | } 117 | 118 | public function getPost() : array { 119 | return $this->post; 120 | } 121 | 122 | public function getFiles() : array { 123 | return $this->files; 124 | } 125 | 126 | public function getCookie() : array { 127 | return $this->cookie; 128 | } 129 | 130 | public function getHeaders() : array { 131 | return $this->HEADERS; 132 | } 133 | 134 | public function getInput() : string { 135 | return $this->INPUT; 136 | } 137 | 138 | public function getParsedInput() : ?array { 139 | return $this->PARSED_INPUT; 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | body = $body; 21 | $this->headers = $headers; 22 | $this->status = $status; 23 | } 24 | 25 | public function getRef() : string { 26 | $content = json_encode([ 27 | $this->body, 28 | $this->status, 29 | $this->headers, 30 | ]); 31 | 32 | if( $content === false ) { 33 | throw new RuntimeException('Failed to encode response content to JSON: ' . json_last_error_msg()); 34 | } 35 | 36 | return md5($content); 37 | } 38 | 39 | public function getBody( RequestInfo $request ) : string { 40 | return $this->body; 41 | } 42 | 43 | public function getHeaders( RequestInfo $request ) : array { 44 | return $this->headers; 45 | } 46 | 47 | public function getStatus( RequestInfo $request ) : int { 48 | return $this->status; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/ResponseByMethod.php: -------------------------------------------------------------------------------- 1 | $responses A map of responses keyed by their method. 29 | * @param ResponseInterface|null $defaultResponse The fallthrough response to return if a response for a given 30 | * method is not found. If this is not defined the server will 31 | * return an HTTP 501 error. 32 | */ 33 | public function __construct( array $responses = [], ?ResponseInterface $defaultResponse = null ) { 34 | foreach( $responses as $method => $response ) { 35 | $this->setMethodResponse($method, $response); 36 | } 37 | 38 | if( $defaultResponse ) { 39 | $this->defaultResponse = $defaultResponse; 40 | } else { 41 | $this->defaultResponse = new Response('MethodResponse - Method Not Defined', [], 501); 42 | } 43 | } 44 | 45 | public function getRef() : string { 46 | $refBase = $this->defaultResponse->getRef(); 47 | foreach( $this->responses as $response ) { 48 | $refBase .= $response->getRef(); 49 | } 50 | 51 | return md5($refBase); 52 | } 53 | 54 | public function getBody( RequestInfo $request ) : string { 55 | return $this->getMethodResponse($request)->getBody($request); 56 | } 57 | 58 | public function getHeaders( RequestInfo $request ) : array { 59 | return $this->getMethodResponse($request)->getHeaders($request); 60 | } 61 | 62 | public function getStatus( RequestInfo $request ) : int { 63 | return $this->getMethodResponse($request)->getStatus($request); 64 | } 65 | 66 | private function getMethodResponse( RequestInfo $request ) : ResponseInterface { 67 | $method = $request->getRequestMethod(); 68 | 69 | return $this->responses[$method] ?? $this->defaultResponse; 70 | } 71 | 72 | /** 73 | * Set the Response for the Given Method 74 | */ 75 | public function setMethodResponse( string $method, ResponseInterface $response ) : void { 76 | $this->responses[$method] = $response; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/ResponseInterface.php: -------------------------------------------------------------------------------- 1 | value or ["Full: Header","OtherFull: Header"] 25 | * 26 | * @internal 27 | */ 28 | public function getHeaders( RequestInfo $request ) : array; 29 | 30 | /** 31 | * Get the HTTP Status Code 32 | * 33 | * @internal 34 | */ 35 | public function getStatus( RequestInfo $request ) : int; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/ResponseStack.php: -------------------------------------------------------------------------------- 1 | responses[] = $response; 33 | 34 | $refBase .= $response->getRef(); 35 | } 36 | 37 | $this->ref = md5($refBase); 38 | 39 | $this->currentResponse = reset($this->responses) ?: null; 40 | $this->pastEndResponse = new Response('Past the end of the ResponseStack', [], 404); 41 | } 42 | 43 | public function initialize( RequestInfo $request ) : void { 44 | if( $this->currentResponse instanceof InitializingResponseInterface ) { 45 | $this->currentResponse->initialize($request); 46 | } 47 | } 48 | 49 | public function next() : bool { 50 | array_shift($this->responses); 51 | $this->currentResponse = reset($this->responses) ?: null; 52 | 53 | return (bool)$this->currentResponse; 54 | } 55 | 56 | public function getRef() : string { 57 | return $this->ref; 58 | } 59 | 60 | public function getBody( RequestInfo $request ) : string { 61 | return $this->currentResponse ? 62 | $this->currentResponse->getBody($request) : 63 | $this->pastEndResponse->getBody($request); 64 | } 65 | 66 | public function getHeaders( RequestInfo $request ) : array { 67 | return $this->currentResponse ? 68 | $this->currentResponse->getHeaders($request) : 69 | $this->pastEndResponse->getHeaders($request); 70 | } 71 | 72 | public function getStatus( RequestInfo $request ) : int { 73 | return $this->currentResponse ? 74 | $this->currentResponse->getStatus($request) : 75 | $this->pastEndResponse->getStatus($request); 76 | } 77 | 78 | /** 79 | * Gets the response returned when the stack is exhausted. 80 | */ 81 | public function getPastEndResponse() : ResponseInterface { 82 | return $this->pastEndResponse; 83 | } 84 | 85 | /** 86 | * Set the response to return when the stack is exhausted. 87 | */ 88 | public function setPastEndResponse( ResponseInterface $pastEndResponse ) : void { 89 | $this->pastEndResponse = $pastEndResponse; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Responses/DefaultResponse.php: -------------------------------------------------------------------------------- 1 | 'application/json' ]; 25 | } 26 | 27 | public function getStatus( RequestInfo $request ) : int { 28 | return 200; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Responses/NotFoundResponse.php: -------------------------------------------------------------------------------- 1 | getParsedUri()['path']; 20 | 21 | return MockWebServer::VND . ": Resource '{$path}' not found!\n"; 22 | } 23 | 24 | public function getHeaders( RequestInfo $request ) : array { 25 | return []; 26 | } 27 | 28 | public function getStatus( RequestInfo $request ) : int { 29 | return 404; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /test/DelayedResponseTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(RequestInfo::class) 21 | ->disableOriginalConstructor() 22 | ->getMock(); 23 | 24 | $resp->initialize($requestInfo); 25 | 26 | $this->assertSame(1234, $foundDelay); 27 | } 28 | 29 | public function testNext() : void { 30 | $resp = new DelayedResponse(new DefaultResponse, 1234); 31 | $this->assertFalse($resp->next()); 32 | 33 | $resp = new DelayedResponse(new DelayedResponse(new DefaultResponse, 1234), 1234); 34 | $this->assertFalse($resp->next()); 35 | 36 | $resp = new DelayedResponse(new ResponseStack( 37 | new Response('foo'), 38 | new Response('bar'), 39 | new Response('baz') 40 | ), 1234); 41 | 42 | $req = $this->getMockBuilder(RequestInfo::class) 43 | ->disableOriginalConstructor() 44 | ->getMock(); 45 | 46 | $this->assertSame('foo', $resp->getBody($req)); 47 | $this->assertTrue($resp->next()); 48 | $this->assertSame('bar', $resp->getBody($req)); 49 | $this->assertTrue($resp->next()); 50 | $this->assertSame('baz', $resp->getBody($req)); 51 | $this->assertFalse($resp->next()); 52 | } 53 | 54 | public function testGetRef() : void { 55 | $resp1 = new DelayedResponse(new DefaultResponse, 1234); 56 | $this->assertNotFalse( 57 | preg_match('/^[a-f0-9]{32}$/', $resp1->getRef()), 58 | 'Ref must be a 32 character hex string' 59 | ); 60 | 61 | $resp2 = new DelayedResponse(new Response('foo'), 1234); 62 | $this->assertNotFalse( 63 | preg_match('/^[a-f0-9]{32}$/', $resp2->getRef()), 64 | 'Ref is a 32 character hex string' 65 | ); 66 | 67 | $this->assertNotSame($resp1->getRef(), $resp2->getRef(), 'Ref is unique per response'); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /test/Integration/InternalServer_IntegrationTest.php: -------------------------------------------------------------------------------- 1 | $method, 34 | 'REQUEST_URI' => $uri, 35 | ], 36 | $GET, $POST, $FILES, $COOKIE, $HEADERS, '' 37 | ); 38 | } 39 | 40 | public function testInternalServer_DefaultResponse() : void { 41 | $tmp = $this->getTempDirectory(); 42 | 43 | $headers = []; 44 | $header = static function ( $header ) use ( &$headers ) { 45 | $headers[] = $header; 46 | }; 47 | 48 | $statusCode = null; 49 | $httpResponseCode = static function ( $code ) use ( &$statusCode ) { 50 | $statusCode = $code; 51 | }; 52 | 53 | $r = $this->getRequestInfo('/test?foo=bar&baz[]=qux&baz[]=quux', [ 'foo' => 1 ], [ 'baz' => 2 ]); 54 | 55 | $server = new InternalServer($tmp, $r, $header, $httpResponseCode); 56 | 57 | ob_start(); 58 | $server(); 59 | $contents = ob_get_clean(); 60 | 61 | $body = json_decode($contents, true); 62 | 63 | $this->assertSame(200, $statusCode); 64 | 65 | $this->assertSame([ 66 | 'Content-Type: application/json', 67 | ], $headers); 68 | 69 | $expectedBody = [ 70 | '_GET' => [ 71 | 'foo' => 1, 72 | ], 73 | '_POST' => [ 74 | 'baz' => 2, 75 | ], 76 | '_FILES' => [ 77 | ], 78 | '_COOKIE' => [ 79 | ], 80 | 'HEADERS' => [ 81 | ], 82 | 'METHOD' => 'GET', 83 | 'INPUT' => '', 84 | 'PARSED_INPUT' => [ 85 | ], 86 | 'REQUEST_URI' => '/test?foo=bar&baz[]=qux&baz[]=quux', 87 | 'PARSED_REQUEST_URI' => [ 88 | 'path' => '/test', 89 | 'query' => 'foo=bar&baz[]=qux&baz[]=quux', 90 | ], 91 | ]; 92 | 93 | $this->assertSame($expectedBody, $body); 94 | } 95 | 96 | /** 97 | * @dataProvider provideBodyWithContentType 98 | */ 99 | public function testInternalServer_CustomResponse( string $body, string $contentType ) : void { 100 | $tmp = $this->getTempDirectory(); 101 | 102 | $headers = []; 103 | $header = static function ( $header ) use ( &$headers ) { 104 | $headers[] = $header; 105 | }; 106 | 107 | $statusCode = null; 108 | $httpResponseCode = static function ( $code ) use ( &$statusCode ) { 109 | $statusCode = $code; 110 | }; 111 | 112 | $response = new Response($body, [ 'Content-Type' => $contentType ], 200); 113 | 114 | $r = $this->getRequestInfo(InternalServer::getPathOfRef($response->getRef())); 115 | 116 | InternalServer::storeResponse($tmp, $response); 117 | $server = new InternalServer($tmp, $r, $header, $httpResponseCode); 118 | 119 | ob_start(); 120 | $server(); 121 | $contents = ob_get_clean(); 122 | 123 | $this->assertSame(200, $statusCode); 124 | 125 | $this->assertSame([ 126 | 'Content-Type: ' . $contentType, 127 | ], $headers); 128 | 129 | $this->assertSame($body, $contents); 130 | } 131 | 132 | public function provideBodyWithContentType() : \Generator { 133 | yield [ 'Hello World!', 'text/plain; charset=UTF-8' ]; 134 | yield [ '{"foo":"bar"}', 'application/json' ]; 135 | yield [ '

Test

', 'text/html' ]; 136 | } 137 | 138 | public function testInternalServer_DefaultResponseFallthrough() : void { 139 | $tmp = $this->getTempDirectory(); 140 | 141 | $headers = []; 142 | $header = static function ( $header ) use ( &$headers ) { 143 | $headers[] = $header; 144 | }; 145 | 146 | $statusCode = null; 147 | $httpResponseCode = static function ( $code ) use ( &$statusCode ) { 148 | $statusCode = $code; 149 | }; 150 | 151 | $response = new Response('Default Response!!!', [ 'Default' => 'Response!' ], 400); 152 | 153 | $r = $this->getRequestInfo('/any/invalid/response'); 154 | 155 | InternalServer::storeDefaultResponse($tmp, $response); 156 | $server = new InternalServer($tmp, $r, $header, $httpResponseCode); 157 | 158 | ob_start(); 159 | $server(); 160 | $contents = ob_get_clean(); 161 | 162 | $this->assertSame(400, $statusCode); 163 | 164 | $this->assertSame([ 165 | 'Default: Response!', 166 | ], $headers); 167 | 168 | $this->assertSame('Default Response!!!', $contents); 169 | } 170 | 171 | public function testInternalServer_InitializingResponse() : void { 172 | $tmp = $this->getTempDirectory(); 173 | 174 | $response = new ExampleInitializingResponse; 175 | 176 | $headers = []; 177 | $header = static function ( $header ) use ( &$headers ) { 178 | $headers[] = $header; 179 | }; 180 | 181 | $r = $this->getRequestInfo(InternalServer::getPathOfRef($response->getRef())); 182 | 183 | InternalServer::storeResponse($tmp, $response); 184 | $server = new InternalServer($tmp, $r, $header, function () { }); 185 | 186 | ob_start(); 187 | $server(); 188 | ob_end_clean(); 189 | 190 | $this->assertSame([ 'X-Did-Call-Init: YES' ], $headers); 191 | } 192 | 193 | public function testInternalServer_InvalidRef404() : void { 194 | $tmp = $this->getTempDirectory(); 195 | 196 | $headers = []; 197 | $header = static function ( $header ) use ( &$headers ) { 198 | $headers[] = $header; 199 | }; 200 | 201 | $statusCode = null; 202 | $httpResponseCode = static function ( $code ) use ( &$statusCode ) { 203 | $statusCode = $code; 204 | }; 205 | 206 | $r = $this->getRequestInfo(InternalServer::getPathOfRef(str_repeat('a', 32))); 207 | 208 | $server = new InternalServer($tmp, $r, $header, $httpResponseCode); 209 | 210 | ob_start(); 211 | $server(); 212 | $contents = ob_get_clean(); 213 | 214 | $this->assertSame(404, $statusCode); 215 | $this->assertSame([], $headers); 216 | $this->assertSame("VND.DonatStudios.MockWebServer: Resource '/VND.DonatStudios.MockWebServer/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' not found!\n", $contents); 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /test/Integration/Mock/ExampleInitializingResponse.php: -------------------------------------------------------------------------------- 1 | headers['X-Did-Call-Init'] = 'YES'; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /test/Integration/MockWebServer_ChangedDefault_IntegrationTest.php: -------------------------------------------------------------------------------- 1 | start(); 15 | 16 | $server->setResponseOfPath('funk', new Response('fresh')); 17 | $path = $server->getUrlOfResponse(new Response('fries')); 18 | 19 | $content = file_get_contents($server->getServerRoot() . '/PageDoesNotExist'); 20 | $result = json_decode($content, true); 21 | $this->assertNotFalse(stripos($http_response_header[0], '200 OK')); 22 | $this->assertSame('/PageDoesNotExist', $result['PARSED_REQUEST_URI']['path']); 23 | 24 | // try with a 404 25 | $server->setDefaultResponse(new NotFoundResponse); 26 | 27 | $content = file_get_contents($server->getServerRoot() . '/PageDoesNotExist', false, stream_context_create([ 28 | 'http' => [ 'ignore_errors' => true ], // allow reading 404s 29 | ])); 30 | 31 | $this->assertNotFalse(stripos($http_response_header[0], '404 Not Found')); 32 | $this->assertSame("VND.DonatStudios.MockWebServer: Resource '/PageDoesNotExist' not found!\n", $content); 33 | 34 | // try with a custom response 35 | $server->setDefaultResponse(new Response('cool beans')); 36 | $content = file_get_contents($server->getServerRoot() . '/BadUrlBadTime'); 37 | $this->assertSame('cool beans', $content); 38 | 39 | // ensure non-404-ing pages continue to work as expected 40 | $content = file_get_contents($server->getServerRoot() . '/funk'); 41 | $this->assertSame('fresh', $content); 42 | 43 | $content = file_get_contents($path); 44 | $this->assertSame('fries', $content); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /test/Integration/MockWebServer_GetRequestByOffset_IntegrationTest.php: -------------------------------------------------------------------------------- 1 | start(); 13 | 14 | for( $i = 0; $i <= 80; $i++ ) { 15 | $link = $server->getServerRoot() . '/link' . $i; 16 | $content = @file_get_contents($link); 17 | $this->assertNotFalse($content, "test link $i"); 18 | } 19 | 20 | for( $i = 0; $i <= 80; $i++ ) { 21 | $this->assertSame('/link' . $i, $server->getRequestByOffset($i)->getRequestUri(), 22 | "test positive offset alignment"); 23 | } 24 | 25 | for( $i = 0; $i <= 80; $i++ ) { 26 | $this->assertSame('/link' . $i, $server->getRequestByOffset(-81 + $i)->getRequestUri(), 27 | "test negative offset alignment"); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /test/Integration/MockWebServer_IntegrationTest.php: -------------------------------------------------------------------------------- 1 | start(); 20 | } 21 | 22 | public function testBasic() : void { 23 | $url = self::$server->getServerRoot() . '/endpoint?get=foobar'; 24 | $content = file_get_contents($url); 25 | 26 | // Some versions of PHP send it with file_get_contents, others do not. 27 | // Might be removable with a context but until I figure that out, terrible hack 28 | $content = preg_replace('/,\s*"Connection": "close"/', '', $content); 29 | 30 | $body = [ 31 | '_GET' => [ 'get' => 'foobar', ], 32 | '_POST' => [], 33 | '_FILES' => [], 34 | '_COOKIE' => [], 35 | 'HEADERS' => [ 'Host' => '127.0.0.1:' . self::$server->getPort(), ], 36 | 'METHOD' => 'GET', 37 | 'INPUT' => '', 38 | 'PARSED_INPUT' => [], 39 | 'REQUEST_URI' => '/endpoint?get=foobar', 40 | 'PARSED_REQUEST_URI' => [ 'path' => '/endpoint', 'query' => 'get=foobar', ], 41 | ]; 42 | 43 | $this->assertJsonStringEqualsJsonString($content, json_encode($body)); 44 | 45 | $lastReq = self::$server->getLastRequest()->jsonSerialize(); 46 | foreach( $body as $key => $val ) { 47 | if( $key === 'HEADERS' ) { 48 | // This is the same horrible connection hack as above. Fix in time. 49 | unset($lastReq[$key]['Connection']); 50 | } 51 | 52 | $this->assertSame($lastReq[$key], $val); 53 | } 54 | } 55 | 56 | public function testSimple() : void { 57 | // We define the servers response to requests of the /definedPath endpoint 58 | $url = self::$server->setResponseOfPath( 59 | '/definedPath', 60 | new Response( 61 | 'This is our http body response', 62 | [ 'X-Foo-Bar' => 'BazBazBaz' ], 63 | 200 64 | ) 65 | ); 66 | 67 | $content = file_get_contents($url); 68 | $this->assertContains('X-Foo-Bar: BazBazBaz', $http_response_header); 69 | $this->assertEquals("This is our http body response", $content); 70 | } 71 | 72 | public function testMulti() : void { 73 | $url = self::$server->getUrlOfResponse( 74 | new ResponseStack( 75 | new Response("Response One", [ 'X-Boop-Bat' => 'Sauce' ], 500), 76 | new Response("Response Two", [ 'X-Slaw-Dawg: FranCran' ], 400) 77 | ) 78 | ); 79 | 80 | $ctx = stream_context_create([ 'http' => [ 'ignore_errors' => true ] ]); 81 | 82 | $content = file_get_contents($url, false, $ctx); 83 | 84 | if( !( 85 | in_array('HTTP/1.0 500 Internal Server Error', $http_response_header, true) 86 | || in_array('HTTP/1.1 500 Internal Server Error', $http_response_header, true)) 87 | ) { 88 | $this->fail('must contain 500 Internal Server Error'); 89 | } 90 | 91 | $this->assertContains('X-Boop-Bat: Sauce', $http_response_header); 92 | $this->assertEquals("Response One", $content); 93 | 94 | $content = file_get_contents($url, false, $ctx); 95 | if( !( 96 | in_array('HTTP/1.0 400 Bad Request', $http_response_header, true) 97 | || in_array('HTTP/1.1 400 Bad Request', $http_response_header, true)) 98 | ) { 99 | $this->fail('must contain 400 Bad Request'); 100 | } 101 | 102 | $this->assertContains('X-Slaw-Dawg: FranCran', $http_response_header); 103 | $this->assertEquals("Response Two", $content); 104 | 105 | // this is expected to fail as we only have two responses in said stack 106 | $content = file_get_contents($url, false, $ctx); 107 | if( !( 108 | in_array('HTTP/1.0 404 Not Found', $http_response_header, true) 109 | || in_array('HTTP/1.1 404 Not Found', $http_response_header, true)) 110 | ) { 111 | $this->fail('must contain 404 Not Found'); 112 | } 113 | 114 | $this->assertEquals("Past the end of the ResponseStack", $content); 115 | } 116 | 117 | public function testHttpMethods() : void { 118 | $methods = [ 119 | ResponseByMethod::METHOD_GET, 120 | ResponseByMethod::METHOD_POST, 121 | ResponseByMethod::METHOD_PUT, 122 | ResponseByMethod::METHOD_PATCH, 123 | ResponseByMethod::METHOD_DELETE, 124 | ResponseByMethod::METHOD_HEAD, 125 | ResponseByMethod::METHOD_OPTIONS, 126 | ResponseByMethod::METHOD_TRACE, 127 | ]; 128 | 129 | $response = new ResponseByMethod; 130 | 131 | foreach( $methods as $method ) { 132 | $response->setMethodResponse($method, new Response( 133 | "This is our http $method body response", 134 | [ 'X-Foo-Bar' => 'Baz ' . $method ], 135 | 200 136 | )); 137 | } 138 | 139 | $url = self::$server->setResponseOfPath('/definedPath', $response); 140 | 141 | foreach( $methods as $method ) { 142 | $context = stream_context_create([ 'http' => [ 'method' => $method ] ]); 143 | $content = file_get_contents($url, false, $context); 144 | 145 | $this->assertContains('X-Foo-Bar: Baz ' . $method, $http_response_header); 146 | 147 | if( $method !== ResponseByMethod::METHOD_HEAD ) { 148 | $this->assertEquals("This is our http $method body response", $content); 149 | } 150 | } 151 | 152 | $context = stream_context_create([ 'http' => [ 'method' => 'PROPFIND' ] ]); 153 | $content = @file_get_contents($url, false, $context); 154 | 155 | $this->assertFalse($content); 156 | $this->assertStringEndsWith('501 Not Implemented', $http_response_header[0]); 157 | } 158 | 159 | public function testHttpMethods_fallthrough() : void { 160 | $response = new ResponseByMethod([], new Response('Default Fallthrough', [], 400)); 161 | 162 | $url = self::$server->setResponseOfPath('/definedPath', $response); 163 | 164 | $context = stream_context_create([ 'http' => [ 'method' => 'PROPFIND', 'ignore_errors' => true ] ]); 165 | $content = @file_get_contents($url, false, $context); 166 | 167 | $this->assertSame('Default Fallthrough', $content); 168 | $this->assertStringEndsWith('400 Bad Request', $http_response_header[0]); 169 | } 170 | 171 | public function testDelayedResponse() : void { 172 | 173 | $realtimeResponse = new Response( 174 | 'This is our http body response', 175 | [ 'X-Foo-Bar' => 'BazBazBaz' ], 176 | 200 177 | ); 178 | 179 | $delayedResponse = new DelayedResponse($realtimeResponse, 1000000); 180 | 181 | $this->assertNotSame($realtimeResponse->getRef(), $delayedResponse->getRef(), 182 | 'DelayedResponse should change the ref. If they are the same, using both causes issues.'); 183 | 184 | $realtimeUrl = self::$server->setResponseOfPath('/realtimePath', $realtimeResponse); 185 | $delayedUrl = self::$server->setResponseOfPath('/delayedPath', $delayedResponse); 186 | 187 | $realtimeStart = microtime(true); 188 | $content = @file_get_contents($realtimeUrl); 189 | $this->assertNotFalse($content); 190 | 191 | $delayedStart = microtime(true); 192 | $delayedContent = file_get_contents($delayedUrl); 193 | 194 | $end = microtime(true); 195 | 196 | $this->assertGreaterThan(.9, ($end - $delayedStart) - ($delayedStart - $realtimeStart), 'Delayed response should take ~1 seconds longer than realtime response'); 197 | 198 | $this->assertEquals('This is our http body response', $delayedContent); 199 | $this->assertContains('X-Foo-Bar: BazBazBaz', $http_response_header); 200 | } 201 | 202 | public function testDelayedMultiResponse() : void { 203 | $multi = new ResponseStack( 204 | new Response('Response One', [ 'X-Boop-Bat' => 'Sauce' ], 200), 205 | new Response('Response Two', [ 'X-Slaw-Dawg: FranCran' ], 200) 206 | ); 207 | 208 | $delayed = new DelayedResponse($multi, 1000000); 209 | 210 | $path = self::$server->setResponseOfPath('/delayedMultiPath', $delayed); 211 | 212 | $start = microtime(true); 213 | $contentOne = file_get_contents($path); 214 | $this->assertSame($contentOne, 'Response One'); 215 | $this->assertContains('X-Boop-Bat: Sauce', $http_response_header); 216 | $this->assertGreaterThan(.9, microtime(true) - $start, 'Delayed response should take ~1 seconds longer than realtime response'); 217 | 218 | $start = microtime(true); 219 | $contentTwo = file_get_contents($path); 220 | $this->assertSame($contentTwo, 'Response Two'); 221 | $this->assertContains('X-Slaw-Dawg: FranCran', $http_response_header); 222 | $this->assertGreaterThan(.9, microtime(true) - $start, 'Delayed response should take ~1 seconds longer than realtime response'); 223 | } 224 | 225 | public function testMultiResponseWithPartialDelay() : void { 226 | $multi = new ResponseStack( 227 | new Response('Response One', [ 'X-Boop-Bat' => 'Sauce' ], 200), 228 | new DelayedResponse(new Response('Response Two', [ 'X-Slaw-Dawg: FranCran' ], 200), 1000000) 229 | ); 230 | 231 | $path = self::$server->setResponseOfPath('/delayedMultiPath', $multi); 232 | 233 | $start = microtime(true); 234 | $contentOne = file_get_contents($path); 235 | $this->assertSame($contentOne, 'Response One'); 236 | $this->assertContains('X-Boop-Bat: Sauce', $http_response_header); 237 | $this->assertLessThan(.2, microtime(true) - $start, 'Delayed response should take less than 200ms'); 238 | 239 | $start = microtime(true); 240 | $contentTwo = file_get_contents($path); 241 | $this->assertSame($contentTwo, 'Response Two'); 242 | $this->assertContains('X-Slaw-Dawg: FranCran', $http_response_header); 243 | $this->assertGreaterThan(.9, microtime(true) - $start, 'Delayed response should take ~1 seconds longer than realtime response'); 244 | } 245 | 246 | /** 247 | * Regression Test - Was a problem in 1.0.0-beta.2 248 | */ 249 | public function testEmptySingle() : void { 250 | $url = self::$server->getUrlOfResponse(new Response('')); 251 | $this->assertSame('', file_get_contents($url)); 252 | } 253 | 254 | /** 255 | * @dataProvider requestInfoProvider 256 | */ 257 | public function testRequestInfo( 258 | $method, 259 | $uri, 260 | $respBody, 261 | $reqBody, 262 | array $headers, 263 | $status, 264 | $query, 265 | array $expectedCookies, 266 | array $serverVars 267 | ) { 268 | $url = self::$server->setResponseOfPath($uri, new Response($respBody, $headers, $status)); 269 | 270 | // Get cURL resource 271 | $ch = curl_init(); 272 | 273 | // Set url 274 | curl_setopt($ch, CURLOPT_URL, $url . '?' . $query); 275 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); 276 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 277 | 278 | $xheaders = []; 279 | foreach( $headers as $hkey => $hval ) { 280 | $xheaders[] = "{$hkey}: $hval"; 281 | } 282 | 283 | curl_setopt($ch, CURLOPT_HTTPHEADER, $xheaders); 284 | // Create body 285 | 286 | if( is_array($reqBody) ) { 287 | $encReqBody = http_build_query($reqBody); 288 | } else { 289 | $encReqBody = $reqBody ?: ''; 290 | } 291 | 292 | if( $encReqBody ) { 293 | curl_setopt($ch, CURLOPT_POST, 1); 294 | curl_setopt($ch, CURLOPT_POSTFIELDS, $encReqBody); 295 | } 296 | 297 | // Send the request & save response to $resp 298 | $resp = curl_exec($ch); 299 | $this->assertNotEmpty($resp, "response body is empty, request failed"); 300 | 301 | $this->assertSame($status, curl_getinfo($ch, CURLINFO_HTTP_CODE)); 302 | 303 | // Close request to clear up some resources 304 | curl_close($ch); 305 | 306 | $request = self::$server->getLastRequest(); 307 | 308 | $this->assertSame($uri . '?' . $query, $request->getRequestUri()); 309 | $this->assertSame([ 'path' => $uri, 'query' => ltrim($query, '?') ], $request->getParsedUri()); 310 | $this->assertContains(self::$server->getHost() . ':' . self::$server->getPort(), 311 | $request->getHeaders()); 312 | 313 | $reqHeaders = $request->getHeaders(); 314 | foreach( $headers as $hkey => $hval ) { 315 | $this->assertSame($reqHeaders[$hkey], $hval); 316 | } 317 | 318 | $this->assertSame($query, http_build_query($request->getGet())); 319 | $this->assertSame($method, $request->getRequestMethod()); 320 | 321 | $this->assertSame($expectedCookies, $request->getCookie()); 322 | 323 | $this->assertSame($encReqBody, $request->getInput()); 324 | 325 | parse_str($encReqBody, $decReqBody); 326 | $this->assertSame($decReqBody, $request->getParsedInput()); 327 | if( $method === 'POST' ) { 328 | $this->assertSame($decReqBody, $request->getPost()); 329 | } 330 | 331 | $server = $request->getServer(); 332 | 333 | $this->assertEquals(self::$server->getHost(), $server['SERVER_NAME']); 334 | $this->assertEquals(self::$server->getPort(), $server['SERVER_PORT']); 335 | 336 | foreach( $serverVars as $sKey => $sVal ) { 337 | $this->assertSame($server[$sKey], $sVal); 338 | } 339 | } 340 | 341 | public function requestInfoProvider() : array { 342 | return [ 343 | [ 344 | 'GET', 345 | '/requestInfoPath', 346 | 'This is our http body response', 347 | null, 348 | [ 'X-Foo-Bar' => 'BazBazBaz', 'Accept' => 'Juice' ], 349 | 200, 350 | 'foo=bar', 351 | [], 352 | [ 'HTTP_ACCEPT' => 'Juice', 'QUERY_STRING' => 'foo=bar' ], 353 | ], 354 | [ 355 | 'POST', 356 | '/requestInfoPath', 357 | 'This is my POST response', 358 | [ 'a' => 1 ], 359 | [ 'X-Boo-Bop' => 'Beep Boop', 'Cookie' => 'juice=mango' ], 360 | 301, 361 | 'x=1', 362 | [ 363 | 'juice' => 'mango', 364 | ], 365 | [ 'REQUEST_METHOD' => 'POST', 'QUERY_STRING' => 'x=1' ], 366 | ], 367 | [ 368 | 'PUT', 369 | '/put/path/90210', 370 | 'Put put put', 371 | [ 'a' => 1 ], 372 | [ 'X-Boo-Bop' => 'Beep Boop', 'Cookie' => 'a=b; c=d; e=f; what="soup"' ], 373 | 301, 374 | 'x=1', 375 | [ 376 | 'a' => 'b', 377 | 'c' => 'd', 378 | 'e' => 'f', 379 | 'what' => '"soup"', 380 | ], 381 | [ 'REQUEST_METHOD' => 'PUT', 'QUERY_STRING' => 'x=1' ], 382 | ], 383 | ]; 384 | } 385 | 386 | public function testStartStopServer() : void { 387 | $server = new MockWebServer; 388 | 389 | $server->start(); 390 | $this->assertTrue($server->isRunning()); 391 | 392 | $server->stop(); 393 | $this->assertFalse($server->isRunning()); 394 | } 395 | 396 | } 397 | -------------------------------------------------------------------------------- /test/InternalServerTest.php: -------------------------------------------------------------------------------- 1 | testTmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'testTemp'; 19 | mkdir($this->testTmpDir); 20 | 21 | $counterFileName = $this->testTmpDir . DIRECTORY_SEPARATOR . MockWebServer::REQUEST_COUNT_FILE; 22 | file_put_contents($counterFileName, '0'); 23 | } 24 | 25 | /** 26 | * @after 27 | */ 28 | public function afterEachTest() : void { 29 | $this->removeTempDirectory(); 30 | } 31 | 32 | private function removeTempDirectory() : void { 33 | $it = new \RecursiveDirectoryIterator($this->testTmpDir, \FilesystemIterator::SKIP_DOTS); 34 | $files = new \RecursiveIteratorIterator($it, 35 | \RecursiveIteratorIterator::CHILD_FIRST); 36 | 37 | foreach( $files as $file ) { 38 | if( $file->isDir() ) { 39 | rmdir($file->getRealPath()); 40 | } else { 41 | unlink($file->getRealPath()); 42 | } 43 | } 44 | 45 | rmdir($this->testTmpDir); 46 | } 47 | 48 | /** 49 | * @dataProvider countProvider 50 | */ 51 | public function testShouldIncrementRequestCounter( ?int $inputCount, int $expectedCount ) : void { 52 | $counterFileName = $this->testTmpDir . DIRECTORY_SEPARATOR . MockWebServer::REQUEST_COUNT_FILE; 53 | file_put_contents($counterFileName, '0'); 54 | 55 | InternalServer::incrementRequestCounter($this->testTmpDir, $inputCount); 56 | $this->assertStringEqualsFile($counterFileName, (string)$expectedCount); 57 | } 58 | 59 | public function countProvider() : array { 60 | return [ 61 | 'null count' => [ 62 | 'inputCount' => null, 63 | 'expectedCount' => 1, 64 | ], 65 | 'int count' => [ 66 | 'inputCount' => 25, 67 | 'expectedCount' => 25, 68 | ], 69 | ]; 70 | } 71 | 72 | public function testShouldLogRequestsOnInstanceCreate() : void { 73 | $fakeReq = new RequestInfo([ 74 | 'REQUEST_URI' => '/', 75 | 'REQUEST_METHOD' => 'GET', 76 | ], 77 | [], [], [], [], [], ''); 78 | new InternalServer($this->testTmpDir, $fakeReq); 79 | 80 | $lastRequestFile = $this->testTmpDir . DIRECTORY_SEPARATOR . MockWebServer::LAST_REQUEST_FILE; 81 | $requestFile = $this->testTmpDir . DIRECTORY_SEPARATOR . 'request.1'; 82 | 83 | $lastRequestContent = file_get_contents($lastRequestFile); 84 | $requestContent = file_get_contents($requestFile); 85 | 86 | $this->assertSame($lastRequestContent, $requestContent); 87 | $this->assertSame(serialize($fakeReq), $requestContent); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /test/Regression/MockWebServer_RegressionTest.php: -------------------------------------------------------------------------------- 1 | start(); 16 | $server->stop(); 17 | $server->stop(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /test/ResponseStackTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(RequestInfo::class)->disableOriginalConstructor()->getMock(); 14 | 15 | $x = new ResponseStack; 16 | 17 | $this->assertSame('Past the end of the ResponseStack', $x->getBody($mock)); 18 | $this->assertSame(404, $x->getStatus($mock)); 19 | $this->assertSame([], $x->getHeaders($mock)); 20 | $this->assertFalse($x->next()); 21 | } 22 | 23 | /** 24 | * @dataProvider customResponseProvider 25 | */ 26 | public function testCustomPastEndResponse( $body, $headers, $status ) : void { 27 | $mock = $this->getMockBuilder(RequestInfo::class)->disableOriginalConstructor()->getMock(); 28 | 29 | $x = new ResponseStack; 30 | $x->setPastEndResponse(new Response($body, $headers, $status)); 31 | 32 | $this->assertSame($body, $x->getBody($mock)); 33 | $this->assertSame($status, $x->getStatus($mock)); 34 | $this->assertSame($headers, $x->getHeaders($mock)); 35 | $this->assertFalse($x->next()); 36 | } 37 | 38 | public function customResponseProvider() : array { 39 | return [ 40 | [ 'PastEnd', [ 'HeaderA' => 'BVAL' ], 420 ], 41 | [ ' Leading and trailing whitespace ', [], 0 ], 42 | ]; 43 | } 44 | 45 | } 46 | --------------------------------------------------------------------------------