BigPipeTest.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. <?php
  2. namespace Drupal\Tests\big_pipe\Functional;
  3. use Behat\Mink\Element\NodeElement;
  4. use Drupal\big_pipe\Render\Placeholder\BigPipeStrategy;
  5. use Drupal\big_pipe\Render\BigPipe;
  6. use Drupal\big_pipe_test\BigPipePlaceholderTestCases;
  7. use Drupal\Component\Serialization\Json;
  8. use Drupal\Component\Utility\Html;
  9. use Drupal\Core\Database\Database;
  10. use Drupal\Core\Logger\RfcLogLevel;
  11. use Drupal\Core\Url;
  12. use Drupal\Tests\BrowserTestBase;
  13. /**
  14. * Tests BigPipe's no-JS detection & response delivery (with and without JS).
  15. *
  16. * Covers:
  17. * - big_pipe_page_attachments()
  18. * - \Drupal\big_pipe\Controller\BigPipeController
  19. * - \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
  20. * - \Drupal\big_pipe\Render\BigPipe
  21. *
  22. * @group big_pipe
  23. */
  24. class BigPipeTest extends BrowserTestBase {
  25. /**
  26. * Modules to enable.
  27. *
  28. * @var array
  29. */
  30. public static $modules = ['big_pipe', 'big_pipe_test', 'dblog'];
  31. /**
  32. * {@inheritdoc}
  33. */
  34. protected $defaultTheme = 'classy';
  35. /**
  36. * {@inheritdoc}
  37. */
  38. protected $dumpHeaders = TRUE;
  39. /**
  40. * {@inheritdoc}
  41. */
  42. protected function setUp() {
  43. parent::setUp();
  44. // Ignore the <meta> refresh that big_pipe.module sets. It causes a redirect
  45. // to a page that sets another cookie, which causes WebTestBase to lose the
  46. // session cookie. To avoid this problem, tests should first call
  47. // drupalGet() and then call checkForMetaRefresh() manually, and then reset
  48. // $this->maximumMetaRefreshCount and $this->metaRefreshCount.
  49. // @see doMetaRefresh()
  50. $this->maximumMetaRefreshCount = 0;
  51. }
  52. /**
  53. * Performs a single <meta> refresh explicitly.
  54. *
  55. * This test disables the automatic <meta> refresh checking, each time it is
  56. * desired that this runs, a test case must explicitly call this.
  57. *
  58. * @see setUp()
  59. */
  60. protected function performMetaRefresh() {
  61. $this->maximumMetaRefreshCount = 1;
  62. $this->checkForMetaRefresh();
  63. $this->maximumMetaRefreshCount = 0;
  64. $this->metaRefreshCount = 0;
  65. }
  66. /**
  67. * Tests BigPipe's no-JS detection.
  68. *
  69. * Covers:
  70. * - big_pipe_page_attachments()
  71. * - \Drupal\big_pipe\Controller\BigPipeController
  72. */
  73. public function testNoJsDetection() {
  74. $no_js_to_js_markup = '<script>document.cookie = "' . BigPipeStrategy::NOJS_COOKIE . '=1; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"</script>';
  75. // 1. No session (anonymous).
  76. $this->drupalGet(Url::fromRoute('<front>'));
  77. $this->assertSessionCookieExists(FALSE);
  78. $this->assertBigPipeNoJsCookieExists(FALSE);
  79. $this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL=');
  80. $this->assertNoRaw($no_js_to_js_markup);
  81. // 2. Session (authenticated).
  82. $this->drupalLogin($this->rootUser);
  83. $this->assertSessionCookieExists(TRUE);
  84. $this->assertBigPipeNoJsCookieExists(FALSE);
  85. $this->assertRaw('<noscript><meta http-equiv="Refresh" content="0; URL=' . base_path() . 'big_pipe/no-js?destination=' . base_path() . 'user/1" />' . "\n" . '</noscript>');
  86. $this->assertNoRaw($no_js_to_js_markup);
  87. $this->assertBigPipeNoJsMetaRefreshRedirect();
  88. $this->assertBigPipeNoJsCookieExists(TRUE);
  89. $this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL=');
  90. $this->assertRaw($no_js_to_js_markup);
  91. $this->drupalLogout();
  92. // Close the prior connection and remove the collected state.
  93. $this->getSession()->reset();
  94. // 3. Session (anonymous).
  95. $this->drupalGet(Url::fromRoute('user.login', [], ['query' => ['trigger_session' => 1]]));
  96. $this->drupalGet(Url::fromRoute('user.login'));
  97. $this->assertSessionCookieExists(TRUE);
  98. $this->assertBigPipeNoJsCookieExists(FALSE);
  99. $this->assertRaw('<noscript><meta http-equiv="Refresh" content="0; URL=' . base_path() . 'big_pipe/no-js?destination=' . base_path() . 'user/login" />' . "\n" . '</noscript>');
  100. $this->assertNoRaw($no_js_to_js_markup);
  101. $this->assertBigPipeNoJsMetaRefreshRedirect();
  102. $this->assertBigPipeNoJsCookieExists(TRUE);
  103. $this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL=');
  104. $this->assertRaw($no_js_to_js_markup);
  105. // Close the prior connection and remove the collected state.
  106. $this->getSession()->reset();
  107. // Edge case: route with '_no_big_pipe' option.
  108. $this->drupalGet(Url::fromRoute('no_big_pipe'));
  109. $this->assertSessionCookieExists(FALSE);
  110. $this->assertBigPipeNoJsCookieExists(FALSE);
  111. $this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL=');
  112. $this->assertNoRaw($no_js_to_js_markup);
  113. $this->drupalLogin($this->rootUser);
  114. $this->drupalGet(Url::fromRoute('no_big_pipe'));
  115. $this->assertSessionCookieExists(TRUE);
  116. $this->assertBigPipeNoJsCookieExists(FALSE);
  117. $this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL=');
  118. $this->assertNoRaw($no_js_to_js_markup);
  119. }
  120. /**
  121. * Tests BigPipe-delivered HTML responses when JavaScript is enabled.
  122. *
  123. * Covers:
  124. * - \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
  125. * - \Drupal\big_pipe\Render\BigPipe
  126. * - \Drupal\big_pipe\Render\BigPipe::sendPlaceholders()
  127. *
  128. * @see \Drupal\big_pipe_test\BigPipePlaceholderTestCases
  129. */
  130. public function testBigPipe() {
  131. // Simulate production.
  132. $this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save();
  133. $this->drupalLogin($this->rootUser);
  134. $this->assertSessionCookieExists(TRUE);
  135. $this->assertBigPipeNoJsCookieExists(FALSE);
  136. $connection = Database::getConnection();
  137. $log_count = $connection->select('watchdog')->countQuery()->execute()->fetchField();
  138. // By not calling performMetaRefresh() here, we simulate JavaScript being
  139. // enabled, because as far as the BigPipe module is concerned, JavaScript is
  140. // enabled in the browser as long as the BigPipe no-JS cookie is *not* set.
  141. // @see setUp()
  142. // @see performMetaRefresh()
  143. $this->drupalGet(Url::fromRoute('big_pipe_test'));
  144. $this->assertBigPipeResponseHeadersPresent();
  145. $this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'cache_tag_set_in_lazy_builder');
  146. $this->setCsrfTokenSeedInTestEnvironment();
  147. $cases = $this->getTestCases();
  148. $this->assertBigPipeNoJsPlaceholders([
  149. $cases['edge_case__invalid_html']->bigPipeNoJsPlaceholder => $cases['edge_case__invalid_html']->embeddedHtmlResponse,
  150. $cases['html_attribute_value']->bigPipeNoJsPlaceholder => $cases['html_attribute_value']->embeddedHtmlResponse,
  151. $cases['html_attribute_value_subset']->bigPipeNoJsPlaceholder => $cases['html_attribute_value_subset']->embeddedHtmlResponse,
  152. ]);
  153. $this->assertBigPipePlaceholders([
  154. $cases['html']->bigPipePlaceholderId => Json::encode($cases['html']->embeddedAjaxResponseCommands),
  155. $cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderId => Json::encode($cases['edge_case__html_non_lazy_builder']->embeddedAjaxResponseCommands),
  156. $cases['exception__lazy_builder']->bigPipePlaceholderId => NULL,
  157. $cases['exception__embedded_response']->bigPipePlaceholderId => NULL,
  158. ], [
  159. 0 => $cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderId,
  160. // The 'html' case contains the 'status messages' placeholder, which is
  161. // always rendered last.
  162. 1 => $cases['html']->bigPipePlaceholderId,
  163. ]);
  164. $this->assertRaw('</body>', 'Closing body tag present.');
  165. // Verifying BigPipe assets are present.
  166. $this->assertFalse(empty($this->getDrupalSettings()), 'drupalSettings present.');
  167. $this->assertContains('big_pipe/big_pipe', explode(',', $this->getDrupalSettings()['ajaxPageState']['libraries']), 'BigPipe asset library is present.');
  168. // Verify that the two expected exceptions are logged as errors.
  169. $this->assertEqual($log_count + 2, (int) $connection->select('watchdog')->countQuery()->execute()->fetchField(), 'Two new watchdog entries.');
  170. // Using dynamic select queries with the method range() allows contrib
  171. // database drivers the ability to insert their own limit and offset
  172. // functionality.
  173. $records = $connection->select('watchdog', 'w')->fields('w')->orderBy('wid', 'DESC')->range(0, 2)->execute()->fetchAll();
  174. $this->assertEqual(RfcLogLevel::ERROR, $records[0]->severity);
  175. $this->assertStringContainsString('Oh noes!', (string) unserialize($records[0]->variables)['@message']);
  176. $this->assertEqual(RfcLogLevel::ERROR, $records[1]->severity);
  177. $this->assertStringContainsString('You are not allowed to say llamas are not cool!', (string) unserialize($records[1]->variables)['@message']);
  178. // Verify that 4xx responses work fine. (4xx responses are handled by
  179. // subrequests to a route pointing to a controller with the desired output.)
  180. $this->drupalGet(Url::fromUri('base:non-existing-path'));
  181. // Simulate development.
  182. // Verifying BigPipe provides useful error output when an error occurs
  183. // while rendering a placeholder if verbose error logging is enabled.
  184. $this->config('system.logging')->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save();
  185. $this->drupalGet(Url::fromRoute('big_pipe_test'));
  186. // The 'edge_case__html_exception' case throws an exception.
  187. $this->assertRaw('The website encountered an unexpected error. Please try again later');
  188. $this->assertRaw('You are not allowed to say llamas are not cool!');
  189. $this->assertNoRaw(BigPipe::STOP_SIGNAL, 'BigPipe stop signal absent: error occurred before then.');
  190. $this->assertNoRaw('</body>', 'Closing body tag absent: error occurred before then.');
  191. // The exception is expected. Do not interpret it as a test failure.
  192. unlink($this->root . '/' . $this->siteDirectory . '/error.log');
  193. }
  194. /**
  195. * Tests BigPipe-delivered HTML responses when JavaScript is disabled.
  196. *
  197. * Covers:
  198. * - \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
  199. * - \Drupal\big_pipe\Render\BigPipe
  200. * - \Drupal\big_pipe\Render\BigPipe::sendNoJsPlaceholders()
  201. *
  202. * @see \Drupal\big_pipe_test\BigPipePlaceholderTestCases
  203. */
  204. public function testBigPipeNoJs() {
  205. // Simulate production.
  206. $this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save();
  207. $this->drupalLogin($this->rootUser);
  208. $this->assertSessionCookieExists(TRUE);
  209. $this->assertBigPipeNoJsCookieExists(FALSE);
  210. // By calling performMetaRefresh() here, we simulate JavaScript being
  211. // disabled, because as far as the BigPipe module is concerned, it is
  212. // enabled in the browser when the BigPipe no-JS cookie is set.
  213. // @see setUp()
  214. // @see performMetaRefresh()
  215. $this->performMetaRefresh();
  216. $this->assertBigPipeNoJsCookieExists(TRUE);
  217. $this->drupalGet(Url::fromRoute('big_pipe_test'));
  218. $this->assertBigPipeResponseHeadersPresent();
  219. $this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'cache_tag_set_in_lazy_builder');
  220. $this->setCsrfTokenSeedInTestEnvironment();
  221. $cases = $this->getTestCases();
  222. $this->assertBigPipeNoJsPlaceholders([
  223. $cases['edge_case__invalid_html']->bigPipeNoJsPlaceholder => $cases['edge_case__invalid_html']->embeddedHtmlResponse,
  224. $cases['html_attribute_value']->bigPipeNoJsPlaceholder => $cases['html_attribute_value']->embeddedHtmlResponse,
  225. $cases['html_attribute_value_subset']->bigPipeNoJsPlaceholder => $cases['html_attribute_value_subset']->embeddedHtmlResponse,
  226. $cases['html']->bigPipeNoJsPlaceholder => $cases['html']->embeddedHtmlResponse,
  227. $cases['edge_case__html_non_lazy_builder']->bigPipeNoJsPlaceholder => $cases['edge_case__html_non_lazy_builder']->embeddedHtmlResponse,
  228. $cases['exception__lazy_builder']->bigPipePlaceholderId => NULL,
  229. $cases['exception__embedded_response']->bigPipePlaceholderId => NULL,
  230. ]);
  231. // Verifying there are no BigPipe placeholders & replacements.
  232. $this->assertEqual('<none>', $this->drupalGetHeader('BigPipe-Test-Placeholders'));
  233. // Verifying BigPipe start/stop signals are absent.
  234. $this->assertNoRaw(BigPipe::START_SIGNAL, 'BigPipe start signal absent.');
  235. $this->assertNoRaw(BigPipe::STOP_SIGNAL, 'BigPipe stop signal absent.');
  236. // Verifying BigPipe assets are absent.
  237. $this->assertTrue(!isset($this->getDrupalSettings()['bigPipePlaceholderIds']) && empty($this->getDrupalSettings()['ajaxPageState']), 'BigPipe drupalSettings and BigPipe asset library absent.');
  238. $this->assertRaw('</body>', 'Closing body tag present.');
  239. // Verify that 4xx responses work fine. (4xx responses are handled by
  240. // subrequests to a route pointing to a controller with the desired output.)
  241. $this->drupalGet(Url::fromUri('base:non-existing-path'));
  242. // Simulate development.
  243. // Verifying BigPipe provides useful error output when an error occurs
  244. // while rendering a placeholder if verbose error logging is enabled.
  245. $this->config('system.logging')->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save();
  246. $this->drupalGet(Url::fromRoute('big_pipe_test'));
  247. // The 'edge_case__html_exception' case throws an exception.
  248. $this->assertRaw('The website encountered an unexpected error. Please try again later');
  249. $this->assertRaw('You are not allowed to say llamas are not cool!');
  250. $this->assertNoRaw('</body>', 'Closing body tag absent: error occurred before then.');
  251. // The exception is expected. Do not interpret it as a test failure.
  252. unlink($this->root . '/' . $this->siteDirectory . '/error.log');
  253. }
  254. /**
  255. * Tests BigPipe with a multi-occurrence placeholder.
  256. */
  257. public function testBigPipeMultiOccurrencePlaceholders() {
  258. $this->drupalLogin($this->rootUser);
  259. $this->assertSessionCookieExists(TRUE);
  260. $this->assertBigPipeNoJsCookieExists(FALSE);
  261. // By not calling performMetaRefresh() here, we simulate JavaScript being
  262. // enabled, because as far as the BigPipe module is concerned, JavaScript is
  263. // enabled in the browser as long as the BigPipe no-JS cookie is *not* set.
  264. // @see setUp()
  265. // @see performMetaRefresh()
  266. $this->drupalGet(Url::fromRoute('big_pipe_test_multi_occurrence'));
  267. $big_pipe_placeholder_id = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA';
  268. $expected_placeholder_replacement = '<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="' . $big_pipe_placeholder_id . '">';
  269. $this->assertRaw('The count is 1.');
  270. $this->assertNoRaw('The count is 2.');
  271. $this->assertNoRaw('The count is 3.');
  272. $raw_content = $this->getSession()->getPage()->getContent();
  273. $this->assertTrue(substr_count($raw_content, $expected_placeholder_replacement) == 1, 'Only one placeholder replacement was found for the duplicate #lazy_builder arrays.');
  274. // By calling performMetaRefresh() here, we simulate JavaScript being
  275. // disabled, because as far as the BigPipe module is concerned, it is
  276. // enabled in the browser when the BigPipe no-JS cookie is set.
  277. // @see setUp()
  278. // @see performMetaRefresh()
  279. $this->performMetaRefresh();
  280. $this->assertBigPipeNoJsCookieExists(TRUE);
  281. $this->drupalGet(Url::fromRoute('big_pipe_test_multi_occurrence'));
  282. $this->assertRaw('The count is 1.');
  283. $this->assertNoRaw('The count is 2.');
  284. $this->assertNoRaw('The count is 3.');
  285. }
  286. protected function assertBigPipeResponseHeadersPresent() {
  287. // Check that Cache-Control header set to "private".
  288. $this->assertSession()->responseHeaderContains('Cache-Control', 'private');
  289. $this->assertEqual('no-store, content="BigPipe/1.0"', $this->drupalGetHeader('Surrogate-Control'));
  290. $this->assertEqual('no', $this->drupalGetHeader('X-Accel-Buffering'));
  291. }
  292. /**
  293. * Asserts expected BigPipe no-JS placeholders are present and replaced.
  294. *
  295. * @param array $expected_big_pipe_nojs_placeholders
  296. * Keys: BigPipe no-JS placeholder markup. Values: expected replacement
  297. * markup.
  298. */
  299. protected function assertBigPipeNoJsPlaceholders(array $expected_big_pipe_nojs_placeholders) {
  300. $this->assertSetsEqual(array_keys($expected_big_pipe_nojs_placeholders), array_map('rawurldecode', explode(' ', $this->drupalGetHeader('BigPipe-Test-No-Js-Placeholders'))));
  301. foreach ($expected_big_pipe_nojs_placeholders as $big_pipe_nojs_placeholder => $expected_replacement) {
  302. // Checking whether the replacement for the BigPipe no-JS placeholder
  303. // $big_pipe_nojs_placeholder is present.
  304. $this->assertNoRaw($big_pipe_nojs_placeholder);
  305. if ($expected_replacement !== NULL) {
  306. $this->assertRaw($expected_replacement);
  307. }
  308. }
  309. }
  310. /**
  311. * Asserts expected BigPipe placeholders are present and replaced.
  312. *
  313. * @param array $expected_big_pipe_placeholders
  314. * Keys: BigPipe placeholder IDs. Values: expected AJAX response.
  315. * @param array $expected_big_pipe_placeholder_stream_order
  316. * Keys: BigPipe placeholder IDs. Values: expected AJAX response. Keys are
  317. * defined in the order that they are expected to be rendered & streamed.
  318. */
  319. protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholders, array $expected_big_pipe_placeholder_stream_order) {
  320. $this->assertSetsEqual(array_keys($expected_big_pipe_placeholders), explode(' ', $this->drupalGetHeader('BigPipe-Test-Placeholders')));
  321. $placeholder_positions = [];
  322. $placeholder_replacement_positions = [];
  323. foreach ($expected_big_pipe_placeholders as $big_pipe_placeholder_id => $expected_ajax_response) {
  324. // Verify expected placeholder.
  325. $expected_placeholder_html = '<span data-big-pipe-placeholder-id="' . $big_pipe_placeholder_id . '"></span>';
  326. $this->assertRaw($expected_placeholder_html, 'BigPipe placeholder for placeholder ID "' . $big_pipe_placeholder_id . '" found.');
  327. $pos = strpos($this->getSession()->getPage()->getContent(), $expected_placeholder_html);
  328. $placeholder_positions[$pos] = $big_pipe_placeholder_id;
  329. // Verify expected placeholder replacement.
  330. $expected_placeholder_replacement = '<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="' . $big_pipe_placeholder_id . '">';
  331. $result = $this->xpath('//script[@data-big-pipe-replacement-for-placeholder-with-id=:id]', [':id' => Html::decodeEntities($big_pipe_placeholder_id)]);
  332. if ($expected_ajax_response === NULL) {
  333. $this->assertCount(0, $result);
  334. $this->assertNoRaw($expected_placeholder_replacement);
  335. continue;
  336. }
  337. $this->assertEqual($expected_ajax_response, trim($result[0]->getText()));
  338. $this->assertRaw($expected_placeholder_replacement);
  339. $pos = strpos($this->getSession()->getPage()->getContent(), $expected_placeholder_replacement);
  340. $placeholder_replacement_positions[$pos] = $big_pipe_placeholder_id;
  341. }
  342. ksort($placeholder_positions, SORT_NUMERIC);
  343. $this->assertEqual(array_keys($expected_big_pipe_placeholders), array_values($placeholder_positions));
  344. $placeholders = array_map(function (NodeElement $element) {
  345. return $element->getAttribute('data-big-pipe-placeholder-id');
  346. }, $this->cssSelect('[data-big-pipe-placeholder-id]'));
  347. $this->assertEqual(count($expected_big_pipe_placeholders), count(array_unique($placeholders)));
  348. $expected_big_pipe_placeholders_with_replacements = [];
  349. foreach ($expected_big_pipe_placeholder_stream_order as $big_pipe_placeholder_id) {
  350. $expected_big_pipe_placeholders_with_replacements[$big_pipe_placeholder_id] = $expected_big_pipe_placeholders[$big_pipe_placeholder_id];
  351. }
  352. $this->assertEqual($expected_big_pipe_placeholders_with_replacements, array_filter($expected_big_pipe_placeholders));
  353. $this->assertSetsEqual(array_keys($expected_big_pipe_placeholders_with_replacements), array_values($placeholder_replacement_positions));
  354. $this->assertEqual(count($expected_big_pipe_placeholders_with_replacements), preg_match_all('/' . preg_quote('<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="', '/') . '/', $this->getSession()->getPage()->getContent()));
  355. // Verifying BigPipe start/stop signals.
  356. $this->assertRaw(BigPipe::START_SIGNAL, 'BigPipe start signal present.');
  357. $this->assertRaw(BigPipe::STOP_SIGNAL, 'BigPipe stop signal present.');
  358. $start_signal_position = strpos($this->getSession()->getPage()->getContent(), BigPipe::START_SIGNAL);
  359. $stop_signal_position = strpos($this->getSession()->getPage()->getContent(), BigPipe::STOP_SIGNAL);
  360. $this->assertTrue($start_signal_position < $stop_signal_position, 'BigPipe start signal appears before stop signal.');
  361. // Verifying BigPipe placeholder replacements and start/stop signals were
  362. // streamed in the correct order.
  363. $expected_stream_order = array_keys($expected_big_pipe_placeholders_with_replacements);
  364. array_unshift($expected_stream_order, BigPipe::START_SIGNAL);
  365. array_push($expected_stream_order, BigPipe::STOP_SIGNAL);
  366. $actual_stream_order = $placeholder_replacement_positions + [
  367. $start_signal_position => BigPipe::START_SIGNAL,
  368. $stop_signal_position => BigPipe::STOP_SIGNAL,
  369. ];
  370. ksort($actual_stream_order, SORT_NUMERIC);
  371. $this->assertEqual($expected_stream_order, array_values($actual_stream_order));
  372. }
  373. /**
  374. * Ensures CSRF tokens can be generated for the current user's session.
  375. */
  376. protected function setCsrfTokenSeedInTestEnvironment() {
  377. $session_data = $this->container->get('session_handler.write_safe')->read($this->getSession()->getCookie($this->getSessionName()));
  378. $csrf_token_seed = unserialize(explode('_sf2_meta|', $session_data)[1])['s'];
  379. $this->container->get('session_manager.metadata_bag')->setCsrfTokenSeed($csrf_token_seed);
  380. }
  381. /**
  382. * @return \Drupal\big_pipe_test\BigPipePlaceholderTestCase[]
  383. */
  384. protected function getTestCases($has_session = TRUE) {
  385. return BigPipePlaceholderTestCases::cases($this->container, $this->rootUser);
  386. }
  387. /**
  388. * Asserts whether arrays A and B are equal, when treated as sets.
  389. */
  390. protected function assertSetsEqual(array $a, array $b) {
  391. return count($a) == count($b) && !array_diff_assoc($a, $b);
  392. }
  393. /**
  394. * Asserts whether a BigPipe no-JS cookie exists or not.
  395. */
  396. protected function assertBigPipeNoJsCookieExists($expected) {
  397. $this->assertCookieExists('big_pipe_nojs', $expected, 'BigPipe no-JS');
  398. }
  399. /**
  400. * Asserts whether a session cookie exists or not.
  401. */
  402. protected function assertSessionCookieExists($expected) {
  403. $this->assertCookieExists($this->getSessionName(), $expected, 'Session');
  404. }
  405. /**
  406. * Asserts whether a cookie exists on the client or not.
  407. */
  408. protected function assertCookieExists($cookie_name, $expected, $cookie_label) {
  409. $this->assertEqual($expected, !empty($this->getSession()->getCookie($cookie_name)), $expected ? "$cookie_label cookie exists." : "$cookie_label cookie does not exist.");
  410. }
  411. /**
  412. * Calls ::performMetaRefresh() and asserts the responses.
  413. */
  414. protected function assertBigPipeNoJsMetaRefreshRedirect() {
  415. $original_url = $this->getSession()->getCurrentUrl();
  416. // Disable automatic following of redirects by the HTTP client, so that this
  417. // test can analyze the response headers of each redirect response.
  418. $this->getSession()->getDriver()->getClient()->followRedirects(FALSE);
  419. $this->performMetaRefresh();
  420. $headers[0] = $this->getSession()->getResponseHeaders();
  421. $statuses[0] = $this->getSession()->getStatusCode();
  422. $this->performMetaRefresh();
  423. $headers[1] = $this->getSession()->getResponseHeaders();
  424. $statuses[1] = $this->getSession()->getStatusCode();
  425. $this->getSession()->getDriver()->getClient()->followRedirects(TRUE);
  426. $this->assertEqual($original_url, $this->getSession()->getCurrentUrl(), 'Redirected back to the original location.');
  427. // First response: redirect.
  428. $this->assertEqual(302, $statuses[0], 'The first response was a 302 (redirect).');
  429. $this->assertStringStartsWith('big_pipe_nojs=1', $headers[0]['Set-Cookie'][0], 'The first response sets the big_pipe_nojs cookie.');
  430. $this->assertEqual($original_url, $headers[0]['Location'][0], 'The first response redirected back to the original page.');
  431. $this->assertTrue(empty(array_diff(['cookies:big_pipe_nojs', 'session.exists'], explode(' ', $headers[0]['X-Drupal-Cache-Contexts'][0]))), 'The first response varies by the "cookies:big_pipe_nojs" and "session.exists" cache contexts.');
  432. $this->assertFalse(isset($headers[0]['Surrogate-Control']), 'The first response has no "Surrogate-Control" header.');
  433. // Second response: redirect followed.
  434. $this->assertEqual(200, $statuses[1], 'The second response was a 200.');
  435. $this->assertTrue(empty(array_diff(['cookies:big_pipe_nojs', 'session.exists'], explode(' ', $headers[0]['X-Drupal-Cache-Contexts'][0]))), 'The first response varies by the "cookies:big_pipe_nojs" and "session.exists" cache contexts.');
  436. $this->assertEqual('no-store, content="BigPipe/1.0"', $headers[1]['Surrogate-Control'][0], 'The second response has a "Surrogate-Control" header.');
  437. $this->assertNoRaw('<noscript><meta http-equiv="Refresh" content="0; URL=', 'Once the BigPipe no-JS cookie is set, the <meta> refresh is absent: only one redirect ever happens.');
  438. }
  439. }