core/modules/big_pipe/src/Tests/BigPipeTest.php | 131 +++++++++++++++++++-- .../big_pipe_test/src/BigPipeTestController.php | 17 ++- 2 files changed, 136 insertions(+), 12 deletions(-) diff --git a/core/modules/big_pipe/src/Tests/BigPipeTest.php b/core/modules/big_pipe/src/Tests/BigPipeTest.php index 370b815..4fc3654 100644 --- a/core/modules/big_pipe/src/Tests/BigPipeTest.php +++ b/core/modules/big_pipe/src/Tests/BigPipeTest.php @@ -6,6 +6,7 @@ use Drupal\big_pipe\Render\BigPipe; use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Html; +use Drupal\Core\Cache\Cache; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Url; use Drupal\simpletest\WebTestBase; @@ -157,7 +158,8 @@ public function testBigPipe() { // @see performMetaRefresh() $this->drupalGet(Url::fromRoute('big_pipe_test')); - $this->assertBigPipeResponseHeadersPresent(); + $this->assertBigPipeResponseHeadersPresent('private', 'no-store'); + $this->assertNoCacheTag('cache_tag_set_in_lazy_builder'); $cases = $this->getTestCases(); $this->assertBigPipeNoJsPlaceholders([ @@ -230,7 +232,8 @@ public function testBigPipeNoJs() { $this->assertBigPipeNoJsCookieExists(TRUE); $this->drupalGet(Url::fromRoute('big_pipe_test')); - $this->assertBigPipeResponseHeadersPresent(); + $this->assertBigPipeResponseHeadersPresent('private', 'no-store'); + $this->assertNoCacheTag('cache_tag_set_in_lazy_builder'); $cases = $this->getTestCases(); $this->assertBigPipeNoJsPlaceholders([ @@ -269,6 +272,102 @@ public function testBigPipeNoJs() { unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log'); } + /** + * Tests BigPipe-delivered HTML responses for requests with no session. + * + * Covers: + * - \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber + * - \Drupal\big_pipe\Render\BigPipe + * - \Drupal\big_pipe\Render\BigPipe::sendNoJsPlaceholders() + * + * @see \Drupal\big_pipe\Tests\BigPipePlaceholderTestCases + */ + public function testBigPipeNoSession() { + $cases = [ + 'default' => [ + 'configured max-age' => 0, + 'cache control visibility' => 'private', + 'surrogate control directive' => 'no-store', + 'page cache hit cache control' => 'must-revalidate, no-cache, private', + ], + 'max-age=300' => [ + 'configured max-age' => 300, + 'cache control visibility' => 'public', + 'surrogate control directive' => 'max-age=300', + 'page cache hit cache control' => 'max-age=300, public', + ], + ]; + + foreach ($cases as $case => $expectations) { + $this->pass("No-session test case: $case"); + + // Simulate production. + $this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save(); + $this->config('system.performance')->set('cache.page.max_age', $expectations['configured max-age'])->save(); + + $this->assertSessionCookieExists(FALSE); + $this->assertBigPipeNoJsCookieExists(FALSE); + + $this->pass('First request: Page Cache miss, streamed response by BigPipe', 'Debug'); + $this->drupalGet(Url::fromRoute('big_pipe_test')); + $this->assertIdentical('MISS', $this->drupalGetHeader('X-Drupal-Cache'), 'Page cache miss.'); + $this->assertBigPipeResponseHeadersPresent($expectations['cache control visibility'], $expectations['surrogate control directive']); + $this->assertNoCacheTag('cache_tag_set_in_lazy_builder'); + + $cases = $this->getTestCases(FALSE); + $this->assertBigPipeNoJsPlaceholders([ + $cases['edge_case__invalid_html']->bigPipeNoJsPlaceholder => $cases['edge_case__invalid_html']->embeddedHtmlResponse, + $cases['html_attribute_value']->bigPipeNoJsPlaceholder => $cases['html_attribute_value']->embeddedHtmlResponse, + $cases['html']->bigPipeNoJsPlaceholder => NULL, + $cases['edge_case__html_non_lazy_builder']->bigPipeNoJsPlaceholder => $cases['edge_case__html_non_lazy_builder']->embeddedHtmlResponse, + $cases['exception__lazy_builder']->bigPipePlaceholderId => NULL, + $cases['exception__embedded_response']->bigPipePlaceholderId => NULL, + ]); + + $this->pass('Verifying there are no BigPipe placeholders & replacements…', 'Debug'); + $this->assertEqual('', $this->drupalGetHeader('BigPipe-Test-Placeholders')); + $this->pass('Verifying BigPipe start/stop signals are absent…', 'Debug'); + $this->assertNoRaw(BigPipe::START_SIGNAL, 'BigPipe start signal absent.'); + $this->assertNoRaw(BigPipe::STOP_SIGNAL, 'BigPipe stop signal absent.'); + + $this->pass('Verifying BigPipe assets are absent…', 'Debug'); + $this->assertTrue(empty($this->getDrupalSettings()), 'drupalSettings and BigPipe asset library absent.'); + $this->assertRaw('', 'Closing body tag present.'); + + + $this->pass('Repeat request: Page Cache hit, BigPipe not involved', 'Debug'); + $this->drupalGet(Url::fromRoute('big_pipe_test')); + $this->assertIdentical('HIT', $this->drupalGetHeader('X-Drupal-Cache'), 'Page cache hit.'); + $this->assertIdentical($expectations['page cache hit cache control'], $this->drupalGetHeader('Cache-Control')); + $this->assertFalse($this->drupalGetHeader('Surrogate-Control'), 'No Surrogate-Control header.'); + $this->assertFalse($this->drupalGetHeader('X-Accel-Buffering'), 'No X-Accel-Buffering header.'); + $this->assertNoCacheTag('cache_tag_set_in_lazy_builder'); + + + // Clear the Page Cache. Note that we use a cache tag that exists on this + // response, despite it being absent from both the Page Cache miss + // response (because it was streamed) and the Page Cache hit response + // (because it is identical to the original Page Cache miss response, + // minus the streaming). + Cache::invalidateTags(['cache_tag_set_in_lazy_builder']); + + + // Simulate development. + $this->pass('Verifying BigPipe provides useful error output when an error occurs while rendering a placeholder if verbose error logging is enabled.', 'Debug'); + $this->config('system.logging')->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save(); + $this->drupalGet(Url::fromRoute('big_pipe_test')); + // The 'edge_case__html_exception' case throws an exception. + $this->assertRaw('The website encountered an unexpected error. Please try again later'); + $this->assertRaw('You are not allowed to say llamas are not cool!'); + $this->assertNoRaw('', 'Closing body tag absent: error occurred before then.'); + // The exception is expected. Do not interpret it as a test failure. + unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log'); + + // Clear the Page Cache. + Cache::invalidateTags(['rendered']); + } + } + /** * Tests BigPipe with a multi-occurrence placeholder. */ @@ -305,10 +404,20 @@ public function testBigPipeMultiOccurrencePlaceholders() { $this->assertNoRaw('The count is 3.'); } - protected function assertBigPipeResponseHeadersPresent() { + /** + * Asserts expected BigPipe response headers. + * + * @param $cache_control_visibility + * Either 'private' or 'public' (the latter only in case of no-session + * BigPipe responses). + * @param $surrogate_control_directive + * Either 'no-store' or 'max-age=N' (the latter only in case of no-session, + * cacheable BigPipe responses). + */ + protected function assertBigPipeResponseHeadersPresent($cache_control_visibility, $surrogate_control_directive) { $this->pass('Verifying BigPipe response headers…', 'Debug'); - $this->assertTrue(FALSE !== strpos($this->drupalGetHeader('Cache-Control'), 'private'), 'Cache-Control header set to "private".'); - $this->assertEqual('no-store, content="BigPipe/1.0"', $this->drupalGetHeader('Surrogate-Control')); + $this->assertTrue(FALSE !== strpos($this->drupalGetHeader('Cache-Control'), $cache_control_visibility), 'Cache-Control header set to "' . $cache_control_visibility . '".'); + $this->assertEqual($surrogate_control_directive . ', content="BigPipe/1.0"', $this->drupalGetHeader('Surrogate-Control')); $this->assertEqual('no', $this->drupalGetHeader('X-Accel-Buffering')); } @@ -391,11 +500,13 @@ protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholde /** * @return \Drupal\big_pipe\Tests\BigPipePlaceholderTestCase[] */ - protected function getTestCases() { - // Ensure we can generate CSRF tokens for the current user's session. - $session_data = $this->container->get('session_handler.write_safe')->read($this->cookies[$this->getSessionName()]['value']); - $csrf_token_seed = unserialize(explode('_sf2_meta|', $session_data)[1])['s']; - $this->container->get('session_manager.metadata_bag')->setCsrfTokenSeed($csrf_token_seed); + protected function getTestCases($has_session = TRUE) { + if ($has_session) { + // Ensure we can generate CSRF tokens for the current user's session. + $session_data = $this->container->get('session_handler.write_safe')->read($this->cookies[$this->getSessionName()]['value']); + $csrf_token_seed = unserialize(explode('_sf2_meta|', $session_data)[1])['s']; + $this->container->get('session_manager.metadata_bag')->setCsrfTokenSeed($csrf_token_seed); + } return BigPipePlaceholderTestCases::cases($this->container, $this->rootUser); } diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php index 30594a5..51ccb1c 100644 --- a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php +++ b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php @@ -14,13 +14,19 @@ class BigPipeTestController { * @return array */ public function test() { + $has_session = \Drupal::service('session_configuration')->hasSession(\Drupal::requestStack()->getMasterRequest()); + $build = []; $cases = BigPipePlaceholderTestCases::cases(\Drupal::getContainer()); // 1. HTML placeholder: status messages. Drupal renders those automatically, // so all that we need to do in this controller is set a message. - drupal_set_message('Hello from BigPipe!'); + if ($has_session) { + // Only set a message if a session already exists, otherwise we always + // trigger a session, which means we can't test no-session requests. + drupal_set_message('Hello from BigPipe!'); + } $build['html'] = $cases['html']->renderArray; // 2. HTML attribute value placeholder: form action. @@ -28,6 +34,10 @@ public function test() { // 3. HTML attribute value subset placeholder: CSRF token in link. $build['html_attribute_value_subset'] = $cases['html_attribute_value_subset']->renderArray; + // We can't test CSRF tokens for no-session requests. + if (!$has_session) { + unset($build['html_attribute_value_subset']); + } // 4. Edge case: custom string to be considered as a placeholder that // happens to not be valid HTML. @@ -98,7 +108,10 @@ public static function currentTime() { public static function helloOrYarhar() { return [ '#markup' => BigPipeMarkup::create('Yarhar llamas forever!'), - '#cache' => ['max-age' => 0], + '#cache' => [ + 'max-age' => 0, + 'tags' => ['cache_tag_set_in_lazy_builder'], + ], ]; }