diff --git a/.travis.yml b/.travis.yml index dcd3f123..bf5d62f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: php php: - - 5.3 - 5.4 - 5.5 - 5.6 @@ -13,10 +12,8 @@ env: matrix: include: - - php: 5.5 - env: VARNISH_VERSION=3.0 - - php: 5.3 - env: SYMFONY_VERSION=2.3.* VARNISH_VERSION=4.0 COMPOSER_FLAGS="--prefer-lowest" + - php: 5.4 + env: SYMFONY_VERSION=2.3.* VARNISH_VERSION=3.0 COMPOSER_FLAGS="--prefer-lowest" branches: only: @@ -24,9 +21,12 @@ branches: # Build maintenance branches for older releases if needed. such branches should be named like "1.2" - '/^\d+\.\d+$/' +install: + - if [[ "$TRAVIS_PHP_VERSION" == "5.4" || "$TRAVIS_PHP_VERSION" == "hhvm" ]]; then composer remove "php-http/guzzle6-adapter" --dev --no-update; fi + - if [[ "$TRAVIS_PHP_VERSION" == "5.4" || "$TRAVIS_PHP_VERSION" == "hhvm" ]]; then composer require "php-http/guzzle5-adapter" --dev --no-update; fi + - composer update $COMPOSER_FLAGS --prefer-source --no-interaction + before_script: - # Install deps - - composer update $COMPOSER_FLAGS --dev --prefer-source --no-interaction # Install Varnish - curl http://repo.varnish-cache.org/debian/GPG-key.txt | sudo apt-key add - - echo "deb http://repo.varnish-cache.org/ubuntu/ precise varnish-${VARNISH_VERSION}" | sudo tee -a /etc/apt/sources.list @@ -36,7 +36,6 @@ before_script: # Install NGINX - sh ./tests/install-nginx.sh # Starting webserver - - sh -c "if [ '$TRAVIS_PHP_VERSION' = '5.3' ]; then ./tests/ci/install-apache.sh; fi" - sh -c "if [ '$TRAVIS_PHP_VERSION' = 'hhvm' ]; then ./tests/ci/install-apache-hhvm.sh; fi" script: diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a4bd3e..d1a86597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,20 @@ Changelog See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpCache/releases). +2.0.0 (unreleased) +------------------ + +* Replace hard coupling on Guzzle HTTP client with HTTP adapter. +* The NGINX purge location is no longer passed as constructor argument but by + calling `setPurgeLocation()`. +* In ProxyTestCase, `getHttpClient()` has been replaced with `getHttpAdapter()`; + added HTTP method parameter to `getResponse()`. + 1.4.0 ----- -* Added symfony/http-kernel [HttpCache client](http://foshttpcache.readthedocs.org/en/latest/proxy-clients.html#symfony-client). -* Added [SymfonyTestCase](http://foshttpcache.readthedocs.org/en/latest/testing-your-application.html#symfonytestcase). +* Added symfony/http-kernel [HttpCache client](http://foshttpcache.readthedocs.org/en/stable/proxy-clients.html#symfony-client). +* Added [SymfonyTestCase](http://foshttpcache.readthedocs.org/en/stable/testing-your-application.html#symfonytestcase). * Removed unneeded files from dist packages. 1.3.2 @@ -19,17 +28,17 @@ See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpC ----- * Added authentication support to user context subscribe. -* Fixed usage of deprecated Guzzle subtree splits. +* Fixed usage of deprecated Guzzle subtree splits. * Fixed exposed cache tags. 1.3.0 ----- -* Added [TagHandler](http://foshttpcache.readthedocs.org/en/latest/invalidation-handlers.html#tag-handler). +* Added [TagHandler](http://foshttpcache.readthedocs.org/en/stable/invalidation-handlers.html#tag-handler). * It is no longer possible to change the event dispatcher of the - CacheInvalidator once its instantiated. If you need a custom dispatcher, set - it right after creating the invalidator instance. -* Deprecated `CacheInvalidator::addSubscriber` in favor of either using the event + CacheInvalidator once its instantiated. If you need a custom dispatcher, set + it right after creating the invalidator instance. +* Deprecated `CacheInvalidator::addSubscriber` in favor of either using the event dispatcher instance you inject or doing `getEventDispatcher()->addSubscriber($subscriber)`. 1.2.0 diff --git a/README.md b/README.md index b2dd3906..2f325d2b 100644 --- a/README.md +++ b/README.md @@ -20,20 +20,20 @@ Symfony2-specific features to help with caching and caching proxies. Features -------- -* Send [cache invalidation requests](http://foshttpcache.readthedocs.org/en/latest/cache-invalidator.html) +* Send [cache invalidation requests](http://foshttpcache.readthedocs.org/en/stable/cache-invalidator.html) with minimal impact on performance. -* Use the built-in support for [Varnish](http://foshttpcache.readthedocs.org/en/latest/varnish-configuration.html) - 3 and 4, [NGINX](http://foshttpcache.readthedocs.org/en/latest/nginx-configuration.html), the - [Symfony reverse proxy from the http-kernel component](http://foshttpcache.readthedocs.org/en/latest/symfony-cache-configuration.html) +* Use the built-in support for [Varnish](http://foshttpcache.readthedocs.org/en/stable/varnish-configuration.html) + 3 and 4, [NGINX](http://foshttpcache.readthedocs.org/en/stable/nginx-configuration.html), the + [Symfony reverse proxy from the http-kernel component](http://foshttpcache.readthedocs.org/en/stable/symfony-cache-configuration.html) or easily implement your own caching proxy client. -* [Test your application](http://foshttpcache.readthedocs.org/en/latest/testing-your-application.html) +* [Test your application](http://foshttpcache.readthedocs.org/en/stable/testing-your-application.html) against your Varnish or NGINX setup. * This library is fully compatible with [HHVM](http://www.hhvm.com). Documentation ------------- -For more information, see [the documentation](http://foshttpcache.readthedocs.org/en/latest/). +For more information, see [the documentation](http://foshttpcache.readthedocs.org/en/stable/). License ------- diff --git a/composer.json b/composer.json index 33d34e91..df2f7ebb 100644 --- a/composer.json +++ b/composer.json @@ -21,14 +21,19 @@ } ], "require": { - "php": ">=5.3.3", - "guzzle/guzzle": "~3.8", + "php": ">=5.4.8", "symfony/event-dispatcher": "~2.3", - "symfony/options-resolver": "~2.3" + "symfony/options-resolver": "~2.3", + "psr/http-message-implementation": "~1.0", + "php-http/adapter-implementation": "^0.1.0", + "php-http/discovery": "^0.1.1", + "php-http/message-decorator": "^0.1.0" }, "require-dev": { "mockery/mockery": "~0.9.1", "monolog/monolog": "~1.0", + "php-http/guzzle6-adapter": "^0.1.0", + "guzzlehttp/psr7": "^1.0", "symfony/process": "~2.3", "symfony/http-kernel": "~2.3" }, @@ -47,7 +52,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "2.0.x-dev" } } } diff --git a/doc/includes/custom-headers.rst b/doc/includes/custom-headers.rst index bfd31bb6..3c840691 100644 --- a/doc/includes/custom-headers.rst +++ b/doc/includes/custom-headers.rst @@ -1,3 +1,4 @@ This allows you to pass headers that are different between purge requests. If you want to add a header to all purge requests, such as ``Authorization``, -use a :ref:`custom Guzzle client ` instead. +:ref:`configure the HTTP adapter ` to use a +custom HTTP client instead. diff --git a/doc/index.rst b/doc/index.rst index e6e210bc..ac200460 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -3,6 +3,11 @@ FOSHttpCache This is the documentation for the `FOSHttpCache library `_. +.. note:: + + This documentation is for the (upcoming) 2.0 of the library. For the stable + 1.* version, please refer to the `stable documentation`_. + This library integrates your PHP applications with HTTP caching proxies such as Varnish, NGINX or the Symfony HttpCache class. Use this library to send invalidation requests from your application to the caching proxy and to test @@ -28,3 +33,5 @@ Contents: testing-your-application contributing + +.. _stable documentation: http://foshttpcache.readthedocs.org/en/stable/ diff --git a/doc/proxy-clients.rst b/doc/proxy-clients.rst index 3dc1b75e..ef05328e 100644 --- a/doc/proxy-clients.rst +++ b/doc/proxy-clients.rst @@ -1,15 +1,71 @@ Caching Proxy Clients ===================== -This library ships with clients for the Varnish, NGINX and Symfony built-in caching proxies. You -can use the clients either wrapped by the :doc:`cache invalidator ` -(recommended), or directly for low-level access to invalidation functionality. +This library ships with clients for the Varnish and NGINX caching servers and +the Symfony built-in HTTP cache. You can use the clients either wrapped by the +:doc:`cache invalidator ` (recommended), or directly for +low-level access to invalidation functionality. Which client you need depends on +which caching solution you use. .. _client setup: Setup ----- +HTTP Adapter Installation +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Because the clients send invalidation requests over HTTP, an `HTTP adapter`_ +must be installed. Which one you need depends on the HTTP client library that +you use in your project. For instance, if you use Guzzle 6 in your project, +install the appropriate adapter: + +.. code-block:: bash + + $ composer require php-http/guzzle6-adapter + +You also need a `PSR-7 message implementation`_. If you use Guzzle 6, Guzzle’s +implementation is already included. If you use another client, install one of +the implementations. Recommended: + +.. code-block:: bash + + $ composer require guzzlehttp/psr7 + +Alternatively: + +.. code-block:: bash + + $ composer require zendframework/zend-diactoros + +.. _HTTP adapter configuration: + +HTTP Adapter Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the proxy client will find the adapter that you have installed +through Composer. But you can also pass the adapter explicitly. This is most +useful when you have created a HTTP client with custom options or middleware +(such as logging):: + + use GuzzleHttp\Client; + + $config = [ + // For instance, custom middlewares + ]; + $yourHttpClient = new Client($config); + +Take your client and create a HTTP adapter from it:: + + use Http\Adapter\Guzzle6HttpAdapter; + + $adapter = new Guzzle6HttpAdapter($client); + +Then pass that adapter to the caching proxy client:: + + $proxyClient = new Varnish($servers, '/baseUrl', $adapter); + // Varnish as example, but also possible for NGINX and Symfony + Varnish Client ~~~~~~~~~~~~~~ @@ -56,9 +112,13 @@ is available as the second parameter:: $nginx = new Nginx($servers, 'my-cool-app.com'); If you have configured NGINX to support purge requests at a separate location, -supply that location to the class as the third parameter:: +call `setPurgeLocation()`:: + + use FOS\HttpCache\ProxyClient\Nginx; + + $nginx = new Nginx($servers, $baseUri); + $nginx->setPurgeLocation('/purge'); - $nginx = new Nginx($servers, 'my-cool-app.com', '/purge'); .. note:: @@ -205,25 +265,5 @@ Varnish client:: Make sure to add any headers that you want to ban on to your :doc:`proxy configuration `. -.. _custom guzzle client: - -Custom Guzzle Client --------------------- - -By default, the proxy clients instantiate a `Guzzle client`_ to communicate -with the caching proxy. If you need to customize the requests, for example to -send a basic authentication header, you can inject a custom Guzzle client:: - - use FOS\HttpCache\ProxyClient\Varnish; - use Guzzle\Http\Client; - - $client = new Client(); - $client->setDefaultOption('auth', array('username', 'password', 'Digest')); - - $servers = array('10.0.0.1'); - $varnish = new Varnish($servers, '/baseUrl', $client); - -The Symfony client accepts a guzzle client as the 3rd parameter as well, NGINX -accepts it as 4th parameter. - -.. _Guzzle client: http://guzzle3.readthedocs.org/ +.. _HTTP Adapter: http://php-http.readthedocs.org/en/latest/ +.. _PSR-7 message implementation: https://packagist.org/providers/psr/http-message-implementation diff --git a/doc/spelling_word_list.txt b/doc/spelling_word_list.txt index 00c2e4cb..8160f3f8 100644 --- a/doc/spelling_word_list.txt +++ b/doc/spelling_word_list.txt @@ -8,6 +8,8 @@ hostnames http invalidator localhost +middleware +middlewares nginx roundtrip symfony diff --git a/doc/testing-your-application.rst b/doc/testing-your-application.rst index d7f77328..e44933f2 100644 --- a/doc/testing-your-application.rst +++ b/doc/testing-your-application.rst @@ -16,7 +16,7 @@ By having your test classes extend one of the test case classes, you get: * an instance of this library’s client that is configured to talk to your reverse proxy server. See reverse proxy specific sections for details; * convenience methods for executing HTTP requests to your application: - ``$this->getHttpClient()`` and ``$this->getResponse()``; + ``$this->getHttpAdapter()`` and ``$this->getResponse()``; * custom assertions ``assertHit`` and ``assertMiss`` for validating a cache hit/miss. diff --git a/src/Exception/ExceptionCollection.php b/src/Exception/ExceptionCollection.php index 44bb313b..4c1d8bbf 100644 --- a/src/Exception/ExceptionCollection.php +++ b/src/Exception/ExceptionCollection.php @@ -15,9 +15,16 @@ * A collection of exceptions that might occur during the flush operation of a * ProxyClientInterface implementation */ -class ExceptionCollection extends \Exception implements \IteratorAggregate, \Countable, HttpCacheExceptionInterface +class ExceptionCollection extends \Exception implements \IteratorAggregate, \Countable, HttpCacheExceptionInterface { private $exceptions = array(); + + public function __construct(array $exceptions = array()) + { + foreach ($exceptions as $exception) { + $this->add($exception); + } + } /** * Add an exception to the collection diff --git a/src/Exception/InvalidUrlException.php b/src/Exception/InvalidUrlException.php index 6a4ba991..cb9b4e8f 100644 --- a/src/Exception/InvalidUrlException.php +++ b/src/Exception/InvalidUrlException.php @@ -20,47 +20,30 @@ class InvalidUrlException extends InvalidArgumentException implements HttpCacheE * @param string $url The invalid URL. * @param string $reason Further explanation why the URL was invalid (optional) * - * @return InvalidUrlException + * @return self */ public static function invalidUrl($url, $reason = null) { $msg = sprintf('URL "%s" is invalid.', $url); if ($reason) { - $msg .= sprintf('Reason: %s', $reason); + $msg .= sprintf(' Reason: %s', $reason); } - return new InvalidUrlException($msg); + return new self($msg); } /** * @param string $server Invalid server * @param array $allowed Allowed URL parts * - * @return InvalidUrlException + * @return self */ public static function invalidUrlParts($server, array $allowed) { - return new InvalidUrlException(sprintf( + return new self(sprintf( 'Server "%s" is invalid. Only %s URL parts are allowed.', $server, implode(', ', $allowed) )); } - - /** - * @param string $url Requested full URL - * @param string $scheme Requested URL scheme - * @param array $allowed Supported URL schemes - * - * @return InvalidUrlException - */ - public static function invalidUrlScheme($url, $scheme, array $allowed) - { - return new InvalidUrlException(sprintf( - 'Host "%s" with scheme "%s" is invalid. Only schemes "%s" are supported', - $url, - $scheme, - implode(', ', $allowed) - )); - } } diff --git a/src/Exception/ProxyResponseException.php b/src/Exception/ProxyResponseException.php index 8ecc7ead..71d598c9 100644 --- a/src/Exception/ProxyResponseException.php +++ b/src/Exception/ProxyResponseException.php @@ -11,32 +11,26 @@ namespace FOS\HttpCache\Exception; +use Psr\Http\Message\ResponseInterface; + /** * Wrapping an error response from the caching proxy. */ class ProxyResponseException extends \RuntimeException implements HttpCacheExceptionInterface { /** - * @param string $host The host name that was contacted. - * @param string $statusCode The status code of the reply. - * @param string $statusMessage The error message. - * @param string $details Further details about the request that caused the error. - * @param \Exception $previous The exception from the HTTP client. + * @param ResponseInterface $response HTTP response * - * @return ProxyUnreachableException + * @return ProxyResponseException */ - public static function proxyResponse($host, $statusCode, $statusMessage, $details = '', \Exception $previous = null) + public static function proxyResponse(ResponseInterface $response) { $message = sprintf( - '%s error response "%s" from caching proxy at %s', - $statusCode, - $statusMessage, - $host + '%s error response "%s" from caching proxy', + $response->getStatusCode(), + $response->getReasonPhrase() ); - if ($details) { - $message .= ". $details"; - } - return new ProxyResponseException($message, $statusCode, $previous); + return new ProxyResponseException($message, 0); } } diff --git a/src/Exception/ProxyUnreachableException.php b/src/Exception/ProxyUnreachableException.php index 6c735306..eb2c9cfc 100644 --- a/src/Exception/ProxyUnreachableException.php +++ b/src/Exception/ProxyUnreachableException.php @@ -11,6 +11,8 @@ namespace FOS\HttpCache\Exception; +use Http\Adapter\Exception\HttpAdapterException; + /** * Thrown when a request to the reverse caching proxy fails to establish a * connection. @@ -18,28 +20,22 @@ class ProxyUnreachableException extends \RuntimeException implements HttpCacheExceptionInterface { /** - * @param string $host The host name that was contacted. - * @param string $message The error message from the HTTP client. - * @param string $details Further details about the request that caused the error. - * @param \Exception $previous The exception from the HTTP client. + * @param HttpAdapterException $adapterException * * @return ProxyUnreachableException */ - public static function proxyUnreachable($host, $message, $details = '', \Exception $previous = null) + public static function proxyUnreachable(HttpAdapterException $adapterException) { $message = sprintf( 'Request to caching proxy at %s failed with message "%s"', - $host, - $message + $adapterException->getRequest()->getHeaderLine('Host'), + $adapterException->getMessage() ); - if ($details) { - $message .= ". $details"; - } - + return new ProxyUnreachableException( $message, 0, - $previous + $adapterException ); } } diff --git a/src/ProxyClient/AbstractProxyClient.php b/src/ProxyClient/AbstractProxyClient.php index ebf81c33..0b4b788d 100644 --- a/src/ProxyClient/AbstractProxyClient.php +++ b/src/ProxyClient/AbstractProxyClient.php @@ -12,107 +12,57 @@ namespace FOS\HttpCache\ProxyClient; use FOS\HttpCache\Exception\ExceptionCollection; -use FOS\HttpCache\Exception\InvalidUrlException; use FOS\HttpCache\Exception\ProxyResponseException; use FOS\HttpCache\Exception\ProxyUnreachableException; -use Guzzle\Http\Client; -use Guzzle\Http\ClientInterface; -use Guzzle\Http\Exception\CurlException; -use Guzzle\Common\Exception\ExceptionCollection as GuzzleExceptionCollection; -use Guzzle\Http\Exception\RequestException; -use Guzzle\Http\Message\RequestInterface; +use FOS\HttpCache\ProxyClient\Request\InvalidationRequest; +use FOS\HttpCache\ProxyClient\Request\RequestQueue; +use Http\Adapter\Exception\MultiHttpAdapterException; +use Http\Adapter\HttpAdapter; +use Http\Discovery\HttpAdapterDiscovery; +use Psr\Http\Message\ResponseInterface; /** - * Guzzle-based abstract caching proxy client + * Abstract caching proxy client * * @author David de Boer */ abstract class AbstractProxyClient implements ProxyClientInterface { - /** - * IP addresses/hostnames of all caching proxy servers - * - * @var array - */ - private $servers; - /** * HTTP client * - * @var ClientInterface + * @var HttpAdapter */ - private $client; + private $httpAdapter; /** * Request queue * - * @var array|RequestInterface[] + * @var RequestQueue */ - private $queue; + protected $queue; /** * Constructor * - * @param array $servers Caching proxy server hostnames or IP addresses, - * including port if not port 80. - * E.g. array('127.0.0.1:6081') - * @param string $baseUrl Default application hostname, optionally + * @param array $servers Caching proxy server hostnames or IP + * addresses, including port if not port 80. + * E.g. ['127.0.0.1:6081'] + * @param string $baseUri Default application hostname, optionally * including base URL, for purge and refresh * requests (optional). This is required if * you purge and refresh paths instead of * absolute URLs. - * @param ClientInterface $client HTTP client (optional). If no HTTP client - * is supplied, a default one will be - * created. - */ - public function __construct(array $servers, $baseUrl = null, ClientInterface $client = null) - { - $this->client = $client ?: new Client(); - $this->setServers($servers); - $this->setBaseUrl($baseUrl); - } - - /** - * Set caching proxy servers - * - * @param array $servers Caching proxy proxy server hostnames or IP - * addresses, including port if not port 80. - * E.g. array('127.0.0.1:6081') - * - * @throws InvalidUrlException If server is invalid or contains URL - * parts other than scheme, host, port - */ - public function setServers(array $servers) - { - $this->servers = array(); - foreach ($servers as $server) { - $this->servers[] = $this->filterUrl($server, array('scheme', 'host', 'port')); - } - } - - /** - * Set application hostname, optionally including a base URL, for purge and - * refresh requests - * - * @param string $url Your application’s base URL or hostname - */ - public function setBaseUrl($url) - { - if ($url) { - $url = $this->filterUrl($url); - } - - $this->client->setBaseUrl($url); - } - - /** - * Get application base URL - * - * @return string Your application base url - */ - protected function getBaseUrl() - { - $this->client->getBaseUrl(); + * @param HttpAdapter $httpAdapter If no HTTP adapter is supplied, a default + * one will be created. + */ + public function __construct( + array $servers, + $baseUri = null, + HttpAdapter $httpAdapter = null + ) { + $this->httpAdapter = $httpAdapter ?: HttpAdapterDiscovery::find(); + $this->initQueue($servers, $baseUri); } /** @@ -120,196 +70,84 @@ protected function getBaseUrl() */ public function flush() { - $queue = $this->queue; - if (0 === count($queue)) { + if (0 === $this->queue->count()) { return 0; } - $this->queue = array(); - $this->sendRequests($queue); + $queue = clone $this->queue; + $this->queue->clear(); - return count($queue); - } + try { + $responses = $this->httpAdapter->sendRequests($queue->all()); + } catch (MultiHttpAdapterException $e) { + // Handle all networking errors: php-http only throws an exception + // if no response is available. + $collection = new ExceptionCollection(); + foreach ($e->getExceptions() as $exception) { + // php-http only throws an exception if no response is available + if (!$exception->getResponse()) { + // Assume networking error if no response was returned. + $collection->add( + ProxyUnreachableException::proxyUnreachable($exception) + ); + } + } - /** - * Add a request to the queue - * - * @param string $method HTTP method - * @param string $url URL - * @param array $headers HTTP headers - */ - protected function queueRequest($method, $url, array $headers = array()) - { - $signature = $this->getSignature($method, $url, $headers); - if (!isset($this->queue[$signature])) { - $this->queue[$signature] = $this->createRequest($method, $url, $headers); + foreach ($this->handleErrorResponses($e->getResponses()) as $exception) { + $collection->add($exception); + } + + throw $collection; } - } - /** - * Calculate a unique hash for the request, based on all significant information. - * - * @param string $method HTTP method - * @param string $url URL - * @param array $headers HTTP headers - * - * @return string A hash value for this request. - */ - private function getSignature($method, $url, array $headers) - { - ksort($headers); + $exceptions = $this->handleErrorResponses($responses); + if (count($exceptions) > 0) { + throw new ExceptionCollection($exceptions); + } - return md5($method."\n".$url."\n".var_export($headers, true)); + return count($queue); } /** - * Create request + * Add invalidation reqest to the queue * * @param string $method HTTP method - * @param string $url URL + * @param string $url HTTP URL * @param array $headers HTTP headers - * - * @return RequestInterface */ - protected function createRequest($method, $url, array $headers = array()) + protected function queueRequest($method, $url, array $headers = []) { - return $this->client->createRequest($method, $url, $headers); + $this->queue->add(new InvalidationRequest($method, $url, $headers)); } /** - * Sends all requests to each caching proxy server - * - * Requests are sent in parallel to minimise impact on performance. + * Initialize the request queue * - * @param RequestInterface[] $requests Requests - * - * @throws ExceptionCollection + * @param array $servers + * @param string $baseUri */ - private function sendRequests(array $requests) + protected function initQueue(array $servers, $baseUri) { - $allRequests = array(); - - foreach ($requests as $request) { - $headers = $request->getHeaders()->toArray(); - // Force to re-create Host header if empty, as Apache chokes on this. See #128 for discussion. - if (empty($headers['Host'])) { - unset($headers['Host']); - } - foreach ($this->servers as $server) { - $proxyRequest = $this->createRequest( - $request->getMethod(), - $server.$request->getResource(), - $headers - ); - $allRequests[] = $proxyRequest; - } - } - - try { - $this->client->send($allRequests); - } catch (GuzzleExceptionCollection $e) { - $this->handleException($e); - } + $this->queue = new RequestQueue($servers, $baseUri); } /** - * Handle request exception - * - * @param GuzzleExceptionCollection $exceptions + * @param ResponseInterface[] $responses * - * @throws ExceptionCollection + * @return ProxyResponseException[] */ - protected function handleException(GuzzleExceptionCollection $exceptions) + private function handleErrorResponses(array $responses) { - $collection = new ExceptionCollection(); + $exceptions = []; - foreach ($exceptions as $exception) { - if ($exception instanceof CurlException) { - // Caching proxy unreachable - $e = ProxyUnreachableException::proxyUnreachable( - $exception->getRequest()->getHost(), - $exception->getMessage(), - $exception->getRequest()->getRawHeaders(), - $exception - ); - } elseif ($exception instanceof RequestException) { - // Other error - $e = ProxyResponseException::proxyResponse( - $exception->getRequest()->getHost(), - $exception->getCode(), - $exception->getMessage(), - $exception->getRequest()->getRawHeaders(), - $exception - ); - } else { - // Unexpected exception type - $e = $exception; + foreach ($responses as $response) { + if ($response->getStatusCode() >= 400 + && $response->getStatusCode() < 600 + ) { + $exceptions[] = ProxyResponseException::proxyResponse($response); } - - $collection->add($e); } - throw $collection; + return $exceptions; } - - /** - * Filter a URL - * - * Prefix the URL with "http://" if it has no scheme, then check the URL - * for validity. You can specify what parts of the URL are allowed. - * - * @param string $url - * @param string[] $allowedParts Array of allowed URL parts (optional) - * - * @throws InvalidUrlException If URL is invalid, the scheme is not http or - * contains parts that are not expected. - * - * @return string The URL (with default scheme if there was no scheme) - */ - protected function filterUrl($url, array $allowedParts = array()) - { - // parse_url doesn’t work properly when no scheme is supplied, so - // prefix URL with HTTP scheme if necessary. - if (false === strpos($url, '://')) { - $url = sprintf('%s://%s', $this->getDefaultScheme(), $url); - } - - if (!$parts = parse_url($url)) { - throw InvalidUrlException::invalidUrl($url); - } - if (empty($parts['scheme'])) { - throw InvalidUrlException::invalidUrl($url, 'empty scheme'); - } - - if (!in_array(strtolower($parts['scheme']), $this->getAllowedSchemes())) { - throw InvalidUrlException::invalidUrlScheme($url, $parts['scheme'], $this->getAllowedSchemes()); - } - - if (count($allowedParts) > 0) { - $diff = array_diff(array_keys($parts), $allowedParts); - if (count($diff) > 0) { - throw InvalidUrlException::invalidUrlParts($url, $allowedParts); - } - } - - return $url; - } - - /** - * Get default scheme - * - * @return string - */ - protected function getDefaultScheme() - { - return 'http'; - } - - /** - * Get schemes allowed by caching proxy - * - * @return string[] Array of schemes allowed by caching proxy, e.g. 'http' - * or 'https' - */ - abstract protected function getAllowedSchemes(); } diff --git a/src/ProxyClient/Nginx.php b/src/ProxyClient/Nginx.php index dcb9e66d..c97355fd 100644 --- a/src/ProxyClient/Nginx.php +++ b/src/ProxyClient/Nginx.php @@ -13,7 +13,6 @@ use FOS\HttpCache\ProxyClient\Invalidation\PurgeInterface; use FOS\HttpCache\ProxyClient\Invalidation\RefreshInterface; -use Guzzle\Http\ClientInterface; /** * NGINX HTTP cache invalidator. @@ -35,37 +34,21 @@ class Nginx extends AbstractProxyClient implements PurgeInterface, RefreshInterf private $purgeLocation; /** - * {@inheritdoc} + * Set path that triggers purge * - * @param array $servers Caching proxy server hostnames or IP addresses, - * including port if not port 80. - * E.g. array('127.0.0.1:6081') - * @param string $baseUrl Default application hostname, optionally - * including base URL, for purge and refresh - * requests (optional). This is required - * if you purge relative URLs and the domain - * is not part of your `proxy_cache_key` - * @param string $purgeLocation Path that triggers purge (optional). - * @param ClientInterface $client HTTP client (optional). If no HTTP client - * is supplied, a default one will be - * created. + * @param string $purgeLocation */ - public function __construct( - array $servers, - $baseUrl = null, - $purgeLocation = '', - ClientInterface $client = null - ) { + public function setPurgeLocation($purgeLocation = '') + { $this->purgeLocation = (string) $purgeLocation; - parent::__construct($servers, $baseUrl, $client); } /** * {@inheritdoc} */ - public function refresh($url, array $headers = array()) + public function refresh($url, array $headers = []) { - $headers = array_merge($headers, array(self::HTTP_HEADER_REFRESH => '1')); + $headers = array_merge($headers, [self::HTTP_HEADER_REFRESH => '1']); $this->queueRequest(self::HTTP_METHOD_REFRESH, $url, $headers); return $this; @@ -74,7 +57,7 @@ public function refresh($url, array $headers = array()) /** * {@inheritdoc} */ - public function purge($url, array $headers = array()) + public function purge($url, array $headers = []) { $purgeUrl = $this->buildPurgeUrl($url); $this->queueRequest(self::HTTP_METHOD_PURGE, $purgeUrl, $headers); @@ -82,14 +65,6 @@ public function purge($url, array $headers = array()) return $this; } - /** - * {@inheritdoc} - */ - protected function getAllowedSchemes() - { - return array('http', 'https'); - } - /** * Create the correct URL to purge a resource * @@ -109,7 +84,7 @@ private function buildPurgeUrl($url) $pathStartAt = strpos($url, $urlParts['path']); $purgeUrl = substr($url, 0, $pathStartAt).$this->purgeLocation.substr($url, $pathStartAt); } else { - $purgeUrl = $this->getBaseUrl().$this->purgeLocation.$url; + $purgeUrl = $this->purgeLocation.$url; } return $purgeUrl; diff --git a/src/ProxyClient/Request/InvalidationRequest.php b/src/ProxyClient/Request/InvalidationRequest.php new file mode 100644 index 00000000..7f09c60a --- /dev/null +++ b/src/ProxyClient/Request/InvalidationRequest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\HttpCache\ProxyClient\Request; + +use Http\Discovery\MessageFactoryDiscovery; +use Http\Message\RequestDecorator; +use Psr\Http\Message\RequestInterface; + +/** + * An invalidation instruction + */ +class InvalidationRequest implements RequestInterface +{ + use RequestDecorator; + + public function __construct($method, $uri, array $headers = []) + { + $this->message = MessageFactoryDiscovery::find()->createRequest( + $method, + $uri, + '1.1', + $headers + ); + } + + /** + * Get unique request signature + * + * This is used for removing duplicate requests from the queue. + * + * @return string + */ + public function getSignature() + { + $headers = $this->getHeaders(); + ksort($headers); + + return md5($this->getMethod(). "\n" . $this->getUri(). "\n" . var_export($headers, true)); + } +} diff --git a/src/ProxyClient/Request/RequestQueue.php b/src/ProxyClient/Request/RequestQueue.php new file mode 100644 index 00000000..3a5b5471 --- /dev/null +++ b/src/ProxyClient/Request/RequestQueue.php @@ -0,0 +1,221 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\HttpCache\ProxyClient\Request; + +use FOS\HttpCache\Exception\InvalidUrlException; +use Http\Discovery\UriFactoryDiscovery; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UriInterface; + +/** + * Stores each invalidation request and replicates it over all HTTP cache servers + * + * @author David de Boer + */ +class RequestQueue implements \Countable +{ + /** + * HTTP cache servers + * + * @var UriInterface[] + */ + private $servers = []; + + /** + * Application base URI + * + * @var UriInterface | null + */ + private $baseUri; + + /** + * @var InvalidationRequest[] + */ + private $queue = []; + + /** + * Constructor + * + * @param array $servers + * @param null $baseUri + */ + public function __construct( + array $servers, + $baseUri = null + ) { + $this->setServers($servers); + $this->setBaseUri($baseUri); + } + + /** + * Add invalidation request + * + * @param InvalidationRequest $request + */ + public function add(InvalidationRequest $request) + { + $signature = $request->getSignature(); + + if (!isset($this->queue[$signature])) { + $this->queue[$signature] = $request; + } + } + + /** + * Clear request queue + */ + public function clear() + { + $this->queue = []; + } + + public function count() + { + return count($this->queue); + } + + /** + * Get each invalidation request replicated over all HTTP caching servers + * + * @return RequestInterface[] + */ + public function all() + { + $requests = []; + foreach ($this->queue as $request) { + $uri = $request->getUri(); + + // If a base URI is configured, try to make partial invalidation + // requests complete. + if ($this->baseUri) { + if ($uri->getHost()) { + // Absolute URI: does it already have a scheme? + if (!$uri->getScheme() && $this->baseUri->getScheme() !== '') { + $uri = $uri->withScheme($this->baseUri->getScheme()); + } + } else { + // Relative URI + if ($this->baseUri->getHost() !== '') { + $uri = $uri->withHost($this->baseUri->getHost()); + } + + if ($this->baseUri->getPort()) { + $uri = $uri->withPort($this->baseUri->getPort()); + } + + // Base path + if ($this->baseUri->getPath() !== '') { + $path = $this->baseUri->getPath() . '/' . ltrim($uri->getPath(), '/'); + $uri = $uri->withPath($path); + } + } + } + + // Close connections to make sure invalidation (PURGE/BAN) requests + // will not interfere with content (GET) requests. + $request = $request->withUri($uri)->withHeader('Connection', 'Close'); + + // Create a request to each caching proxy server + foreach ($this->servers as $server) { + $requests[] = $request->withUri( + $uri + ->withScheme($server->getScheme()) + ->withHost($server->getHost()) + ->withPort($server->getPort()) + , + true // Preserve application Host header + ); + } + } + + return $requests; + } + + /** + * Set caching proxy servers + * + * @param array $servers Caching proxy proxy server hostnames or IP + * addresses, including port if not port 80. + * E.g. ['127.0.0.1:6081'] + * + * @throws InvalidUrlException If server is invalid or contains URL + * parts other than scheme, host, port + */ + public function setServers(array $servers) + { + $this->servers = []; + foreach ($servers as $server) { + $this->servers[] = $this->filterUri($server, ['scheme', 'host', 'port']); + } + } + + /** + * Set application base URI that will be prefixed to relative purge and + * refresh requests + * + * @param string $uriString Your application’s base URI + */ + private function setBaseUri($uriString = null) + { + if (null === $uriString) { + $this->baseUri = null; + + return; + } + + $this->baseUri = $this->filterUri($uriString); + } + + /** + * Filter a URL + * + * Prefix the URL with "http://" if it has no scheme, then check the URL + * for validity. You can specify what parts of the URL are allowed. + * + * @param string $uriString + * @param string[] $allowedParts Array of allowed URL parts (optional) + * + * @throws InvalidUrlException If URL is invalid, the scheme is not http or + * contains parts that are not expected. + * + * @return UriInterface Filtered URI (with default scheme if there was no scheme) + */ + private function filterUri($uriString, array $allowedParts = []) + { + // Creating a PSR-7 URI without scheme (with parse_url) results in the + // original hostname to be seen as path. So first add a scheme if none + // is given. + if (false === strpos($uriString, '://')) { + $uriString = sprintf('%s://%s', 'http', $uriString); + } + + try { + $uri = UriFactoryDiscovery::find()->createUri($uriString); + } catch (\InvalidArgumentException $e) { + throw InvalidUrlException::invalidUrl($uriString); + } + + if (!$uri->getScheme()) { + throw InvalidUrlException::invalidUrl($uriString, 'empty scheme'); + } + + if (count($allowedParts) > 0) { + $parts = parse_url((string) $uri); + $diff = array_diff(array_keys($parts), $allowedParts); + if (count($diff) > 0) { + throw InvalidUrlException::invalidUrlParts($uriString, $allowedParts); + } + } + + return $uri; + } +} diff --git a/src/ProxyClient/Symfony.php b/src/ProxyClient/Symfony.php index d9125d8a..17e3c76f 100644 --- a/src/ProxyClient/Symfony.php +++ b/src/ProxyClient/Symfony.php @@ -11,13 +11,10 @@ namespace FOS\HttpCache\ProxyClient; -use FOS\HttpCache\Exception\InvalidArgumentException; -use FOS\HttpCache\Exception\MissingHostException; -use FOS\HttpCache\ProxyClient\Invalidation\BanInterface; use FOS\HttpCache\ProxyClient\Invalidation\PurgeInterface; use FOS\HttpCache\ProxyClient\Invalidation\RefreshInterface; use FOS\HttpCache\SymfonyCache\PurgeSubscriber; -use Guzzle\Http\ClientInterface; +use Http\Adapter\HttpAdapter; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -46,9 +43,13 @@ class Symfony extends AbstractProxyClient implements PurgeInterface, RefreshInte * * @param array $options The purge_method that should be used. */ - public function __construct(array $servers, $baseUrl = null, ClientInterface $client = null, array $options = array()) - { - parent::__construct($servers, $baseUrl, $client); + public function __construct( + array $servers, + $baseUrl = null, + HttpAdapter $httpAdapter = null, + $options = [] + ) { + parent::__construct($servers, $baseUrl, $httpAdapter); $resolver = new OptionsResolver(); $resolver->setDefaults(array( @@ -78,12 +79,4 @@ public function refresh($url, array $headers = array()) return $this; } - - /** - * {@inheritdoc} - */ - protected function getAllowedSchemes() - { - return array('http', 'https'); - } } diff --git a/src/ProxyClient/Varnish.php b/src/ProxyClient/Varnish.php index 8b95b3ab..327adcc6 100644 --- a/src/ProxyClient/Varnish.php +++ b/src/ProxyClient/Varnish.php @@ -16,6 +16,9 @@ use FOS\HttpCache\ProxyClient\Invalidation\BanInterface; use FOS\HttpCache\ProxyClient\Invalidation\PurgeInterface; use FOS\HttpCache\ProxyClient\Invalidation\RefreshInterface; +use FOS\HttpCache\ProxyClient\Request\InvalidationRequest; +use FOS\HttpCache\ProxyClient\Request\RequestQueue; +use Http\Adapter\HttpAdapter; /** * Varnish HTTP cache invalidator. @@ -36,11 +39,30 @@ class Varnish extends AbstractProxyClient implements BanInterface, PurgeInterfac * * @var array */ - private $defaultBanHeaders = array( + private $defaultBanHeaders = [ self::HTTP_HEADER_HOST => self::REGEX_MATCH_ALL, self::HTTP_HEADER_URL => self::REGEX_MATCH_ALL, self::HTTP_HEADER_CONTENT_TYPE => self::REGEX_MATCH_ALL - ); + ]; + + /** + * Has a base URI been set? + * + * @var bool + */ + private $baseUriSet; + + /** + * {@inheritdoc} + */ + public function __construct( + array $servers, + $baseUri = null, + HttpAdapter $httpAdapter = null + ) { + parent::__construct($servers, $baseUri, $httpAdapter); + $this->baseUriSet = $baseUri !== null; + } /** * Set the default headers that get merged with the provided headers in self::ban(). @@ -91,9 +113,9 @@ public function banPath($path, $contentType = null, $hosts = null) $hosts = '^('.join('|', $hosts).')$'; } - $headers = array( + $headers = [ self::HTTP_HEADER_URL => $path, - ); + ]; if ($contentType) { $headers[self::HTTP_HEADER_CONTENT_TYPE] = $contentType; @@ -108,7 +130,7 @@ public function banPath($path, $contentType = null, $hosts = null) /** * {@inheritdoc} */ - public function purge($url, array $headers = array()) + public function purge($url, array $headers = []) { $this->queueRequest(self::HTTP_METHOD_PURGE, $url, $headers); @@ -118,9 +140,9 @@ public function purge($url, array $headers = array()) /** * {@inheritdoc} */ - public function refresh($url, array $headers = array()) + public function refresh($url, array $headers = []) { - $headers = array_merge($headers, array('Cache-Control' => 'no-cache')); + $headers = array_merge($headers, ['Cache-Control' => 'no-cache']); $this->queueRequest(self::HTTP_METHOD_REFRESH, $url, $headers); return $this; @@ -133,26 +155,17 @@ public function refresh($url, array $headers = array()) * refresh and no base URL is set * */ - protected function createRequest($method, $url, array $headers = array()) + protected function queueRequest($method, $url, array $headers = []) { - $request = parent::createRequest($method, $url, $headers); + $request = new InvalidationRequest($method, $url, $headers); - // For purge and refresh, add a host header to the request if it hasn't - // been set if (self::HTTP_METHOD_BAN !== $method - && '' == $request->getHeader('Host') + && !$this->baseUriSet + && !$request->getHeaderLine('Host') ) { throw MissingHostException::missingHost($url); } - return $request; - } - - /** - * {@inheritdoc} - */ - protected function getAllowedSchemes() - { - return array('http'); + parent::queueRequest($method, $url, $headers); } } diff --git a/src/Test/HttpClient/MockHttpAdapter.php b/src/Test/HttpClient/MockHttpAdapter.php new file mode 100644 index 00000000..4d88658a --- /dev/null +++ b/src/Test/HttpClient/MockHttpAdapter.php @@ -0,0 +1,96 @@ +requests[] = $request; + + if ($this->exception) { + throw $this->exception; + } + + if (count($this->responses) > 0) { + return array_shift($this->responses); + } + + return MessageFactoryDiscovery::find()->createResponse(); + + } + + /** + * {@inheritdoc} + */ + public function sendRequests(array $requests, array $options = []) + { + $responses = []; + $exceptions = new MultiHttpAdapterException(); + + foreach ($requests as $request) { + try { + $responses[] = $this->sendRequest($request); + } catch (\Exception $e) { + $exceptions->addException($e); + } + } + + if ($exceptions->hasExceptions()) { + throw $exceptions; + } + + return $responses; + } + + public function setException(\Exception $exception) + { + $this->exception = $exception; + } + + public function addResponse(ResponseInterface $response) + { + $this->responses[] = $response; + } + + /** + * {@inheritdoc} + * + * @return string The name. + */ + public function getName() + { + return 'mock'; + } + + public function getRequests() + { + return $this->requests; + } + + public function clear() + { + $this->exception = null; + } +} diff --git a/src/Test/NginxTestCase.php b/src/Test/NginxTestCase.php index 2eb4023f..557baaed 100644 --- a/src/Test/NginxTestCase.php +++ b/src/Test/NginxTestCase.php @@ -129,9 +129,10 @@ protected function getProxyClient($purgeLocation = '') if (null === $this->proxyClient) { $this->proxyClient = new Nginx( array('http://127.0.0.1:' . $this->getCachingProxyPort()), - $this->getHostName(), - $purgeLocation + $this->getHostName() ); + + $this->proxyClient->setPurgeLocation($purgeLocation); } return $this->proxyClient; diff --git a/src/Test/PHPUnit/AbstractCacheConstraint.php b/src/Test/PHPUnit/AbstractCacheConstraint.php index 61d027d4..b3496de8 100644 --- a/src/Test/PHPUnit/AbstractCacheConstraint.php +++ b/src/Test/PHPUnit/AbstractCacheConstraint.php @@ -54,7 +54,7 @@ protected function matches($other) ); } - return $this->getValue() === (string) $other->getHeader($this->header); + return $this->getValue() === (string) $other->getHeaderLine($this->header); } /** @@ -62,6 +62,10 @@ protected function matches($other) */ protected function failureDescription($other) { - return (string) $other . ' ' . $this->toString(); + return sprintf( + 'response (with status code %s) %s', + $other->getStatusCode(), + $this->toString() + ); } } diff --git a/src/Test/ProxyTestCase.php b/src/Test/ProxyTestCase.php index 073ed5a6..07cc9cb5 100644 --- a/src/Test/ProxyTestCase.php +++ b/src/Test/ProxyTestCase.php @@ -13,8 +13,13 @@ use FOS\HttpCache\Test\PHPUnit\IsCacheHitConstraint; use FOS\HttpCache\Test\PHPUnit\IsCacheMissConstraint; -use Guzzle\Http\Client; -use Guzzle\Http\Message\Response; +use Http\Adapter\HttpAdapter; +use Http\Discovery\HttpAdapterDiscovery; +use Http\Discovery\MessageFactoryDiscovery; +use Http\Discovery\UriFactoryDiscovery; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriInterface; /** * Abstract caching proxy test case @@ -23,19 +28,19 @@ abstract class ProxyTestCase extends \PHPUnit_Framework_TestCase { /** - * A Guzzle HTTP client. + * HTTP adapter for requests to the application * - * @var Client + * @var HttpAdapter */ - protected $httpClient; + protected $httpAdapter; /** * Assert a cache miss * - * @param Response $response - * @param string $message Test failure message (optional) + * @param ResponseInterface $response + * @param string $message Test failure message (optional) */ - public function assertMiss(Response $response, $message = null) + public function assertMiss(ResponseInterface $response, $message = null) { self::assertThat($response, self::isCacheMiss(), $message); } @@ -43,10 +48,10 @@ public function assertMiss(Response $response, $message = null) /** * Assert a cache hit * - * @param Response $response - * @param string $message Test failure message (optional) + * @param ResponseInterface $response + * @param string $message Test failure message (optional) */ - public function assertHit(Response $response, $message = null) + public function assertHit(ResponseInterface $response, $message = null) { self::assertThat($response, self::isCacheHit(), $message); } @@ -64,32 +69,34 @@ public static function isCacheMiss() /** * Get HTTP response from your application * - * @param string $url - * @param array $headers - * @param array $options + * @param string $uri HTTP URI + * @param array $headers HTTP headers + * @param string $method HTTP method * - * @return Response + * @return ResponseInterface */ - public function getResponse($url, array $headers = array(), $options = array()) + public function getResponse($uri, array $headers = [], $method = 'GET') { - return $this->getHttpClient()->get($url, $headers, $options)->send(); + // Close connections to make sure invalidation (PURGE/BAN) requests will + // not interfere with content (GET) requests. + $headers['Connection'] = 'Close'; + $request = $this->createRequest($method, $uri, $headers); + + return $this->getHttpAdapter()->sendRequest($request); } /** - * Get HTTP client for your application + * Get HTTP adapter for your application * - * @return Client + * @return HttpAdapter */ - public function getHttpClient() + protected function getHttpAdapter() { - if (null === $this->httpClient) { - $this->httpClient = new Client( - 'http://' . $this->getHostName() . ':' . $this->getCachingProxyPort(), - array('curl.options' => array(CURLOPT_FORBID_REUSE => true)) - ); + if ($this->httpAdapter === null) { + $this->httpAdapter = HttpAdapterDiscovery::find(); } - return $this->httpClient; + return $this->httpAdapter; } /** @@ -124,6 +131,52 @@ protected function getHostName() return WEB_SERVER_HOSTNAME; } + /** + * Create a request + * + * @param string $method + * @param string $uri + * @param array $headers + * + * @return RequestInterface + * @throws \Exception + */ + protected function createRequest($method, $uri, $headers) + { + $uri = $this->createUri($uri); + if ($uri->getHost() === '') { + // Add base URI host + $uri = $uri->withHost($this->getHostName()); + } + + if (!$uri->getPort()) { + $uri = $uri->withPort($this->getCachingProxyPort()); + } + + if ($uri->getScheme() === '') { + $uri = $uri->withScheme('http'); + } + + return MessageFactoryDiscovery::find()->createRequest( + $method, + $uri, + '1.1', + $headers + ); + } + + /** + * Create PSR-7 URI object from URI string + * + * @param string $uriString + * + * @return UriInterface + */ + protected function createUri($uriString) + { + return UriFactoryDiscovery::find()->createUri($uriString); + } + /** * Get proxy server * diff --git a/tests/Functional/NginxProxyClientTest.php b/tests/Functional/NginxProxyClientTest.php index a760e55a..e0734439 100644 --- a/tests/Functional/NginxProxyClientTest.php +++ b/tests/Functional/NginxProxyClientTest.php @@ -74,7 +74,10 @@ public function testRefresh() $nginx->refresh('/cache.php')->flush(); usleep(1000); $refreshed = $this->getResponse('/cache.php'); - $this->assertGreaterThan((float) $response->getBody(true), (float) $refreshed->getBody(true)); + $this->assertGreaterThan( + (float)(string) $response->getBody(), + (float)(string) $refreshed->getBody() + ); } public function testRefreshPath() @@ -87,6 +90,10 @@ public function testRefreshPath() $nginx->refresh('/cache.php')->flush(); usleep(1000); $refreshed = $this->getResponse('/cache.php'); - $this->assertGreaterThan((float) $response->getBody(true), (float) $refreshed->getBody(true)); + + $this->assertGreaterThan( + (float)(string) $response->getBody(), + (float)(string) $refreshed->getBody() + ); } } diff --git a/tests/Functional/SymfonyProxyClientTest.php b/tests/Functional/SymfonyProxyClientTest.php index d4561a20..c8e09008 100644 --- a/tests/Functional/SymfonyProxyClientTest.php +++ b/tests/Functional/SymfonyProxyClientTest.php @@ -36,11 +36,11 @@ public function testPurgeContentType() $response = $this->getResponse('/symfony.php/negotiation', $json); $this->assertMiss($response); - $this->assertEquals('application/json', $response->getContentType()); + $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); $this->assertHit($this->getResponse('/symfony.php/negotiation', $json)); $response = $this->getResponse('/symfony.php/negotiation', $html); - $this->assertContains('text/html', $response->getContentType()); + $this->assertContains('text/html', $response->getHeaderLine('Content-Type')); $this->assertMiss($response); $this->assertHit($this->getResponse('/symfony.php/negotiation', $html)); @@ -69,7 +69,11 @@ public function testRefresh() $this->getProxyClient()->refresh('/symfony.php/cache')->flush(); usleep(100); $refreshed = $this->getResponse('/symfony.php/cache'); - $this->assertGreaterThan((float) $response->getBody(true), (float) $refreshed->getBody(true)); + + $originalTimestamp = (float)(string) $response->getBody(); + $refreshedTimestamp = (float)(string) $refreshed->getBody(); + + $this->assertGreaterThan($originalTimestamp, $refreshedTimestamp); } public function testRefreshContentType() diff --git a/tests/Functional/Varnish/UserContextFailureTest.php b/tests/Functional/Varnish/UserContextFailureTest.php index 085f7bde..563f4f41 100644 --- a/tests/Functional/Varnish/UserContextFailureTest.php +++ b/tests/Functional/Varnish/UserContextFailureTest.php @@ -12,8 +12,6 @@ namespace FOS\HttpCache\Tests\Functional\Varnish; use FOS\HttpCache\Test\VarnishTestCase; -use Guzzle\Http\Exception\ClientErrorResponseException; -use Guzzle\Http\Exception\ServerErrorResponseException; /** * Test edge conditions and attacks. @@ -37,18 +35,15 @@ public function setUp() */ public function testUserContextNoExposeHash() { - try { - $response = $this->getResponse( - '/user_context_hash_nocache.php', - array('accept' => 'application/vnd.fos.user-context-hash'), - array('cookies' => array('miam')) - ); - - $this->fail("Request should have failed with a 400 response.\n\n" . $response->getRawHeaders() . "\n" . $response->getBody(true)); - } catch (ClientErrorResponseException $e) { - $this->assertEquals(400, $e->getResponse()->getStatusCode()); - $this->assertFalse($e->getResponse()->hasHeader('X-User-Context-Hash')); - } + $response = $this->getResponse( + '/user_context_hash_nocache.php', + [ + 'Accept' => 'application/vnd.fos.user-context-hash', + 'Cookie' => ['0=miam'], + ] + ); + $this->assertEquals(400, $response->getStatusCode()); + $this->assertFalse($response->hasHeader('X-User-Context-Hash')); } /** @@ -56,17 +51,14 @@ public function testUserContextNoExposeHash() */ public function testUserContextNoForgedHash() { - try { - $response = $this->getResponse( - '/user_context_hash_nocache.php', - array('X-User-Context-Hash' => 'miam'), - array('cookies' => array('miam')) - ); - - $this->fail("Request should have failed with a 400 response.\n\n" . $response->getRawHeaders() . "\n" . $response->getBody(true)); - } catch (ClientErrorResponseException $e) { - $this->assertEquals(400, $e->getResponse()->getStatusCode()); - } + $response = $this->getResponse( + '/user_context_hash_nocache.php', + [ + 'X-User-Context-Hash' => 'miam', + 'Cookie' => ['0=miam'], + ] + ); + $this->assertEquals(400, $response->getStatusCode()); } /** @@ -74,28 +66,25 @@ public function testUserContextNoForgedHash() */ public function testUserContextNotUsed() { - //First request in get - $this->getResponse('/user_context.php', array(), array('cookies' => array('foo'))); - - //Second request in head or post - $postResponse = $this->getHttpClient() - ->post('/user_context.php', array(), null, array('cookies' => array('foo'))) - ->send(); - - $this->assertEquals('POST', $postResponse->getBody(true)); - $this->assertEquals('MISS', $postResponse->getHeader('X-HashCache')); + // First request in GET + $this->getResponse('/user_context.php', ['Cookie' => '0=foo']); + + // Second request in HEAD or POST + $postResponse = $this->getResponse( + '/user_context.php', + ['Cookie' => '0=foo'], + 'POST' + ); + + $this->assertEquals('POST', $postResponse->getBody()); + $this->assertEquals('MISS', $postResponse->getHeaderLine('X-HashCache')); $this->assertMiss($postResponse); } public function testHashRequestFailure() { - try { - $response = $this->getResponse('/user_context.php', array(), array('cookies' => array('foo'))); - - $this->fail("Request should have failed with a 500 response.\n\n" . $response->getRawHeaders() . "\n" . $response->getBody(true)); - } catch (ServerErrorResponseException $e) { - $this->assertEquals(503, $e->getResponse()->getStatusCode()); - } + $response = $this->getResponse('/user_context.php', ['Cookie' => '0=foo']); + $this->assertEquals(503, $response->getStatusCode()); } protected function getConfigFile() diff --git a/tests/Functional/Varnish/UserContextTestCase.php b/tests/Functional/Varnish/UserContextTestCase.php index 8f5d7e94..87bae085 100644 --- a/tests/Functional/Varnish/UserContextTestCase.php +++ b/tests/Functional/Varnish/UserContextTestCase.php @@ -12,7 +12,6 @@ namespace FOS\HttpCache\Tests\Functional\Varnish; use FOS\HttpCache\Test\VarnishTestCase; -use Guzzle\Http\Exception\ClientErrorResponseException; /** * @group webserver @@ -33,38 +32,32 @@ abstract protected function assertContextCache($hashCache); */ public function testUserContextHash() { - $response1 = $this->getResponse('/user_context.php', array(), array('cookies' => array('foo'))); - $this->assertEquals('foo', $response1->getBody(true)); - $this->assertEquals('MISS', $response1->getHeader('X-HashCache')); + $response1 = $this->getResponse('/user_context.php', ['Cookie' => ['0=foo']]); + $this->assertEquals('foo', (string) $response1->getBody()); + $this->assertEquals('MISS', $response1->getHeaderLine('X-HashCache')); - $response2 = $this->getResponse('/user_context.php', array(), array('cookies' => array('bar'))); - $this->assertEquals('bar', $response2->getBody(true)); - $this->assertEquals('MISS', $response2->getHeader('X-HashCache')); + $response2 = $this->getResponse('/user_context.php', ['Cookie' => ['0=bar']]); + $this->assertEquals('bar', (string) $response2->getBody()); + $this->assertEquals('MISS', $response2->getHeaderLine('X-HashCache')); - $cachedResponse1 = $this->getResponse('/user_context.php', array(), array('cookies' => array('foo'))); - $this->assertEquals('foo', $cachedResponse1->getBody(true)); - $this->assertContextCache($cachedResponse1->getHeader('X-HashCache')); + $cachedResponse1 = $this->getResponse('/user_context.php', ['Cookie' => ['0=foo']]); + $this->assertEquals('foo', (string) $cachedResponse1->getBody()); + $this->assertContextCache($cachedResponse1->getHeaderLine('X-HashCache')); $this->assertHit($cachedResponse1); - $cachedResponse2 = $this->getResponse('/user_context.php', array(), array('cookies' => array('bar'))); - $this->assertEquals('bar', $cachedResponse2->getBody(true)); - $this->assertContextCache($cachedResponse2->getHeader('X-HashCache')); + $cachedResponse2 = $this->getResponse('/user_context.php', ['Cookie' => ['0=bar']]); + $this->assertEquals('bar', $cachedResponse2->getBody()); + $this->assertContextCache($cachedResponse2->getHeaderLine('X-HashCache')); $this->assertHit($cachedResponse2); - $headResponse1 = $this->getHttpClient() - ->head('/user_context.php', array(), array('cookies' => array('foo'))) - ->send(); - - $this->assertEquals('foo', $headResponse1->getHeader('X-HashTest')); - $this->assertContextCache($headResponse1->getHeader('X-HashCache')); + $headResponse1 = $this->getResponse('/user_context.php', ['Cookie' => ['0=foo'], [], 'HEAD']); + $this->assertEquals('foo', $headResponse1->getHeaderLine('X-HashTest')); + $this->assertContextCache($headResponse1->getHeaderLine('X-HashCache')); $this->assertHit($headResponse1); - $headResponse2 = $this->getHttpClient() - ->head('/user_context.php', array(), array('cookies' => array('bar'))) - ->send(); - - $this->assertEquals('bar', $headResponse2->getHeader('X-HashTest')); - $this->assertContextCache($headResponse2->getHeader('X-HashCache')); + $headResponse2 = $this->getResponse('/user_context.php', ['Cookie' => ['0=bar'], [], 'HEAD']); + $this->assertEquals('bar', $headResponse2->getHeaderLine('X-HashTest')); + $this->assertContextCache($headResponse2->getHeaderLine('X-HashCache')); $this->assertHit($headResponse2); } @@ -74,52 +67,45 @@ public function testUserContextHash() public function testUserContextNoAuth() { $response1 = $this->getResponse('/user_context_anon.php'); - $this->assertEquals('anonymous', $response1->getBody(true)); - $this->assertEquals('MISS', $response1->getHeader('X-HashCache')); + $this->assertEquals('anonymous', $response1->getBody()); + $this->assertEquals('MISS', $response1->getHeaderLine('X-HashCache')); - $response1 = $this->getResponse('/user_context_anon.php', array(), array('cookies' => array('foo'))); - $this->assertEquals('foo', $response1->getBody(true)); - $this->assertEquals('MISS', $response1->getHeader('X-HashCache')); + $response1 = $this->getResponse('/user_context_anon.php', ['Cookie' => ['0=foo']]); + $this->assertEquals('foo', (string) $response1->getBody()); + $this->assertEquals('MISS', $response1->getHeaderLine('X-HashCache')); $cachedResponse1 = $this->getResponse('/user_context_anon.php'); - $this->assertEquals('anonymous', $cachedResponse1->getBody(true)); + $this->assertEquals('anonymous', (string) $cachedResponse1->getBody()); $this->assertHit($cachedResponse1); - $cachedResponse1 = $this->getResponse('/user_context_anon.php', array(), array('cookies' => array('foo'))); - $this->assertEquals('foo', $cachedResponse1->getBody(true)); - $this->assertContextCache($cachedResponse1->getHeader('X-HashCache')); - $this->assertHit($cachedResponse1); + $cachedResponse2 = $this->getResponse('/user_context_anon.php', ['Cookie' => ['0=foo']]); + $this->assertEquals('foo', (string) $cachedResponse2->getBody()); + $this->assertContextCache($cachedResponse2->getHeaderLine('X-HashCache')); + $this->assertHit($cachedResponse2); } public function testAcceptHeader() { $response1 = $this->getResponse( '/user_context.php?accept=text/plain', - array('Accept' => 'text/plain'), - array('cookies' => array('foo')) + [ + 'Accept' => 'text/plain', + 'Cookie' => '0=foo', + ] ); - $this->assertEquals('foo', $response1->getBody(true)); - + $this->assertEquals('foo', $response1->getBody()); } public function testUserContextUnauthorized() { - try { - $this->getResponse('/user_context.php', array(), array('cookies' => array('miam'))); - - $this->fail('Request should have failed with a 403 response'); - } catch (ClientErrorResponseException $e) { - $this->assertEquals('MISS', $e->getResponse()->getHeader('X-HashCache')); - $this->assertEquals(403, $e->getResponse()->getStatusCode()); - } - - try { - $this->getResponse('/user_context.php', array(), array('cookies' => array('miam'))); - - $this->fail('Request should have failed with a 403 response'); - } catch (ClientErrorResponseException $e) { - $this->assertContextCache($e->getResponse()->getHeader('X-HashCache')); - $this->assertEquals(403, $e->getResponse()->getStatusCode()); - } + $response = $this->getResponse('/user_context.php', ['Cookie' => ['0=miam']]); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('MISS', $response->getHeaderLine('X-HashCache')); + $this->assertEquals(403, $response->getStatusCode()); + + $response = $this->getResponse('/user_context.php', ['Cookie' => ['0=miam']]); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertContextCache($response->getHeaderLine('X-HashCache')); + $this->assertEquals(403, $response->getStatusCode()); } } diff --git a/tests/Functional/Varnish/VarnishProxyClientTest.php b/tests/Functional/Varnish/VarnishProxyClientTest.php index b6a80e2b..f9607e1e 100644 --- a/tests/Functional/Varnish/VarnishProxyClientTest.php +++ b/tests/Functional/Varnish/VarnishProxyClientTest.php @@ -88,11 +88,11 @@ public function testPurgeContentType() $response = $this->getResponse('/negotation.php', $json); $this->assertMiss($response); - $this->assertEquals('application/json', $response->getContentType()); + $this->assertEquals('application/json', $response->getHeaderLine('Content-Type')); $this->assertHit($this->getResponse('/negotation.php', $json)); $response = $this->getResponse('/negotation.php', $html); - $this->assertContains('text/html', $response->getContentType()); + $this->assertContains('text/html', $response->getHeaderLine('Content-Type')); $this->assertMiss($response); $this->assertHit($this->getResponse('/negotation.php', $html)); @@ -121,7 +121,11 @@ public function testRefresh() $this->getProxyClient()->refresh('/cache.php')->flush(); usleep(1000); $refreshed = $this->getResponse('/cache.php'); - $this->assertGreaterThan((float) $response->getBody(true), (float) $refreshed->getBody(true)); + + $originalTimestamp = (float)(string) $response->getBody(); + $refreshedTimestamp = (float)(string) $refreshed->getBody(); + + $this->assertGreaterThan($originalTimestamp, $refreshedTimestamp); } public function testRefreshContentType() diff --git a/tests/Unit/CacheInvalidatorTest.php b/tests/Unit/CacheInvalidatorTest.php index 876e1c43..9458ca5c 100644 --- a/tests/Unit/CacheInvalidatorTest.php +++ b/tests/Unit/CacheInvalidatorTest.php @@ -18,6 +18,7 @@ use FOS\HttpCache\Exception\ProxyUnreachableException; use FOS\HttpCache\Exception\UnsupportedProxyOperationException; use FOS\HttpCache\ProxyClient\Varnish; +use Http\Adapter\Exception\HttpAdapterException; use \Mockery; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -184,8 +185,19 @@ public function testMethodException() */ public function testProxyClientExceptionsAreLogged() { - $unreachableException = ProxyUnreachableException::proxyUnreachable('http://127.0.0.1', 'Couldn\'t connect to host'); - $responseException = ProxyResponseException::proxyResponse('http://127.0.0.1', 403, 'Forbidden'); + $failedRequest = \Mockery::mock('\Psr\Http\Message\RequestInterface') + ->shouldReceive('getHeaderLine')->with('Host')->andReturn('127.0.0.1') + ->getMock(); + $adapterException = new HttpAdapterException('Couldn\'t connect to host'); + $adapterException->setRequest($failedRequest); + + $unreachableException = ProxyUnreachableException::proxyUnreachable($adapterException); + + $response = \Mockery::mock('\Psr\Http\Message\ResponseInterface') + ->shouldReceive('getStatusCode')->andReturn(403) + ->shouldReceive('getReasonPhrase')->andReturn('Forbidden') + ->getMock(); + $responseException = ProxyResponseException::proxyResponse($response); $exceptions = new ExceptionCollection(); $exceptions->add($unreachableException)->add($responseException); @@ -200,13 +212,15 @@ public function testProxyClientExceptionsAreLogged() ->shouldReceive('log')->once() ->with( 'critical', - 'Request to caching proxy at http://127.0.0.1 failed with message "Couldn\'t connect to host"', - array( - 'exception' => $unreachableException - ) + 'Request to caching proxy at 127.0.0.1 failed with message "Couldn\'t connect to host"', + ['exception' => $unreachableException] ) ->shouldReceive('log')->once() - ->with('critical', '403 error response "Forbidden" from caching proxy at http://127.0.0.1', array('exception' => $responseException)) + ->with( + 'critical', + '403 error response "Forbidden" from caching proxy', + ['exception' => $responseException] + ) ->getMock(); $cacheInvalidator->getEventDispatcher()->addSubscriber(new LogSubscriber($logger)); diff --git a/tests/Unit/ProxyClient/AbstractProxyClientTest.php b/tests/Unit/ProxyClient/AbstractProxyClientTest.php index 8b356729..aea82943 100644 --- a/tests/Unit/ProxyClient/AbstractProxyClientTest.php +++ b/tests/Unit/ProxyClient/AbstractProxyClientTest.php @@ -13,13 +13,10 @@ use FOS\HttpCache\Exception\ExceptionCollection; use FOS\HttpCache\ProxyClient\Varnish; -use Guzzle\Http\Client; -use Guzzle\Http\Exception\CurlException; -use Guzzle\Http\Exception\MultiTransferException; -use Guzzle\Http\Exception\RequestException; -use Guzzle\Plugin\Mock\MockPlugin; -use Guzzle\Http\Message\Response; -use Guzzle\Http\Message\Request; +use FOS\HttpCache\Test\HttpClient\MockHttpAdapter; +use Http\Adapter\Exception\HttpAdapterException; +use Http\Discovery\MessageFactoryDiscovery; +use Psr\Http\Message\RequestInterface; use \Mockery; /** @@ -28,81 +25,83 @@ class AbstractProxyClientTest extends \PHPUnit_Framework_TestCase { /** - * @var MockPlugin + * @var MockHttpAdapter */ - private $mock; - private $client; - public function testUnreachableException() + /** + * @dataProvider exceptionProvider + * + * @param \Exception $exception Exception thrown by HTTP adapter. + * @param string $type The returned exception class to be expected. + * @param string $message Optional exception message to match against. + */ + public function testExceptions(\Exception $exception, $type, $message = null) { - $mock = new MockPlugin(); - $mock->addException(new CurlException('connect to host')); - - $client = new Client(); - $client->addSubscriber($mock); + $this->client->setException($exception); + $varnish = new Varnish(['127.0.0.1:123'], 'my_hostname.dev', $this->client); - $varnish = new Varnish(array('127.0.0.1:123'), 'my_hostname.dev', $client); + $varnish->purge('/'); try { - $varnish->purge('/paths')->flush(); + $varnish->flush(); + $this->fail('Should have aborted with an exception'); } catch (ExceptionCollection $exceptions) { $this->assertCount(1, $exceptions); - $this->assertInstanceOf('\FOS\HttpCache\Exception\ProxyUnreachableException', $exceptions->getFirst()); + $this->assertInstanceOf($type, $exceptions->getFirst()); + if ($message) { + $this->assertContains( + $message, + $exceptions->getFirst()->getMessage() + ); + } } - $mock->clearQueue(); - $mock->addResponse(new Response(200)); + $this->client->clear(); // Queue must now be empty, so exception above must not be thrown again. $varnish->purge('/path')->flush(); } - public function curlExceptionProvider() + public function exceptionProvider() { - $requestException = new RequestException('request'); - $requestException->setRequest(new Request('GET', '/')); - - $curlException = new CurlException('curl'); - $curlException->setRequest(new Request('GET', '/')); - return array( - array($curlException, '\FOS\HttpCache\Exception\ProxyUnreachableException'), - array($requestException, '\FOS\HttpCache\Exception\ProxyResponseException'), - array(new \InvalidArgumentException('something'), '\InvalidArgumentException'), - ); + // Timeout exception (without response) + $request = \Mockery::mock('\Psr\Http\Message\RequestInterface') + ->shouldReceive('getHeaderLine') + ->with('Host') + ->andReturn('bla.com') + ->getMock() + ; + $unreachableException = new HttpAdapterException(); + $unreachableException->setRequest($request); + + return [ + [ + $unreachableException, + '\FOS\HttpCache\Exception\ProxyUnreachableException', + 'bla.com' + ] + ]; } - /** - * @dataProvider curlExceptionProvider - * - * @param \Exception $exception The exception that curl should throw. - * @param string $type The returned exception class to be expected. - */ - public function testExceptions(\Exception $exception, $type) + public function testErrorResponsesAreConvertedToExceptions() { - // the guzzle mock plugin does not allow arbitrary exceptions - // mockery does not provide all methods of the interface - $collection = new MultiTransferException(); - $collection->setExceptions(array($exception)); - $client = $this->getMock('\Guzzle\Http\ClientInterface'); - $client->expects($this->any()) - ->method('createRequest') - ->willReturn(new Request('BAN', '/')) - ; - $client->expects($this->once()) - ->method('send') - ->willThrowException($collection) - ; - - $varnish = new Varnish(array('127.0.0.1:123'), 'my_hostname.dev', $client); + $response = MessageFactoryDiscovery::find()->createResponse( + 405, + 'Not allowed' + ); + $this->client->addResponse($response); - $varnish->ban(array()); + $varnish = new Varnish(['127.0.0.1:123'], 'my_hostname.dev', $this->client); try { - $varnish->flush(); + $varnish->purge('/')->flush(); $this->fail('Should have aborted with an exception'); } catch (ExceptionCollection $exceptions) { $this->assertCount(1, $exceptions); - $this->assertInstanceOf($type, $exceptions->getFirst()); + $this->assertEquals( + '405 error response "Not allowed" from caching proxy', + $exceptions->getFirst()->getMessage() + ); } } @@ -112,41 +111,33 @@ public function testExceptions(\Exception $exception, $type) */ public function testMissingHostExceptionIsThrown() { - $varnish = new Varnish(array('127.0.0.1:123'), null, $this->client); + $varnish = new Varnish(['127.0.0.1:123'], null, $this->client); $varnish->purge('/path/without/hostname'); } public function testSetBasePathWithHost() { - $varnish = new Varnish(array('127.0.0.1'), 'fos.lo', $this->client); + $varnish = new Varnish(['127.0.0.1'], 'fos.lo', $this->client); $varnish->purge('/path')->flush(); $requests = $this->getRequests(); - $this->assertEquals('fos.lo', $requests[0]->getHeader('Host')); + $this->assertEquals('fos.lo', $requests[0]->getHeaderLine('Host')); } public function testSetBasePathWithPath() { - $varnish = new Varnish(array('127.0.0.1'), 'http://fos.lo/my/path', $this->client); + $varnish = new Varnish(['127.0.0.1'], 'http://fos.lo/my/path', $this->client); $varnish->purge('append')->flush(); $requests = $this->getRequests(); - $this->assertEquals('fos.lo', $requests[0]->getHeader('Host')); - $this->assertEquals('http://127.0.0.1/my/path/append', $requests[0]->getUrl()); - } - - /** - * @expectedException \FOS\HttpCache\Exception\InvalidUrlException - */ - public function testSetBasePathThrowsInvalidUrlSchemeException() - { - new Varnish(array('127.0.0.1'), 'https://fos.lo/my/path'); + $this->assertEquals('fos.lo', $requests[0]->getHeaderLine('Host')); + $this->assertEquals('http://127.0.0.1/my/path/append', (string) $requests[0]->getUri()); } public function testSetServersDefaultSchemeIsAdded() { - $varnish = new Varnish(array('127.0.0.1'), 'fos.lo', $this->client); + $varnish = new Varnish(['127.0.0.1'], 'fos.lo', $this->client); $varnish->purge('/some/path')->flush(); $requests = $this->getRequests(); - $this->assertEquals('http://127.0.0.1/some/path', $requests[0]->getUrl()); + $this->assertEquals('http://127.0.0.1/some/path', $requests[0]->getUri()); } /** @@ -155,7 +146,7 @@ public function testSetServersDefaultSchemeIsAdded() */ public function testSetServersThrowsInvalidUrlException() { - new Varnish(array('http:///this is no url')); + new Varnish(['http:///this is no url']); } /** @@ -164,16 +155,7 @@ public function testSetServersThrowsInvalidUrlException() */ public function testSetServersThrowsWeirdInvalidUrlException() { - new Varnish(array('this ://is no url')); - } - - /** - * @expectedException \FOS\HttpCache\Exception\InvalidUrlException - * @expectedExceptionMessage Host "https://127.0.0.1" with scheme "https" is invalid - */ - public function testSetServersThrowsInvalidUrlSchemeException() - { - new Varnish(array('https://127.0.0.1')); + new Varnish(['this ://is no url']); } /** @@ -182,43 +164,39 @@ public function testSetServersThrowsInvalidUrlSchemeException() */ public function testSetServersThrowsInvalidServerException() { - new Varnish(array('http://127.0.0.1:80/some/weird/path')); + new Varnish(['http://127.0.0.1:80/some/weird/path']); } public function testFlushEmpty() { - $client = \Mockery::mock('\Guzzle\Http\Client[send]', array('', null)) - ->shouldReceive('send') - ->never() - ->getMock() - ; - - $varnish = new Varnish(array('127.0.0.1', '127.0.0.2'), 'fos.lo', $client); + $varnish = new Varnish(array('127.0.0.1', '127.0.0.2'), 'fos.lo', $this->client); $this->assertEquals(0, $varnish->flush()); + + $this->assertCount(0, $this->client->getRequests()); } public function testFlushCountSuccess() { - $self = $this; - $client = \Mockery::mock('\Guzzle\Http\Client[send]', array('', null)) - ->shouldReceive('send') + $httpAdapter = \Mockery::mock('\Http\Adapter\HttpAdapter') + ->shouldReceive('sendRequests') ->once() ->with( \Mockery::on( - function ($requests) use ($self) { - /** @type Request[] $requests */ - $self->assertCount(4, $requests); + function ($requests) { + /** @type RequestInterface[] $requests */ + $this->assertCount(4, $requests); foreach ($requests as $request) { - $self->assertEquals('PURGE', $request->getMethod()); + $this->assertEquals('PURGE', $request->getMethod()); } return true; } ) ) + ->andReturn([]) ->getMock(); - $varnish = new Varnish(array('127.0.0.1', '127.0.0.2'), 'fos.lo', $client); + $varnish = new Varnish(['127.0.0.1', '127.0.0.2'], 'fos.lo', $httpAdapter); $this->assertEquals( 2, @@ -231,23 +209,23 @@ function ($requests) use ($self) { public function testEliminateDuplicates() { - $self = $this; - $client = \Mockery::mock('\Guzzle\Http\Client[send]', array('', null)) - ->shouldReceive('send') + $client = \Mockery::mock('\Http\Adapter\HttpAdapter') + ->shouldReceive('sendRequests') ->once() ->with( \Mockery::on( - function ($requests) use ($self) { - /** @type Request[] $requests */ - $self->assertCount(4, $requests); + function ($requests) { + /** @type RequestInterface[] $requests */ + $this->assertCount(4, $requests); foreach ($requests as $request) { - $self->assertEquals('PURGE', $request->getMethod()); + $this->assertEquals('PURGE', $request->getMethod()); } return true; } ) ) + ->andReturn([]) ->getMock(); $varnish = new Varnish(array('127.0.0.1', '127.0.0.2'), 'fos.lo', $client); @@ -263,20 +241,16 @@ function ($requests) use ($self) { ); } - protected function setUp() { - $this->mock = new MockPlugin(); - $this->mock->addResponse(new Response(200)); - $this->client = new Client(); - $this->client->addSubscriber($this->mock); + $this->client = new MockHttpAdapter(); } /** - * @return array|Request[] + * @return array|RequestInterface[] */ protected function getRequests() { - return $this->mock->getReceivedRequests(); + return $this->client->getRequests(); } } diff --git a/tests/Unit/ProxyClient/SymfonyTest.php b/tests/Unit/ProxyClient/SymfonyTest.php index 2fe4d726..0596fb79 100644 --- a/tests/Unit/ProxyClient/SymfonyTest.php +++ b/tests/Unit/ProxyClient/SymfonyTest.php @@ -14,6 +14,7 @@ use FOS\HttpCache\Exception\ExceptionCollection; use FOS\HttpCache\ProxyClient\Symfony; use FOS\HttpCache\ProxyClient\Varnish; +use FOS\HttpCache\Test\HttpClient\MockHttpAdapter; use Guzzle\Http\Client; use Guzzle\Http\Exception\CurlException; use Guzzle\Http\Exception\MultiTransferException; @@ -26,61 +27,34 @@ class SymfonyTest extends \PHPUnit_Framework_TestCase { /** - * @var MockPlugin + * @var MockHttpAdapter */ - private $mock; - - private $client; + protected $client; public function testPurge() { - $self = $this; // For PHP 5.3 - $client = \Mockery::mock('\Guzzle\Http\Client[send]', array('', null)) - ->shouldReceive('send') - ->once() - ->with( - \Mockery::on( - function ($requests) use ($self) { - /** @type Request[] $requests */ - $self->assertCount(4, $requests); - foreach ($requests as $request) { - $self->assertEquals('PURGE', $request->getMethod()); - $self->assertEquals('my_hostname.dev', $request->getHeaders()->get('host')); - } - - $self->assertEquals('127.0.0.1', $requests[0]->getHost()); - $self->assertEquals('8080', $requests[0]->getPort()); - $self->assertEquals('/url/one', $requests[0]->getPath()); - - $self->assertEquals('123.123.123.2', $requests[1]->getHost()); - $self->assertEquals('/url/one', $requests[1]->getPath()); - - $self->assertEquals('127.0.0.1', $requests[2]->getHost()); - $self->assertEquals('8080', $requests[2]->getPort()); - $self->assertEquals('/url/two', $requests[2]->getPath()); - $self->assertEquals('bar', $requests[2]->getHeader('X-Foo')); - - $self->assertEquals('123.123.123.2', $requests[3]->getHost()); - $self->assertEquals('/url/two', $requests[3]->getPath()); - $self->assertEquals('bar', $requests[3]->getHeader('X-Foo')); - - return true; - } - ) - ) - ->getMock(); - - $ips = array( - '127.0.0.1:8080', - '123.123.123.2', - ); - - $symfony = new Symfony($ips, 'my_hostname.dev', $client); - - $symfony->purge('/url/one'); - $symfony->purge('/url/two', array('X-Foo' => 'bar')); - - $symfony->flush(); + $ips = ['127.0.0.1:8080', '123.123.123.2']; + $varnish = new Varnish($ips, 'my_hostname.dev', $this->client); + + $count = $varnish->purge('/url/one') + ->purge('/url/two', array('X-Foo' => 'bar')) + ->flush() + ; + $this->assertEquals(2, $count); + + $requests = $this->getRequests(); + $this->assertCount(4, $requests); + foreach ($requests as $request) { + $this->assertEquals('PURGE', $request->getMethod()); + $this->assertEquals('my_hostname.dev', $request->getHeaderLine('Host')); + } + + $this->assertEquals('http://127.0.0.1:8080/url/one', $requests[0]->getUri()); + $this->assertEquals('http://123.123.123.2/url/one', $requests[1]->getUri()); + $this->assertEquals('http://127.0.0.1:8080/url/two', $requests[2]->getUri()); + $this->assertEquals('bar', $requests[2]->getHeaderLine('X-Foo')); + $this->assertEquals('http://123.123.123.2/url/two', $requests[3]->getUri()); + $this->assertEquals('bar', $requests[3]->getHeaderLine('X-Foo')); } public function testRefresh() @@ -91,22 +65,19 @@ public function testRefresh() $requests = $this->getRequests(); $this->assertCount(1, $requests); $this->assertEquals('GET', $requests[0]->getMethod()); - $this->assertEquals('http://127.0.0.1:123/fresh', $requests[0]->getUrl()); + $this->assertEquals('http://127.0.0.1:123/fresh', $requests[0]->getUri()); } protected function setUp() { - $this->mock = new MockPlugin(); - $this->mock->addResponse(new Response(200)); - $this->client = new Client(); - $this->client->addSubscriber($this->mock); + $this->client = new MockHttpAdapter(); } - + /** - * @return array|Request[] + * @return array|RequestInterface[] */ protected function getRequests() { - return $this->mock->getReceivedRequests(); + return $this->client->getRequests(); } } diff --git a/tests/Unit/ProxyClient/VarnishTest.php b/tests/Unit/ProxyClient/VarnishTest.php index 92c4def0..00f23995 100644 --- a/tests/Unit/ProxyClient/VarnishTest.php +++ b/tests/Unit/ProxyClient/VarnishTest.php @@ -11,24 +11,15 @@ namespace FOS\HttpCache\Tests\Unit\ProxyClient; -use FOS\HttpCache\Exception\ExceptionCollection; use FOS\HttpCache\ProxyClient\Varnish; -use Guzzle\Http\Client; -use Guzzle\Http\Exception\CurlException; -use Guzzle\Http\Exception\MultiTransferException; -use Guzzle\Http\Exception\RequestException; -use Guzzle\Plugin\Mock\MockPlugin; -use Guzzle\Http\Message\Response; -use Guzzle\Http\Message\Request; +use FOS\HttpCache\Test\HttpClient\MockHttpAdapter; use \Mockery; class VarnishTest extends \PHPUnit_Framework_TestCase { /** - * @var MockPlugin + * @var MockHttpAdapter */ - protected $mock; - protected $client; public function testBanEverything() @@ -40,11 +31,10 @@ public function testBanEverything() $this->assertCount(1, $requests); $this->assertEquals('BAN', $requests[0]->getMethod()); - $headers = $requests[0]->getHeaders(); - $this->assertEquals('.*', $headers->get('X-Host')); - $this->assertEquals('.*', $headers->get('X-Url')); - $this->assertEquals('.*', $headers->get('X-Content-Type')); - $this->assertEquals('fos.lo', $headers->get('Host')); + $this->assertEquals('.*', $requests[0]->getHeaderLine('X-Host')); + $this->assertEquals('.*', $requests[0]->getHeaderLine('X-Url')); + $this->assertEquals('.*', $requests[0]->getHeaderLine('X-Content-Type')); + $this->assertEquals('fos.lo', $requests[0]->getHeaderLine('Host')); } public function testBanEverythingNoBaseUrl() @@ -56,12 +46,12 @@ public function testBanEverythingNoBaseUrl() $this->assertCount(1, $requests); $this->assertEquals('BAN', $requests[0]->getMethod()); - $headers = $requests[0]->getHeaders(); - $this->assertEquals('.*', $headers->get('X-Host')); - $this->assertEquals('.*', $headers->get('X-Url')); - $this->assertEquals('.*', $headers->get('X-Content-Type')); + $this->assertEquals('.*', $requests[0]->getHeaderLine('X-Host')); + $this->assertEquals('.*', $requests[0]->getHeaderLine('X-Url')); + $this->assertEquals('.*', $requests[0]->getHeaderLine('X-Content-Type')); + // Ensure host header matches the Varnish server one. - $this->assertEquals(array('127.0.0.1:123'), $headers->get('Host')->toArray()); + $this->assertEquals('http://127.0.0.1:123/', $requests[0]->getUri()); } public function testBanHeaders() @@ -77,10 +67,9 @@ public function testBanHeaders() $this->assertCount(1, $requests); $this->assertEquals('BAN', $requests[0]->getMethod()); - $headers = $requests[0]->getHeaders(); - $this->assertEquals('.*', $headers->get('Test')); - $this->assertEquals('B', $headers->get('A')); - $this->assertEquals('fos.lo', $headers->get('Host')); + $this->assertEquals('.*', $requests[0]->getHeaderLine('Test')); + $this->assertEquals('B', $requests[0]->getHeaderLine('A')); + $this->assertEquals('fos.lo', $requests[0]->getHeaderLine('Host')); } public function testBanPath() @@ -94,10 +83,9 @@ public function testBanPath() $this->assertCount(1, $requests); $this->assertEquals('BAN', $requests[0]->getMethod()); - $headers = $requests[0]->getHeaders(); - $this->assertEquals('^(fos.lo|fos2.lo)$', $headers->get('X-Host')); - $this->assertEquals('/articles/.*', $headers->get('X-Url')); - $this->assertEquals('text/html', $headers->get('X-Content-Type')); + $this->assertEquals('^(fos.lo|fos2.lo)$', $requests[0]->getHeaderLine('X-Host')); + $this->assertEquals('/articles/.*', $requests[0]->getHeaderLine('X-Url')); + $this->assertEquals('text/html', $requests[0]->getHeaderLine('X-Content-Type')); } /** @@ -113,53 +101,28 @@ public function testBanPathEmptyHost() public function testPurge() { - $self = $this; // For PHP 5.3 - $client = \Mockery::mock('\Guzzle\Http\Client[send]', array('', null)) - ->shouldReceive('send') - ->once() - ->with( - \Mockery::on( - function ($requests) use ($self) { - /** @type Request[] $requests */ - $self->assertCount(4, $requests); - foreach ($requests as $request) { - $self->assertEquals('PURGE', $request->getMethod()); - $self->assertEquals('my_hostname.dev', $request->getHeaders()->get('host')); - } - - $self->assertEquals('127.0.0.1', $requests[0]->getHost()); - $self->assertEquals('8080', $requests[0]->getPort()); - $self->assertEquals('/url/one', $requests[0]->getPath()); - - $self->assertEquals('123.123.123.2', $requests[1]->getHost()); - $self->assertEquals('/url/one', $requests[1]->getPath()); - - $self->assertEquals('127.0.0.1', $requests[2]->getHost()); - $self->assertEquals('8080', $requests[2]->getPort()); - $self->assertEquals('/url/two', $requests[2]->getPath()); - $self->assertEquals('bar', $requests[2]->getHeader('X-Foo')); - - $self->assertEquals('123.123.123.2', $requests[3]->getHost()); - $self->assertEquals('/url/two', $requests[3]->getPath()); - $self->assertEquals('bar', $requests[3]->getHeader('X-Foo')); - - return true; - } - ) - ) - ->getMock(); - - $ips = array( - '127.0.0.1:8080', - '123.123.123.2', - ); - - $varnish = new Varnish($ips, 'my_hostname.dev', $client); - - $varnish->purge('/url/one'); - $varnish->purge('/url/two', array('X-Foo' => 'bar')); - - $varnish->flush(); + $ips = ['127.0.0.1:8080', '123.123.123.2']; + $varnish = new Varnish($ips, 'my_hostname.dev', $this->client); + + $count = $varnish->purge('/url/one') + ->purge('/url/two', array('X-Foo' => 'bar')) + ->flush() + ; + $this->assertEquals(2, $count); + + $requests = $this->getRequests(); + $this->assertCount(4, $requests); + foreach ($requests as $request) { + $this->assertEquals('PURGE', $request->getMethod()); + $this->assertEquals('my_hostname.dev', $request->getHeaderLine('Host')); + } + + $this->assertEquals('http://127.0.0.1:8080/url/one', $requests[0]->getUri()); + $this->assertEquals('http://123.123.123.2/url/one', $requests[1]->getUri()); + $this->assertEquals('http://127.0.0.1:8080/url/two', $requests[2]->getUri()); + $this->assertEquals('bar', $requests[2]->getHeaderLine('X-Foo')); + $this->assertEquals('http://123.123.123.2/url/two', $requests[3]->getUri()); + $this->assertEquals('bar', $requests[3]->getHeaderLine('X-Foo')); } public function testRefresh() @@ -170,22 +133,19 @@ public function testRefresh() $requests = $this->getRequests(); $this->assertCount(1, $requests); $this->assertEquals('GET', $requests[0]->getMethod()); - $this->assertEquals('http://127.0.0.1:123/fresh', $requests[0]->getUrl()); + $this->assertEquals('http://127.0.0.1:123/fresh', $requests[0]->getUri()); } protected function setUp() { - $this->mock = new MockPlugin(); - $this->mock->addResponse(new Response(200)); - $this->client = new Client(); - $this->client->addSubscriber($this->mock); + $this->client = new MockHttpAdapter(); } /** - * @return array|Request[] + * @return array|RequestInterface[] */ protected function getRequests() { - return $this->mock->getReceivedRequests(); + return $this->client->getRequests(); } } diff --git a/tests/Unit/Test/PHPUnit/AbstractCacheConstraintTest.php b/tests/Unit/Test/PHPUnit/AbstractCacheConstraintTest.php index 5dc5290b..e8a251f0 100644 --- a/tests/Unit/Test/PHPUnit/AbstractCacheConstraintTest.php +++ b/tests/Unit/Test/PHPUnit/AbstractCacheConstraintTest.php @@ -6,9 +6,10 @@ abstract class AbstractCacheConstraintTest extends \PHPUnit_Framework_TestCase { protected function getResponseMock() { - return \Mockery::mock( - '\Guzzle\Http\Message\Response[hasHeader,getHeader]', - array(null) + $mock = \Mockery::mock( + '\Psr\Http\Message\ResponseInterface[hasHeader,getHeaderLine,getStatusCode]' ); + + return $mock; } } diff --git a/tests/Unit/Test/PHPUnit/IsCacheHitConstraintTest.php b/tests/Unit/Test/PHPUnit/IsCacheHitConstraintTest.php index 476fe842..6c1ff72e 100644 --- a/tests/Unit/Test/PHPUnit/IsCacheHitConstraintTest.php +++ b/tests/Unit/Test/PHPUnit/IsCacheHitConstraintTest.php @@ -27,12 +27,14 @@ public function setUp() /** * @expectedException \PHPUnit_Framework_ExpectationFailedException + * @expectedExceptionMessage Failed asserting that response (with status code 500) is a cache hit */ public function testMatches() { $response = $this->getResponseMock() ->shouldReceive('hasHeader')->with('cache-header')->andReturn(true) - ->shouldReceive('getHeader')->with('cache-header')->once()->andReturn('MISS') + ->shouldReceive('getHeaderLine')->with('cache-header')->once()->andReturn('MISS') + ->shouldReceive('getStatusCode')->andReturn(500) ->getMock(); $this->constraint->evaluate($response); diff --git a/tests/Unit/Test/PHPUnit/IsCacheMissConstraintTest.php b/tests/Unit/Test/PHPUnit/IsCacheMissConstraintTest.php index 48b98756..01529fcf 100644 --- a/tests/Unit/Test/PHPUnit/IsCacheMissConstraintTest.php +++ b/tests/Unit/Test/PHPUnit/IsCacheMissConstraintTest.php @@ -27,13 +27,14 @@ public function setUp() /** * @expectedException \PHPUnit_Framework_ExpectationFailedException + * @expectedExceptionMessage Failed asserting that response (with status code 200) is a cache miss */ public function testMatches() { $response = $this->getResponseMock() ->shouldReceive('hasHeader')->with('cache-header')->andReturn(true) - ->shouldReceive('getHeader')->with('cache-header')->once() - ->andReturn('HIT') + ->shouldReceive('getHeaderLine')->with('cache-header')->once()->andReturn('HIT') + ->shouldReceive('getStatusCode')->andReturn(200) ->getMock(); $this->constraint->evaluate($response); diff --git a/tests/Unit/Test/Proxy/VarnishProxyTest.php b/tests/Unit/Test/Proxy/VarnishProxyTest.php index e0c9b426..adcab0a4 100644 --- a/tests/Unit/Test/Proxy/VarnishProxyTest.php +++ b/tests/Unit/Test/Proxy/VarnishProxyTest.php @@ -13,7 +13,7 @@ use FOS\HttpCache\Test\Proxy\VarnishProxy; -class VarnishTestCaseTest extends \PHPUnit_Framework_TestCase +class VarnishProxyTest extends \PHPUnit_Framework_TestCase { /** * @expectedException \InvalidArgumentException diff --git a/tests/ci/install-apache.sh b/tests/ci/install-apache.sh deleted file mode 100755 index 9f5a04fa..00000000 --- a/tests/ci/install-apache.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# https://github.com/travis-ci/travis-ci.github.com/blob/master/docs/user/languages/php.md#apache--php - -sudo apt-get install apache2 libapache2-mod-fastcgi -# enable php-fpm -sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf -sudo a2enmod rewrite actions fastcgi alias -echo "cgi.fix_pathinfo = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini -~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm -# configure apache virtual hosts -sudo cp -f tests/ci/travis-ci-apache /etc/apache2/sites-available/default -sudo sed -e "s?%TRAVIS_BUILD_DIR%?$(pwd)?g" --in-place /etc/apache2/sites-available/default -sudo service apache2 restart