diff --git a/.gitignore b/.gitignore index e2739571..39a9b4de 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ doc/html/ vendor/ zf-mkdoc-theme/ +/test/TestAsset/.cache/ phpunit.xml diff --git a/.travis.yml b/.travis.yml index 7c9c6798..66d19569 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: php cache: directories: - $HOME/.composer/cache + - test/TestAsset/.cache # Cache archives are currently set to expire after 28 days by default env: global: diff --git a/composer.json b/composer.json index 60fdfd38..5e26fdd6 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "psr/http-message": "^1.0" }, "require-dev": { + "ext-curl": "*", "ext-dom": "*", "ext-libxml": "*", "http-interop/http-factory-tests": "^0.5.0", diff --git a/composer.lock b/composer.lock index c74a3704..32d45d1e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d3371a4353461b951e43e35fe472b59e", + "content-hash": "1460d979a214badb7a4393d3b770d676", "packages": [ { "name": "psr/http-factory", @@ -678,8 +678,8 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", @@ -726,8 +726,8 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "FilterIterator implementation that filters files based on a list of suffixes.", @@ -817,8 +817,8 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "Utility class for timing", @@ -948,8 +948,8 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "The PHP Unit Testing framework.", @@ -1733,6 +1733,7 @@ "php": "^7.1" }, "platform-dev": { + "ext-curl": "*", "ext-dom": "*", "ext-libxml": "*" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b6e22992..7ea9a15a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,6 +15,7 @@ + diff --git a/test/ResponseTest.php b/test/ResponseTest.php index 03838ce4..00ae89b5 100644 --- a/test/ResponseTest.php +++ b/test/ResponseTest.php @@ -1,7 +1,7 @@ assertSame('Unprocessable Entity', $response->getReasonPhrase()); } - public function ianaCodesReasonPhrasesProvider() + private function fetchIanaStatusCodes() : DOMDocument { - $ianaHttpStatusCodes = new DOMDocument(); - - libxml_set_streams_context( - stream_context_create( - [ - 'http' => [ - 'method' => 'GET', - 'timeout' => 30, - 'user_agent' => 'PHP', - ], - ] - ) - ); - - $ianaHttpStatusCodes->load('https://www.iana.org/assignments/http-status-codes/http-status-codes.xml'); - - if (! $ianaHttpStatusCodes->relaxNGValidate(__DIR__ . '/TestAsset/http-status-codes.rng')) { - self::fail('Unable to retrieve IANA response status codes due to timeout or invalid XML'); + $updated = null; + $ianaHttpStatusCodesFile = __DIR__ . '/TestAsset/.cache/http-status-codes.xml'; + $ianaHttpStatusCodes = null; + if (file_exists($ianaHttpStatusCodesFile)) { + $ianaHttpStatusCodes = new DOMDocument(); + $ianaHttpStatusCodes->load($ianaHttpStatusCodesFile); + if (! $ianaHttpStatusCodes->relaxNGValidate(__DIR__ . '/TestAsset/http-status-codes.rng')) { + $ianaHttpStatusCodes = null; + } + } + if ($ianaHttpStatusCodes) { + if (! getenv('ALWAYS_REFRESH_IANA_HTTP_STATUS_CODES')) { + // use cached codes + return $ianaHttpStatusCodes; + } + $xpath = new DOMXPath($ianaHttpStatusCodes); + $xpath->registerNamespace('ns', 'http://www.iana.org/assignments'); + $updated = $xpath->query('//ns:updated')->item(0)->nodeValue; + $updated = strtotime($updated); + } + + $ch = curl_init('https://www.iana.org/assignments/http-status-codes/http-status-codes.xml'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_USERAGENT, 'PHP Curl'); + if ($updated) { + $ifModifiedSince = sprintf( + 'If-Modified-Since: %s', + gmdate('D, d M Y H:i:s \G\M\T', $updated) + ); + curl_setopt($ch, CURLOPT_HTTPHEADER, [$ifModifiedSince]); + } + $response = curl_exec($ch); + $responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($responseCode === 304 && $ianaHttpStatusCodes) { + // status codes did not change + return $ianaHttpStatusCodes; } + if ($responseCode === 200) { + $downloadedIanaHttpStatusCodes = new DOMDocument(); + $downloadedIanaHttpStatusCodes->loadXML($response); + if ($downloadedIanaHttpStatusCodes->relaxNGValidate(__DIR__ . '/TestAsset/http-status-codes.rng')) { + file_put_contents($ianaHttpStatusCodesFile, $response, LOCK_EX); + return $downloadedIanaHttpStatusCodes; + } + } + if ($ianaHttpStatusCodes) { + // return cached codes if available + return $ianaHttpStatusCodes; + } + self::fail('Unable to retrieve IANA response status codes due to timeout or invalid XML'); + } + + public function ianaCodesReasonPhrasesProvider() + { + $ianaHttpStatusCodes = $this->fetchIanaStatusCodes(); + $ianaCodesReasonPhrases = []; $xpath = new DOMXPath($ianaHttpStatusCodes); diff --git a/test/TestAsset/.cache/.gitkeep b/test/TestAsset/.cache/.gitkeep new file mode 100644 index 00000000..e69de29b