Issue #2920001 by Wim Leers, dawehner, borisson_, aheimlich, davidwbarratt: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata

8.5.x
effulgentsia 2017-11-08 19:31:27 -08:00
parent 52a598e2bf
commit b83782a74b
26 changed files with 610 additions and 53 deletions

View File

@ -0,0 +1,67 @@
<?php
namespace Drupal\Core\Cache;
/**
* Trait for \Drupal\Core\Cache\CacheableDependencyInterface.
*/
trait CacheableDependencyTrait {
/**
* Cache contexts.
*
* @var string[]
*/
protected $cacheContexts = [];
/**
* Cache tags.
*
* @var string[]
*/
protected $cacheTags = [];
/**
* Cache max-age.
*
* @var int
*/
protected $cacheMaxAge = Cache::PERMANENT;
/**
* Sets cacheability; useful for value object constructors.
*
* @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheability
* The cacheability to set.
*
* @return $this
*/
protected function setCacheability(CacheableDependencyInterface $cacheability) {
$this->cacheContexts = $cacheability->getCacheContexts();
$this->cacheTags = $cacheability->getCacheTags();
$this->cacheMaxAge = $cacheability->getCacheMaxAge();
return $this;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->cacheTags;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->cacheContexts;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->cacheMaxAge;
}
}

View File

@ -7,47 +7,7 @@ namespace Drupal\Core\Cache;
*/
trait RefinableCacheableDependencyTrait {
/**
* Cache contexts.
*
* @var string[]
*/
protected $cacheContexts = [];
/**
* Cache tags.
*
* @var string[]
*/
protected $cacheTags = [];
/**
* Cache max-age.
*
* @var int
*/
protected $cacheMaxAge = Cache::PERMANENT;
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->cacheTags;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->cacheContexts;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->cacheMaxAge;
}
use CacheableDependencyTrait;
/**
* {@inheritdoc}

View File

@ -2,6 +2,7 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Utility\Error;
use Psr\Log\LoggerInterface;
@ -170,6 +171,13 @@ class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
$response->setStatusCode($status_code);
}
// Persist the exception's cacheability metadata, if any. If the exception
// itself isn't cacheable, then this will make the response uncacheable:
// max-age=0 will be set.
if ($response instanceof CacheableResponseInterface) {
$response->addCacheableDependency($exception);
}
// Persist any special HTTP headers that were set on the exception.
if ($exception instanceof HttpExceptionInterface) {
$response->headers->add($exception->getHeaders());

View File

@ -2,6 +2,8 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableJsonResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
@ -35,7 +37,16 @@ class ExceptionJsonSubscriber extends HttpExceptionSubscriberBase {
public function on4xx(GetResponseForExceptionEvent $event) {
/** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $exception */
$exception = $event->getException();
$response = new JsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
// If the exception is cacheable, generate a cacheable response.
if ($exception instanceof CacheableDependencyInterface) {
$response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
$response->addCacheableDependency($exception);
}
else {
$response = new JsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
}
$event->setResponse($response);
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* A cacheable AccessDeniedHttpException.
*/
class CacheableAccessDeniedHttpException extends AccessDeniedHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* A cacheable BadRequestHttpException.
*/
class CacheableBadRequestHttpException extends BadRequestHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
/**
* A cacheable ConflictHttpException.
*/
class CacheableConflictHttpException extends ConflictHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
/**
* A cacheable GoneHttpException.
*/
class CacheableGoneHttpException extends GoneHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* A cacheable HttpException.
*/
class CacheableHttpException extends HttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $statusCode = 0, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($statusCode, $message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException;
/**
* A cacheable LengthRequiredHttpException.
*/
class CacheableLengthRequiredHttpException extends LengthRequiredHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
/**
* A cacheable MethodNotAllowedHttpException.
*/
class CacheableMethodNotAllowedHttpException extends MethodNotAllowedHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, array $allow, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($allow, $message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
/**
* A cacheable NotAcceptableHttpException.
*/
class CacheableNotAcceptableHttpException extends NotAcceptableHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* A cacheable NotFoundHttpException.
*/
class CacheableNotFoundHttpException extends NotFoundHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
/**
* A cacheable PreconditionFailedHttpException.
*/
class CacheablePreconditionFailedHttpException extends PreconditionFailedHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
/**
* A cacheable PreconditionRequiredHttpException.
*/
class CacheablePreconditionRequiredHttpException extends PreconditionRequiredHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* A cacheable ServiceUnavailableHttpException.
*/
class CacheableServiceUnavailableHttpException extends ServiceUnavailableHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $retryAfter = NULL, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($retryAfter, $message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
/**
* A cacheable TooManyRequestsHttpException.
*/
class CacheableTooManyRequestsHttpException extends TooManyRequestsHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $retryAfter = NULL, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($retryAfter, $message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
/**
* A cacheable UnauthorizedHttpException.
*/
class CacheableUnauthorizedHttpException extends UnauthorizedHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $challenge, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($challenge, $message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
/**
* A cacheable UnprocessableEntityHttpException.
*/
class CacheableUnprocessableEntityHttpException extends UnprocessableEntityHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\Core\Http\Exception;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
/**
* A cacheable UnsupportedMediaTypeHttpException.
*/
class CacheableUnsupportedMediaTypeHttpException extends UnsupportedMediaTypeHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
$this->setCacheability($cacheability);
parent::__construct($message, $previous, $code);
}
}

View File

@ -5,12 +5,13 @@ namespace Drupal\basic_auth\Authentication\Provider;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Flood\FloodInterface;
use Drupal\Core\Http\Exception\CacheableUnauthorizedHttpException;
use Drupal\user\UserAuthInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
/**
* HTTP Basic authentication provider.
@ -126,11 +127,35 @@ class BasicAuth implements AuthenticationProviderInterface, AuthenticationProvid
* {@inheritdoc}
*/
public function challengeException(Request $request, \Exception $previous) {
$site_name = $this->configFactory->get('system.site')->get('name');
$site_config = $this->configFactory->get('system.site');
$site_name = $site_config->get('name');
$challenge = SafeMarkup::format('Basic realm="@realm"', [
'@realm' => !empty($site_name) ? $site_name : 'Access restricted',
]);
return new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
// A 403 is converted to a 401 here, but it doesn't matter what the
// cacheability was of the 403 exception: what matters here is that
// authentication credentials are missing, i.e. that this request was made
// as the anonymous user.
// Therefore, all we must do, is make this response:
// 1. vary by whether the current user has the 'anonymous' role or not. This
// works fine because:
// - Thanks to \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests,
// Page Cache never caches a response whose request has Basic Auth
// credentials.
// - Dynamic Page Cache will cache a different result for when the
// request is unauthenticated (this 401) versus authenticated (some
// other response)
// 2. have the 'config:user.role.anonymous' cache tag, because the only
// reason this 401 would no longer be a 401 is if permissions for the
// 'anonymous' role change, causing that cache tag to be invalidated.
// @see \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
// @see \Drupal\Core\EventSubscriber\ClientErrorResponseSubscriber()
// @see \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onAllResponds()
$cacheability = CacheableMetadata::createFromObject($site_config)
->addCacheTags(['config:user.role.anonymous'])
->addCacheContexts(['user.roles:anonymous']);
return new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous);
}
}

View File

@ -7,6 +7,7 @@ use Drupal\Core\Url;
use Drupal\Tests\basic_auth\Traits\BasicAuthTestTrait;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
/**
* Tests for BasicAuth authentication provider.
@ -180,6 +181,47 @@ class BasicAuthTest extends BrowserTestBase {
$this->assertText('Access denied', "A user friendly access denied message is displayed");
}
/**
* Tests the cacheability of Basic Auth's 401 response.
*
* @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
*/
public function testCacheabilityOf401Response() {
$session = $this->getSession();
$url = Url::fromRoute('router_test.11');
$assert_response_cacheability = function ($expected_page_cache_header_value, $expected_dynamic_page_cache_header_value) use ($session, $url) {
$this->drupalGet($url);
$this->assertSession()->statusCodeEquals(401);
$this->assertSame($expected_page_cache_header_value, $session->getResponseHeader('X-Drupal-Cache'));
$this->assertSame($expected_dynamic_page_cache_header_value, $session->getResponseHeader('X-Drupal-Dynamic-Cache'));
};
// 1. First request: cold caches, both Page Cache and Dynamic Page Cache are
// now primed.
$assert_response_cacheability('MISS', 'MISS');
// 2. Second request: Page Cache HIT, we don't even hit Dynamic Page Cache.
// This is going to keep happening.
$assert_response_cacheability('HIT', 'MISS');
// 3. Third request: after clearing Page Cache, we now see that Dynamic Page
// Cache is a HIT too.
$this->container->get('cache.page')->deleteAll();
$assert_response_cacheability('MISS', 'HIT');
// 4. Fourth request: warm caches.
$assert_response_cacheability('HIT', 'HIT');
// If the permissions of the 'anonymous' role change, it may no longer be
// necessary to be authenticated to access this route. Therefore the cached
// 401 responses should be invalidated.
$this->grantPermissions(Role::load(Role::ANONYMOUS_ID), [$this->randomMachineName()]);
$assert_response_cacheability('MISS', 'MISS');
$assert_response_cacheability('HIT', 'MISS');
// Idem for when the 'system.site' config changes.
$this->config('system.site')->save();
$assert_response_cacheability('MISS', 'MISS');
$assert_response_cacheability('HIT', 'MISS');
}
/**
* Tests if the controller is called before authentication.
*

View File

@ -7,3 +7,8 @@ services:
class: Drupal\rest_test\Authentication\Provider\TestAuthGlobal
tags:
- { name: authentication_provider, provider_id: 'rest_test_auth_global', global: TRUE }
rest_test.page_cache_request_policy.deny_test_auth_requests:
class: Drupal\rest_test\PageCache\RequestPolicy\DenyTestAuthRequests
public: false
tags:
- { name: page_cache_request_policy }

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\rest_test\PageCache\RequestPolicy;
use Drupal\Core\PageCache\RequestPolicyInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Cache policy for pages requested with REST Test Auth.
*
* This policy disallows caching of requests that use the REST Test Auth
* authentication provider for security reasons (just like basic_auth).
* Otherwise responses for authenticated requests can get into the page cache
* and could be delivered to unprivileged users.
*
* @see \Drupal\rest_test\Authentication\Provider\TestAuth
* @see \Drupal\rest_test\Authentication\Provider\TestAuthGlobal
* @see \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests
*/
class DenyTestAuthRequests implements RequestPolicyInterface {
/**
* {@inheritdoc}
*/
public function check(Request $request) {
if ($request->headers->has('REST-test-auth') || $request->headers->has('REST-test-auth-global')) {
return self::DENY;
}
}
}

View File

@ -414,14 +414,20 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
':pattern' => '%[route]=rest.%',
])
->fetchAllAssoc('cid');
$this->assertCount(2, $cache_items);
$this->assertTrue(count($cache_items) >= 2);
$found_cache_redirect = FALSE;
$found_cached_response = FALSE;
$found_cached_200_response = FALSE;
$other_cached_responses_are_4xx = TRUE;
foreach ($cache_items as $cid => $cache_item) {
$cached_data = unserialize($cache_item->data);
if (!isset($cached_data['#cache_redirect'])) {
$found_cached_response = TRUE;
$cached_response = $cached_data['#response'];
if ($cached_response->getStatusCode() === 200) {
$found_cached_200_response = TRUE;
}
elseif (!$cached_response->isClientError()) {
$other_cached_responses_are_4xx = FALSE;
}
$this->assertNotInstanceOf(ResourceResponseInterface::class, $cached_response);
$this->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
}
@ -430,7 +436,8 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
}
}
$this->assertTrue($found_cache_redirect);
$this->assertTrue($found_cached_response);
$this->assertTrue($found_cached_200_response);
$this->assertTrue($other_cached_responses_are_4xx);
}
$cache_tags_header_value = $response->getHeader('X-Drupal-Cache-Tags')[0];
$this->assertEquals($this->getExpectedCacheTags(), empty($cache_tags_header_value) ? [] : explode(' ', $cache_tags_header_value));

View File

@ -2,11 +2,15 @@
namespace Drupal\Tests\Core\EventSubscriber;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\EventSubscriber\ExceptionJsonSubscriber;
use Drupal\Core\Http\Exception\CacheableMethodNotAllowedHttpException;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
@ -18,21 +22,34 @@ class ExceptionJsonSubscriberTest extends UnitTestCase {
/**
* @covers ::on4xx
* @dataProvider providerTestOn4xx
*/
public function testOn4xx() {
public function testOn4xx(HttpExceptionInterface $exception, $expected_response_class) {
$kernel = $this->prophesize(HttpKernelInterface::class);
$request = Request::create('/test');
$e = new MethodNotAllowedHttpException(['POST', 'PUT'], 'test message');
$event = new GetResponseForExceptionEvent($kernel->reveal(), $request, 'GET', $e);
$event = new GetResponseForExceptionEvent($kernel->reveal(), $request, 'GET', $exception);
$subscriber = new ExceptionJsonSubscriber();
$subscriber->on4xx($event);
$response = $event->getResponse();
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertInstanceOf($expected_response_class, $response);
$this->assertEquals('{"message":"test message"}', $response->getContent());
$this->assertEquals(405, $response->getStatusCode());
$this->assertEquals('POST, PUT', $response->headers->get('Allow'));
$this->assertEquals('application/json', $response->headers->get('Content-Type'));
}
public function providerTestOn4xx() {
return [
'uncacheable exception' => [
new MethodNotAllowedHttpException(['POST', 'PUT'], 'test message'),
JsonResponse::class
],
'cacheable exception' => [
new CacheableMethodNotAllowedHttpException((new CacheableMetadata())->setCacheContexts(['route']), ['POST', 'PUT'], 'test message'),
CacheableJsonResponse::class
],
];
}
}