diff --git a/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php b/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..19cb0f2b7664705dcc8707f1ee9c4f19be57149f --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php @@ -0,0 +1,67 @@ +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; + } + +} diff --git a/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php b/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php index 21b61b220a9bd826c709d92bbee56fbf0f59d358..fcbc11f8d1ecb83d58d2d54e6071a4a956a3c65e 100644 --- a/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php +++ b/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php @@ -7,47 +7,7 @@ */ 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} diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php index f168a02caddf12c99e55bbd4836a334a459f981e..8812583b66c855c1a3fa34b638e4b32fb02f19f5 100644 --- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php @@ -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 @@ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $st $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()); diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php index 6ccb743356de49dc3c874a7e1ca707ae313a4f6b..239e5821160fbed84ca38c0ea17db921a186b7fa 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php @@ -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 @@ protected static function getPriority() { 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); } diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..e0fd5cb671e5b0e1ff62a86b8dadff806841865d --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..97d432ae04495b08c0d9195554463209d4ebd8de --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..ca804fb3eb1718ba2f5779860e6389580006ec27 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..4568c91d7282b4c605aacc7e7ab3ed7175472be2 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..76f529eea011efad6cd5115ca217e7b241c474f4 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($statusCode, $message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..a75f80a6ca9138b8371f5ff6ad814db8ef94560f --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..d9919b1e1e56bce5b11f1d42c85acae9f7fa85d4 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($allow, $message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..94bf1c2a6c99cd189815e01660f43fa2297af0c8 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..9e5e136cd131a593d668541e7925af735fd4efe2 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..7921d3efc668be02419d89e5ee7cb1e2215521a9 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..d66b255ad8e762bbfbe3a00f71d7f59194895164 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..313b9ae58d6e65a476f221250133aec050311a83 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($retryAfter, $message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..e709c0b504fc31b7882bd29b04adf375e29349a3 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($retryAfter, $message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..35dbd72e37d190b0963d436243e28f48d12ccfd2 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($challenge, $message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..655c67a071a42d4ca9dc5eab28717e505fc1fae3 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php new file mode 100644 index 0000000000000000000000000000000000000000..c6f6023daf7197ac096859261a01ace5b55fa6c5 --- /dev/null +++ b/core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php @@ -0,0 +1,24 @@ +setCacheability($cacheability); + parent::__construct($message, $previous, $code); + } + +} diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php index c72e3f0c0168e24e49d9a694257d079d85910d09..01034f4a8c44d0406cf2900c8dd13868c70d5cda 100644 --- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php @@ -5,12 +5,13 @@ 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 @@ public function authenticate(Request $request) { * {@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); } } diff --git a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php index b197a7631a190f5c1ccdb57b61dc5e35ac5778cb..f844cb930531f6ff4d6edd75c7550450b633662c 100644 --- a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php +++ b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php @@ -7,6 +7,7 @@ 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 @@ public function testUnauthorizedErrorMessage() { $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. * diff --git a/core/modules/rest/tests/modules/rest_test/rest_test.services.yml b/core/modules/rest/tests/modules/rest_test/rest_test.services.yml index ccdbeae7385c1d677046dbc88e4a448d6fe57a58..d316cf6072b8d0169aee2c570dc035ff9e33b05a 100644 --- a/core/modules/rest/tests/modules/rest_test/rest_test.services.yml +++ b/core/modules/rest/tests/modules/rest_test/rest_test.services.yml @@ -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 } diff --git a/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php b/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php new file mode 100644 index 0000000000000000000000000000000000000000..17be647dfad34485637dcdb2cbdec1f0374dc609 --- /dev/null +++ b/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php @@ -0,0 +1,31 @@ +headers->has('REST-test-auth') || $request->headers->has('REST-test-auth-global')) { + return self::DENY; + } + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 6faf028c2b38a7990f82ee50a5d4124b797013e6..418ddfbb4a7902efaa69b6219150bfd94d2b6072 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -414,14 +414,20 @@ public function testGet() { ':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 @@ public function testGet() { } } $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)); diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php index 86c6d86bfd5f43ecd66264b44d2faa7013417f43..8f339a839dcdfa283050249f7fc05c69c2536053 100644 --- a/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php @@ -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 + ], + ]; + } + }